@@ -248,21 +226,10 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
{offer.headline}
{offer.subline ?
{offer.subline}
: null}
- ${price.perMonth}
+ ${plan.pricePerMonth}
/month
-
{price.note}
-
-
-
-
-
+
Billed monthly · cancel anytime
{plan.details.map((detail, idx) => (
@@ -275,7 +242,7 @@ export const UpgradeCheckoutModal = ({ gate, plan: planName, onClose }) => {
Total today
- ${price.today} + tax
+ ${plan.pricePerMonth} + tax
{completed ? (
@@ -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 => {
@@ -340,7 +310,7 @@ export const UpgradeGateButton = ({ gate, plan, renderTrigger, value = 'Upgrade
return (
<>
-
+ {logView ?
: null}
{renderTrigger ? renderTrigger(openCheckout) :
}
{open ?
setOpen(false)} /> : null}
>
diff --git a/src/components/UpgradeCheckout/UpgradeCheckout.scss b/src/components/UpgradeCheckout/UpgradeCheckout.scss
index 84598b7fb2..f7027deb60 100644
--- a/src/components/UpgradeCheckout/UpgradeCheckout.scss
+++ b/src/components/UpgradeCheckout/UpgradeCheckout.scss
@@ -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 {
@@ -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 {
@@ -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;
diff --git a/src/components/UsageLimitBanner/UsageLimitBanner.react.js b/src/components/UsageLimitBanner/UsageLimitBanner.react.js
index c8cd4557ff..f470286ea4 100644
--- a/src/components/UsageLimitBanner/UsageLimitBanner.react.js
+++ b/src/components/UsageLimitBanner/UsageLimitBanner.react.js
@@ -17,7 +17,7 @@ const UsageLimitBanner = ({ planData, blocked, slug, showPlansLink = true }) =>
}
let action = null;
if (alert.action === 'checkout') {
- action = ;
+ action = ;
} else if (showPlansLink) {
action = {alert.actionLabel};
}
diff --git a/src/dashboard/AppPlan/AppPlan.react.js b/src/dashboard/AppPlan/AppPlan.react.js
index 35d09ea196..8424e5dc45 100644
--- a/src/dashboard/AppPlan/AppPlan.react.js
+++ b/src/dashboard/AppPlan/AppPlan.react.js
@@ -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';
@@ -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,
@@ -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 || [];
@@ -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;
diff --git a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js
index 3dabd54e91..a74ed3b43b 100644
--- a/src/dashboard/CustomParseOptions/CustomParseOptions.react.js
+++ b/src/dashboard/CustomParseOptions/CustomParseOptions.react.js
@@ -463,7 +463,7 @@ class CustomParseOptions extends DashboardView {
}
input={
-
+
}
theme={Field.Theme.BLUE}
diff --git a/src/dashboard/Data/Logs/LogsFooter.react.js b/src/dashboard/Data/Logs/LogsFooter.react.js
index 8e0da07adc..a44d73fa89 100644
--- a/src/dashboard/Data/Logs/LogsFooter.react.js
+++ b/src/dashboard/Data/Logs/LogsFooter.react.js
@@ -35,6 +35,7 @@ const LogsFooter = ({ showRetention = false }) => {
Free keeps access logs for 1 day; MVP keeps 7 days.{' '}
(
Upgrade to MVP
)}
diff --git a/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js b/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js
index 2dc9ba61a5..fee1846e37 100644
--- a/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js
+++ b/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js
@@ -214,7 +214,7 @@ class DatabaseProfile extends DashboardView {
{/* Choose plan to entire div with button */}
-
+
diff --git a/src/dashboard/DomainSettings/DomainSettings.react.js b/src/dashboard/DomainSettings/DomainSettings.react.js
index be2e9b1ab7..ce2f2e14a0 100644
--- a/src/dashboard/DomainSettings/DomainSettings.react.js
+++ b/src/dashboard/DomainSettings/DomainSettings.react.js
@@ -408,7 +408,7 @@ class DomainSettings extends DashboardView {
}
input={
-
+
}
theme={Field.Theme.BLUE}
/>
@@ -427,7 +427,7 @@ class DomainSettings extends DashboardView {
disabled={!this.state.canChangeSubdomain && this.state.currentSubdomain.trim().length === 0}
/>
) : (
-
+
)}
}
@@ -558,7 +558,7 @@ class DomainSettings extends DashboardView {
/>
>
) : (
-