Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/components/Sidebar/PlanUsageBadge.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { getCachedPlanData } from 'lib/planDataCache';
import { getUsageBadge } from 'dashboard/Data/AppOverview/usageAlert';
import styles from 'components/Sidebar/PlanUsageBadge.scss';

// "84%" next to Plan Usage in the sidebar, following the banner's rules: Free yellow above 70%
// and red above 90%; MVP yellow above 70%; Pay As You Go neutral once over its included usage.
// Legacy plans follow the plan of their size.
// "84%" next to Plan Usage in the sidebar, following the banner's rules on every plan:
// yellow above 70% and red above 90%.
const PlanUsageBadge = () => {
const context = useContext(CurrentApp);
const [badge, setBadge] = useState(null);
Expand Down
6 changes: 0 additions & 6 deletions src/components/Sidebar/PlanUsageBadge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@
color: #FBFF3B;
}

.info {
border-color: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.06);
color: #c3cad6;
}

.danger {
border-color: rgba(232, 92, 62, 0.6);
background: rgba(232, 92, 62, 0.12);
Expand Down
66 changes: 55 additions & 11 deletions src/components/UpgradeCheckout/UpgradeCheckout.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SUPPORT_TICKET_URL, UpgradeEvent, UpgradeGate, UpgradeGateView, logGate
import { Cycle, initPaddle, paddlePlanId, paddlePriceId, recordSubscription } from 'lib/paddleCheckout';
import { prices } from 'dashboard/AppPlan/prices';
import { getCachedPlanData } from 'lib/planDataCache';
import { planKindOf } from 'dashboard/Data/AppOverview/usageAlert';
import { upgradeTargetOf } from 'dashboard/Data/AppOverview/usageAlert';
import styles from './UpgradeCheckout.scss';

const CHECKOUT_FRAME_CLASS = 'upgrade-checkout-frame';
Expand Down Expand Up @@ -55,7 +55,7 @@ const GATE_OFFERS = {
headline: 'Get the latest MongoDB',
subline: 'Faster queries and the newest MongoDB features.',
},
// Overview usage banner: near or over a Free limit, where the app gets blocked.
// Usage banner on Free, where the app gets blocked. Paid plans: USAGE_LIMIT_OFFERS below.
[UpgradeGate.USAGE_LIMIT]: {
plan: 'MVP',
headline: 'Keep your app running',
Expand Down Expand Up @@ -96,6 +96,45 @@ const GATE_OFFERS = {
[UpgradeGate.COMPLIANCE_HIPAA]: { plan: 'Dedicated', headline: 'Pass your customer\'s security review', subline: 'HIPAA-ready infrastructure, after a signed BAA.' },
};

// Usage banner on paid plans: the caller names the next plan up, which raises the limits again.
const USAGE_LIMIT_OFFERS = {
'Pay As You Go': {
plan: 'Pay As You Go',
headline: 'Get more room to grow',
subline: 'Pay As You Go raises your limits to 5M requests, 3 GB of database and 250 GB of files.',
},
Dedicated: {
plan: 'Dedicated',
headline: 'Stop counting requests',
subline: 'Dedicated has unlimited requests, 8 GB of database and 1 TB of files.',
},
};

// The overview's Plan Usage card on paid plans: the next plan up as a whole.
const NEXT_PLAN_AS_A_WHOLE = {
'Pay As You Go': {
plan: 'Pay As You Go',
headline: 'Give this app room to scale',
subline: 'Custom domain with HTTPS, SOC 2 and ISO 27001, and 10x the requests.',
},
Dedicated: {
plan: 'Dedicated',
headline: 'Run this app on dedicated resources',
subline: 'Unlimited requests, point-in-time backups and HIPAA-ready infrastructure.',
},
};

// Gates that sell more than one plan: the caller names the plan, GATE_OFFERS is the default.
const OFFERS_BY_PLAN = {
[UpgradeGate.USAGE_LIMIT]: USAGE_LIMIT_OFFERS,
[UpgradeGate.OVERVIEW_PLAN_CARD]: NEXT_PLAN_AS_A_WHOLE,
};

// The offer for a paywall. `plan` picks the plan when the same gate sells more than one.
const offerFor = (gate, plan) => (OFFERS_BY_PLAN[gate] && OFFERS_BY_PLAN[gate][plan]) || GATE_OFFERS[gate];

const planIndex = name => prices.findIndex(p => p.name === name);

const detailText = detail => [detail.number, detail.text].filter(Boolean).join(' ');

const priceSummary = (plan, cycle) => {
Expand All @@ -115,9 +154,9 @@ const getOwnerEmail = async context => {
};

// Opens the Paddle checkout for the plan that unlocks `gate`, skipping the plan comparison page.
export const UpgradeCheckoutModal = ({ gate, onClose }) => {
export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
const context = useContext(CurrentApp);
const offer = GATE_OFFERS[gate];
const offer = offerFor(gate, planName);
const plan = prices.find(p => p.name === offer.plan);

const [dark] = useState(prefersDark);
Expand Down Expand Up @@ -276,11 +315,13 @@ const goToPlanUsage = (slug, gate) => {
window.location.assign(`${base}${planUsagePath(slug, gate)}`);
};

// Paywall entry point: logs the gate view and, on click, opens the direct checkout. The direct
// checkout starts a new subscription, so it is only for Free apps; an app already on any paid
// plan (MVP, Starter, Gold, ...) goes to Plan Usage instead of buying a second plan.
// Paywall entry point: logs the gate view and, on click, opens the direct checkout when the
// plan it sells is above the app's plan (MVP on Free, Pay As You Go on MVP, ...). Paywalls
// follow the backend permissions, so they also show on plans at or above the one they sell,
// and on plans with no next plan up (Gold, custom, ...): those go to Plan Usage instead of
// buying a plan below their own. `plan` picks the plan when the gate sells more than one.
// `renderTrigger(open)` lets anchors and custom buttons keep their look.
export const UpgradeGateButton = ({ gate, renderTrigger, value = 'Upgrade Plan' }) => {
export const UpgradeGateButton = ({ gate, plan, renderTrigger, value = 'Upgrade Plan' }) => {
const context = useContext(CurrentApp);
const [open, setOpen] = useState(false);
const openCheckout = event => {
Expand All @@ -289,16 +330,19 @@ export const UpgradeGateButton = ({ gate, renderTrigger, value = 'Upgrade Plan'
}
logGateClicked(gate, context.applicationId);
return getCachedPlanData(context)
.then(planData => planKindOf(planData) === 'free')
.then(planData => {
const target = upgradeTargetOf(planData);
return !!target && planIndex(offerFor(gate, plan).plan) >= planIndex(target);
})
.catch(() => false)
.then(isFree => (isFree ? setOpen(true) : goToPlanUsage(context.slug, gate)));
.then(isUpgrade => (isUpgrade ? setOpen(true) : goToPlanUsage(context.slug, gate)));
};

return (
<>
<UpgradeGateView gate={gate} appId={context.applicationId} />
{renderTrigger ? renderTrigger(openCheckout) : <Button value={value} primary={true} onClick={openCheckout} trackClick={false} />}
{open ? <UpgradeCheckoutModal gate={gate} onClose={() => setOpen(false)} /> : null}
{open ? <UpgradeCheckoutModal gate={gate} plan={plan} onClose={() => setOpen(false)} /> : null}
</>
);
};
7 changes: 4 additions & 3 deletions src/components/UsageLimitBanner/UsageLimitBanner.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { getUsageAlert } from 'dashboard/Data/AppOverview/usageAlert';
import styles from 'components/UsageLimitBanner/UsageLimitBanner.scss';

// Near or over a plan limit (Overview and Plan Usage). The wording and the action come from
// getUsageAlert: Free opens the MVP checkout; paid plans get a link to Plan Usage naming the
// next plan up, unless they are already on it (showPlansLink={false}).
// getUsageAlert: the button opens the checkout of the next plan up (MVP on Free, Pay As You Go
// on MVP, ...). Plans with no next plan to name get a link to Plan Usage, unless they are
// already on it (showPlansLink={false}).
const UsageLimitBanner = ({ planData, blocked, slug, showPlansLink = true }) => {
const alert = getUsageAlert(planData, blocked);
if (!alert) {
return null;
}
let action = null;
if (alert.action === 'checkout') {
action = <UpgradeGateButton gate={UpgradeGate.USAGE_LIMIT} value={alert.actionLabel} />;
action = <UpgradeGateButton gate={UpgradeGate.USAGE_LIMIT} plan={alert.plan} value={alert.actionLabel} />;
} else if (showPlansLink) {
action = <Link className={styles.link} to={planUsagePath(slug, UpgradeGate.USAGE_LIMIT)}>{alert.actionLabel}</Link>;
}
Expand Down
12 changes: 1 addition & 11 deletions src/components/UsageLimitBanner/UsageLimitBanner.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
@import 'stylesheets/globals.scss';
@import 'stylesheets/back4app.scss';

// Yellow above 70% (always yellow on MVP), red above 90% on Free or when the app is blocked,
// neutral for Pay As You Go over its included usage.
// Yellow above 70%, red above 90% or when the app is blocked, on every plan.
.banner {
display: flex;
align-items: center;
Expand Down Expand Up @@ -35,15 +34,6 @@
}
}

.info {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.04);

svg {
color: #9aa5b8;
}
}

.text {
flex: 1;
min-width: 0;
Expand Down
17 changes: 6 additions & 11 deletions src/dashboard/Data/AppOverview/AppOverview.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import B4aTooltip from 'components/Tooltip/B4aTooltip.react';
import OnboardingBoxes from './OnboardingBoxes.react';
import AccountManager from 'lib/AccountManager';
import { amplitudeLogEvent } from 'lib/amplitudeEvents';
import { UpgradeGate, UpgradeGateView, logGateClicked, planUsagePath } from 'lib/upgradeEvents';
import { UpgradeGate } from 'lib/upgradeEvents';
import UsageLimitBanner from 'components/UsageLimitBanner/UsageLimitBanner.react';
import RegionChange from './RegionChange.react';
import { UpgradeGateButton } from 'components/UpgradeCheckout/UpgradeCheckout.react';
Expand Down Expand Up @@ -344,15 +344,10 @@ class AppOverview extends DashboardView {
renderTrigger={open => <a className={styles.changeRegionLink} href="#" onClick={open}>Enable web hosting</a>}
/>
) : httpsLocked ? (
// Already paying for MVP: compare plans instead of opening a second subscription.
<Link
className={styles.changeRegionLink}
to={planUsagePath(this.context.slug, UpgradeGate.HTTPS)}
onClick={() => logGateClicked(UpgradeGate.HTTPS, this.context.applicationId)}
>
<UpgradeGateView gate={UpgradeGate.HTTPS} appId={this.context.applicationId} />
Enable HTTPS
</Link>
<UpgradeGateButton
gate={UpgradeGate.HTTPS}
renderTrigger={open => <a className={styles.changeRegionLink} href="#" onClick={open}>Enable HTTPS</a>}
/>
) : (
<Link className={styles.changeRegionLink} to={`/apps/${this.context.slug}/domain-settings`}>
{!webUrl ? 'Enable web hosting' : customDomain ? 'Manage' : 'Use your domain'}
Expand Down Expand Up @@ -389,7 +384,7 @@ class AppOverview extends DashboardView {

<div className={styles.cardsContainer}>
{/* App plan card */}
<AppPlanCard loading={this.state.isLoadingAppPlanData} planData={this.state.appPlanData} appSlug={this.context.slug} appId={this.context.applicationId} />
<AppPlanCard loading={this.state.isLoadingAppPlanData} planData={this.state.appPlanData} />
{/* App Secutiry Card */}
<AppSecurityCard appId={this.context.slug} loading={this.state.isLoadingSecurityReport} securityReport={this.state.securityReport} />
</div>
Expand Down
35 changes: 15 additions & 20 deletions src/dashboard/Data/AppOverview/AppPlanCard.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react';
import styles from 'dashboard/Data/AppOverview/AppOverview.scss';
import { Button } from '@back4app2/react-components';
import Icon from 'components/Icon/Icon.react';
import { Link } from 'react-router-dom';
import { UpgradeGate, logGateClicked, planUsagePath } from 'lib/upgradeEvents';
import { UpgradeGate } from 'lib/upgradeEvents';
import { upgradeTargetOf } from 'dashboard/Data/AppOverview/usageAlert';
import { UpgradeGateButton } from 'components/UpgradeCheckout/UpgradeCheckout.react';

const formatDate = (dateString) => {
Expand All @@ -19,10 +19,10 @@ const formatDate = (dateString) => {
}
}

const AppPlanCard = ({ loading, planData, appSlug, appId }) => {
// Free has a single next step (MVP), so its button opens that checkout directly.
// Paid plans still go to the plan page to compare the next tier.
const isFree = !loading && planData && !(planData instanceof Error) && /Free/i.test(planData.planName || '');
const AppPlanCard = ({ loading, planData }) => {
// The button opens the checkout of the next plan up (MVP on Free). Plans with no next plan
// to name go to the plan page.
const nextPlan = loading ? undefined : upgradeTargetOf(planData);
let content = null;
if (loading) {
content = <div className={styles.loading}><Icon name="status-spinner" width="24px" height="24px" fill="#1377B8" className={styles.spinnerStatus} /></div>;
Expand Down Expand Up @@ -51,20 +51,15 @@ const AppPlanCard = ({ loading, planData, appSlug, appId }) => {
<div className={styles.serverLogsWrapper}>
<div className={styles.header}>
<div className={styles.headerText}>Plan Usage</div>
{isFree ? (
<UpgradeGateButton
gate={UpgradeGate.OVERVIEW_PLAN_CARD}
renderTrigger={open => (
<a href="#" onClick={open}>
<Button type="primary" value="Upgrade Plan" className={styles.upgradeBtn} />
</a>
)}
/>
) : (
<Link to={planUsagePath(appSlug, UpgradeGate.OVERVIEW_PLAN_CARD)} onClick={() => logGateClicked(UpgradeGate.OVERVIEW_PLAN_CARD, appId)}>
<Button type="primary" value="Upgrade Plan" className={styles.upgradeBtn} />
</Link>
)}
<UpgradeGateButton
gate={UpgradeGate.OVERVIEW_PLAN_CARD}
plan={nextPlan}
renderTrigger={open => (
<a href="#" onClick={open}>
<Button type="primary" value="Upgrade Plan" className={styles.upgradeBtn} />
</a>
)}
/>
</div>
<div className={styles.planDataBox}>
{content}
Expand Down
Loading
Loading