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
2 changes: 1 addition & 1 deletion src/components/UpgradeCheckout/BackupUpsell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const BackupUpsell = ({ gate }) => {
}
input={
<div style={{ width: '100%', padding: '0 1rem', textAlign: 'right' }}>
<UpgradeGateButton gate={gate} value="Get daily backups" />
<UpgradeGateButton gate={gate} value="Get daily backups" logView />
</div>
}
/>
Expand Down
60 changes: 15 additions & 45 deletions src/components/UpgradeCheckout/UpgradeCheckout.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Icon from 'components/Icon/Icon.react';
import AccountManager from 'lib/AccountManager';
import { CurrentApp } from 'context/currentApp';
import { amplitudeLogEvent } from 'lib/amplitudeEvents';
import { SUPPORT_TICKET_URL, UpgradeEvent, UpgradeGate, UpgradeGateView, logGateClicked, planUsagePath, regionChangeFormUrl } from 'lib/upgradeEvents';
import { SUPPORT_TICKET_URL, UpgradeEvent, UpgradeGate, UpgradeGateView, logGateClicked, planUsagePath, regionChangeFormUrl, trackPaddleEvent } from 'lib/upgradeEvents';
import { Cycle, initPaddle, paddlePlanId, paddlePriceId, recordSubscription } from 'lib/paddleCheckout';
import { prices } from 'dashboard/AppPlan/prices';
import { getCachedPlanData } from 'lib/planDataCache';
Expand Down Expand Up @@ -137,14 +137,6 @@ const planIndex = name => prices.findIndex(p => p.name === name);

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

const priceSummary = (plan, cycle) => {
if (cycle === Cycle.YEARLY) {
const yearly = Number(plan.pricePerYear) * 12;
return { perMonth: plan.pricePerYear, note: `Billed $${yearly}/year · save ${plan.savePercent}`, today: yearly };
}
return { perMonth: plan.pricePerMonth, note: 'Billed monthly · cancel anytime', today: Number(plan.pricePerMonth) };
};

const getOwnerEmail = async context => {
if (context.custom && context.custom.isOwner) {
return AccountManager.currentUser().email;
Expand All @@ -160,19 +152,22 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
const plan = prices.find(p => p.name === offer.plan);

const [dark] = useState(prefersDark);
// Yearly first, like the pricing page; monthly stays one click away.
const [cycle, setCycle] = useState(Cycle.YEARLY);
// Monthly only: a paywall sells the plan at its monthly price. Yearly stays on Plan Usage.
const cycle = Cycle.MONTHLY;
const [paddle, setPaddle] = useState(null);
const [ownerEmail, setOwnerEmail] = useState(null);
const [frame, setFrame] = useState(null);
const [completed, setCompleted] = useState(false);
const [error, setError] = useState(null);
const funnel = useRef({ app_id: context.applicationId, plan: plan.name, cycle, source: 'gate', gate });
const opened = useRef(false);
// The Paddle events already logged for this checkout (the modal opens one).
const paddleSeen = useRef(new Set());

useEffect(() => {
let active = true;
initPaddle(async data => {
trackPaddleEvent(data, funnel.current, paddleSeen.current);
if (data.name === 'checkout.completed') {
setCompleted(true);
await recordSubscription(data, { source: 'gate', gate });
Expand Down Expand Up @@ -207,21 +202,6 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
});
}, [paddle, ownerEmail, frame]);

const changeCycle = next => {
if (next === cycle || completed) {
return;
}
setCycle(next);
funnel.current = { ...funnel.current, cycle: next };
amplitudeLogEvent(UpgradeEvent.CHECKOUT_CYCLE_CHANGED, funnel.current);
if (opened.current) {
paddle.Checkout.updateCheckout({
items: [{ priceId: paddlePriceId(plan, next), quantity: 1 }],
customData: { appId: context.applicationId, planId: paddlePlanId(plan, next) },
});
}
};

const close = () => {
if (completed) {
// Reload so the paywall that brought the user here reflects the new plan.
Expand All @@ -235,8 +215,6 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
onClose();
};

const price = priceSummary(plan, cycle);

return (
<Popover fadeIn={true} fixed={true} position={ORIGIN} modal={true} color="rgba(17,13,17,0.8)">
<div className={[styles.checkout, dark ? styles.dark : styles.light].join(' ')}>
Expand All @@ -248,21 +226,10 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
<div className={styles.headline}>{offer.headline}</div>
{offer.subline ? <div className={styles.subline}>{offer.subline}</div> : null}
<div className={styles.price}>
<span className={styles.priceValue}>${price.perMonth}</span>
<span className={styles.priceValue}>${plan.pricePerMonth}</span>
<span className={styles.priceUnit}>/month</span>
</div>
<div className={styles.priceNote}>{price.note}</div>

<div className={styles.cycles}>
<label className={styles.cycle}>
<input type="radio" checked={cycle === Cycle.YEARLY} onChange={() => changeCycle(Cycle.YEARLY)} disabled={completed} />
Yearly · ${plan.pricePerYear}/month <span className={styles.save}>Save {plan.savePercent}</span>
</label>
<label className={styles.cycle}>
<input type="radio" checked={cycle === Cycle.MONTHLY} onChange={() => changeCycle(Cycle.MONTHLY)} disabled={completed} />
Monthly · ${plan.pricePerMonth}/month
</label>
</div>
<div className={styles.priceNote}>Billed monthly · cancel anytime</div>

<div className={styles.benefits}>
{plan.details.map((detail, idx) => (
Expand All @@ -275,7 +242,7 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {

<div className={styles.total}>
<span>Total today</span>
<span>${price.today} + tax</span>
<span>${plan.pricePerMonth} + tax</span>
</div>

{completed ? (
Expand Down Expand Up @@ -315,13 +282,16 @@ 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 when the
// Paywall entry point: on click, logs the gate click and 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, plan, renderTrigger, value = 'Upgrade Plan' }) => {
// `logView` also logs the gate view when the paywall shows. Only for paywalls the user reaches by
// trying to use the feature (opening its page); buttons that are always on screen, like the
// Overview ones, count only the click.
export const UpgradeGateButton = ({ gate, plan, renderTrigger, value = 'Upgrade Plan', logView = false }) => {
const context = useContext(CurrentApp);
const [open, setOpen] = useState(false);
const openCheckout = event => {
Expand All @@ -340,7 +310,7 @@ export const UpgradeGateButton = ({ gate, plan, renderTrigger, value = 'Upgrade

return (
<>
<UpgradeGateView gate={gate} appId={context.applicationId} />
{logView ? <UpgradeGateView gate={gate} appId={context.applicationId} /> : null}
{renderTrigger ? renderTrigger(openCheckout) : <Button value={value} primary={true} onClick={openCheckout} trackClick={false} />}
{open ? <UpgradeCheckoutModal gate={gate} plan={plan} onClose={() => setOpen(false)} /> : null}
</>
Expand Down
44 changes: 0 additions & 44 deletions src/components/UpgradeCheckout/UpgradeCheckout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
--uc-text: #f9f9f9;
--uc-muted: #9aa5b8;
--uc-border: rgba(255, 255, 255, 0.1);
--uc-option: rgba(255, 255, 255, 0.03);
--uc-option-selected: rgba(39, 174, 96, 0.12);
}

.light {
Expand All @@ -18,8 +16,6 @@
--uc-text: #10203a;
--uc-muted: #5b6577;
--uc-border: #e2e6ec;
--uc-option: #ffffff;
--uc-option-selected: rgba(39, 174, 96, 0.08);
}

.checkout {
Expand Down Expand Up @@ -112,46 +108,6 @@
color: var(--uc-muted);
}

.cycles {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1.5rem;
}

.cycle {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--uc-border);
border-radius: 8px;
background: var(--uc-option);
font-size: 0.9375rem;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;

input {
margin: 0;
accent-color: $cta-green;
}

&:has(input:checked) {
border-color: $cta-green;
background: var(--uc-option-selected);
}
}

.save {
margin-left: auto;
padding: 2px 8px;
border-radius: 999px;
background: rgba(39, 174, 96, 0.15);
color: $cta-green;
font-size: 0.75rem;
font-weight: 600;
}

.benefits {
display: flex;
flex-direction: column;
Expand Down
2 changes: 1 addition & 1 deletion src/components/UsageLimitBanner/UsageLimitBanner.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const UsageLimitBanner = ({ planData, blocked, slug, showPlansLink = true }) =>
}
let action = null;
if (alert.action === 'checkout') {
action = <UpgradeGateButton gate={UpgradeGate.USAGE_LIMIT} plan={alert.plan} value={alert.actionLabel} />;
action = <UpgradeGateButton gate={UpgradeGate.USAGE_LIMIT} plan={alert.plan} value={alert.actionLabel} logView />;
} else if (showPlansLink) {
action = <Link className={styles.link} to={planUsagePath(slug, UpgradeGate.USAGE_LIMIT)}>{alert.actionLabel}</Link>;
}
Expand Down
7 changes: 5 additions & 2 deletions src/dashboard/AppPlan/AppPlan.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Icon from 'components/Icon/Icon.react';
import { initializePaddle } from '@paddle/paddle-js';
import B4aModal from 'components/B4aModal/B4aModal.react';
import { amplitudeLogEvent } from 'lib/amplitudeEvents';
import { UpgradeEvent, getGateFromSearch } from 'lib/upgradeEvents';
import { UpgradeEvent, getGateFromSearch, trackPaddleEvent } from 'lib/upgradeEvents';
import UsageLimitBanner from 'components/UsageLimitBanner/UsageLimitBanner.react';
import { prices } from 'dashboard/AppPlan/prices';

Expand All @@ -36,7 +36,7 @@ class AppPlan extends DashboardView {
appPlanName: 'Free Plan',
appPlanError: null,
selectedPlan: prices[1],
billingCycle: 1, // 0 --> monthly || 1 --> yearly
billingCycle: 0, // 0 --> monthly || 1 --> yearly
isLoadingPaddle: false,
paddleError: null,
paddle: null,
Expand Down Expand Up @@ -98,6 +98,8 @@ class AppPlan extends DashboardView {
// Paddle calls this as a plain function, so keep a handle on the component for the funnel context.
const component = this;
const paddleEventCallback = async function (data) {
// Only for a checkout opened by handleOnClickPlan, which sets the context.
trackPaddleEvent(data, component.checkoutContext, component.paddleSeen);
if (data.name === 'checkout.completed') {
component.checkoutCompleted = true;
window.dataLayer = window.dataLayer || [];
Expand Down Expand Up @@ -219,6 +221,7 @@ class AppPlan extends DashboardView {
gate,
};
this.checkoutCompleted = false;
this.paddleSeen = new Set();
amplitudeLogEvent(UpgradeEvent.CHECKOUT_OPENED, this.checkoutContext);
this.setState({ selectedPlan: plan, openCheckout: true }, () => {
const priceId = this.state.billingCycle === 0 ? plan.monthlyPlanId : plan.annuallyPlanId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ class CustomParseOptions extends DashboardView {
}
input={
<div style={{ width: '100%', padding: '0 1rem', textAlign: 'right' }}>
<UpgradeGateButton gate={UpgradeGate.PARSE_OPTIONS} />
<UpgradeGateButton gate={UpgradeGate.PARSE_OPTIONS} logView />
</div>
}
theme={Field.Theme.BLUE}
Expand Down
1 change: 1 addition & 0 deletions src/dashboard/Data/Logs/LogsFooter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const LogsFooter = ({ showRetention = false }) => {
Free keeps access logs for 1 day; MVP keeps 7 days.{' '}
<UpgradeGateButton
gate={UpgradeGate.LOGS_RETENTION}
logView
renderTrigger={open => (
<a href="#" onClick={open}>Upgrade to MVP</a>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ class DatabaseProfile extends DashboardView {

{/* Choose plan to entire div with button */}
<div style={{ display: 'flex', justifyContent: 'center' }} className={styles.upgradeCard}>
<UpgradeGateButton gate={UpgradeGate.DB_PROFILER} value="Buy now" />
<UpgradeGateButton gate={UpgradeGate.DB_PROFILER} value="Buy now" logView />
</div>
</div>

Expand Down
6 changes: 3 additions & 3 deletions src/dashboard/DomainSettings/DomainSettings.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ class DomainSettings extends DashboardView {
<Field
label={<Label text="Upgrade your plan" dark={true} description="Upgrade your plan to enable web hosting on a b4a.app subdomain." />}
input={<div style={{ width: '100%', padding: '0 1rem', textAlign: 'right' }}>
<UpgradeGateButton gate={UpgradeGate.WEB_HOSTING} />
<UpgradeGateButton gate={UpgradeGate.WEB_HOSTING} logView />
</div>}
theme={Field.Theme.BLUE}
/>
Expand All @@ -427,7 +427,7 @@ class DomainSettings extends DashboardView {
disabled={!this.state.canChangeSubdomain && this.state.currentSubdomain.trim().length === 0}
/>
) : (
<UpgradeGateButton gate={UpgradeGate.WEB_HOSTING} />
<UpgradeGateButton gate={UpgradeGate.WEB_HOSTING} logView />
)}
</div>
}
Expand Down Expand Up @@ -558,7 +558,7 @@ class DomainSettings extends DashboardView {
/>
</>
) : (
<UpgradeGateButton gate={UpgradeGate.CUSTOM_DOMAIN} value="Upgrade to Pay As You Go" />
<UpgradeGateButton gate={UpgradeGate.CUSTOM_DOMAIN} value="Upgrade to Pay As You Go" logView />
)}

</div>}
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/Notification/EmailPasswordReset.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class EmailPasswordReset extends DashboardView {
}
input={
<div style={{ width: '100%', padding: '0 1rem', textAlign: 'right' }}>
<UpgradeGateButton gate={UpgradeGate.EMAIL_TEMPLATES} />
<UpgradeGateButton gate={UpgradeGate.EMAIL_TEMPLATES} logView />
</div>
}
theme={Field.Theme.BLUE}
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/Notification/EmailVerification.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ class EmailVerification extends DashboardView {
}
input={
<div style={{ width: '100%', padding: '0 1rem', textAlign: 'right' }}>
<UpgradeGateButton gate={UpgradeGate.EMAIL_TEMPLATES} />
<UpgradeGateButton gate={UpgradeGate.EMAIL_TEMPLATES} logView />
</div>
}
theme={Field.Theme.BLUE}
Expand Down
2 changes: 1 addition & 1 deletion src/dashboard/Settings/Collaborators.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export default class Collaborators extends React.Component {
<div style={{ width: '100%', padding: '0 1rem' }}>
{maxCollaborators === 0 || maxCollaborators === null || maxCollaborators === false ? (
<div style={{ textAlign: 'right' }}>
<UpgradeGateButton gate={UpgradeGate.COLLABORATORS} />
<UpgradeGateButton gate={UpgradeGate.COLLABORATORS} logView />
</div>
) : maxCollaborators !== true && maxCollaborators !== null && collaboratorUsage >= maxCollaborators ? (
<a
Expand Down
Loading
Loading