@growth-operations/ui-shared 1.6.11 → 1.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/billing/PlanGrid.jsx +65 -18
package/package.json
CHANGED
package/src/billing/PlanGrid.jsx
CHANGED
|
@@ -31,15 +31,21 @@ import { fmtMoney } from "../lib/format";
|
|
|
31
31
|
// appKey — the billing app_key for THIS app (always a prop, never hardcoded).
|
|
32
32
|
|
|
33
33
|
// One plan card. Shows tier name, price for the selected interval, credits,
|
|
34
|
-
// feature bullets, and a CTA: "Current" (disabled) for the active tier
|
|
35
|
-
// "Contact sales" for talk_to_sales tiers, else "Choose"
|
|
36
|
-
//
|
|
34
|
+
// feature bullets, and a CTA: "Current plan" (disabled) for the active tier AT
|
|
35
|
+
// its actual interval, "Contact sales" for talk_to_sales tiers, else "Choose"
|
|
36
|
+
// (or "Switch to Annual/Monthly" on the current card) which pre-creates a
|
|
37
|
+
// checkout/upgrade session for that tier's price in the selected interval.
|
|
37
38
|
//
|
|
38
39
|
// The Monthly/Annual toggle is PER CARD (not a single grid-wide switch): a tier
|
|
39
40
|
// may offer one interval and not the other (e.g. a top tier that's monthly-only),
|
|
40
41
|
// so each card owns its own interval state and only shows the toggle when it has
|
|
41
42
|
// BOTH legs. A subscription still bills on one cadence — the cadence bought is
|
|
42
|
-
// whatever this card's toggle shows when its
|
|
43
|
+
// whatever this card's toggle shows when its CTA is clicked. The CURRENT card
|
|
44
|
+
// also shows the toggle (defaulting to the account's real interval via
|
|
45
|
+
// plan.current_interval) so a monthly customer can switch to annual in place —
|
|
46
|
+
// toggling it to a DIFFERENT interval than what's billed swaps the disabled
|
|
47
|
+
// "Current plan" marker for an actionable switch CTA (always upgrade/start,
|
|
48
|
+
// never a new checkout, since a real subscription already exists).
|
|
43
49
|
function PlanCard({
|
|
44
50
|
plan,
|
|
45
51
|
billingBaseUrl,
|
|
@@ -69,9 +75,25 @@ function PlanCard({
|
|
|
69
75
|
openIframe = null,
|
|
70
76
|
}) {
|
|
71
77
|
// Default this card to annual when it offers annual (cheaper-per-month story);
|
|
72
|
-
// monthly-only tiers default to monthly. Both legs → show the toggle.
|
|
78
|
+
// monthly-only tiers default to monthly. Both legs → show the toggle. The
|
|
79
|
+
// CURRENT card is the one exception: default to the account's ACTUAL billing
|
|
80
|
+
// interval (plan.current_interval, stamped server-side from the mirrored
|
|
81
|
+
// Subscription) so the toggle opens showing reality, not a marketing default
|
|
82
|
+
// — falling back to the annual-preferred default only if the interval is
|
|
83
|
+
// unknown (e.g. an older payload before this field existed).
|
|
73
84
|
const hasMonthly = !!plan.monthly;
|
|
74
85
|
const hasAnnual = !!plan.annual;
|
|
86
|
+
const defaultInterval = plan.current
|
|
87
|
+
? plan.current_interval === "month"
|
|
88
|
+
? "monthly"
|
|
89
|
+
: plan.current_interval === "year"
|
|
90
|
+
? "annual"
|
|
91
|
+
: hasAnnual
|
|
92
|
+
? "annual"
|
|
93
|
+
: "monthly"
|
|
94
|
+
: hasAnnual
|
|
95
|
+
? "annual"
|
|
96
|
+
: "monthly";
|
|
75
97
|
// A lower-ranked tier than the customer's current one: a downgrade. Not
|
|
76
98
|
// self-serve (changes go through support) — show it for context, disabled,
|
|
77
99
|
// with a Talk-to-sales CTA. Only meaningful in the trial picker (currentOrder
|
|
@@ -81,10 +103,19 @@ function PlanCard({
|
|
|
81
103
|
!plan.current &&
|
|
82
104
|
plan.tier_order != null &&
|
|
83
105
|
plan.tier_order < currentOrder;
|
|
84
|
-
// The interval toggle is
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
const
|
|
106
|
+
// The interval toggle is useful whenever the card can be acted on OR is the
|
|
107
|
+
// current plan (so a monthly customer can switch to annual in place) — only
|
|
108
|
+
// a downgrade card is fully read-only.
|
|
109
|
+
const canToggle = hasMonthly && hasAnnual && !isDowngrade;
|
|
110
|
+
const [interval, setInterval] = useState(defaultInterval);
|
|
111
|
+
// On the current card, toggling to a DIFFERENT interval than what's actually
|
|
112
|
+
// billed is the in-place monthly<->annual switch; toggling back to the same
|
|
113
|
+
// interval (or current_interval being unknown) is just "Current plan".
|
|
114
|
+
const isIntervalSwitch =
|
|
115
|
+
plan.current &&
|
|
116
|
+
plan.current_interval != null &&
|
|
117
|
+
((interval === "annual" && plan.current_interval !== "year") ||
|
|
118
|
+
(interval === "monthly" && plan.current_interval !== "month"));
|
|
88
119
|
const leg = interval === "annual" ? plan.annual : plan.monthly;
|
|
89
120
|
const periodLabel = interval === "annual" ? "/yr" : "/mo";
|
|
90
121
|
|
|
@@ -105,12 +136,18 @@ function PlanCard({
|
|
|
105
136
|
? Math.round((1 - plan.annual.unit_amount / annualFull) * 100)
|
|
106
137
|
: 0;
|
|
107
138
|
|
|
108
|
-
// Direct one-click
|
|
109
|
-
//
|
|
110
|
-
// price) — the button is disabled until then.
|
|
139
|
+
// Direct one-click link to the billing service's redirect endpoint. Null
|
|
140
|
+
// until we have everything (billing host, appKey, portal, this interval's
|
|
141
|
+
// price) — the button is disabled until then. The current card's in-place
|
|
142
|
+
// interval switch is ALWAYS upgrade/start (swap the existing sub's item),
|
|
143
|
+
// regardless of what `endpoint` this grid instance was given — it can never
|
|
144
|
+
// be a first-time checkout/start, since plan.current + a real
|
|
145
|
+
// current_interval only happens with an existing subscription already in
|
|
146
|
+
// place. Getting this wrong would double-bill (a second Checkout session).
|
|
147
|
+
const startEndpoint = isIntervalSwitch ? "upgrade/start" : endpoint;
|
|
111
148
|
const startUrl =
|
|
112
149
|
billingBaseUrl && appKey && portalId && leg?.price_id
|
|
113
|
-
? `${billingBaseUrl}/v1/billing/${
|
|
150
|
+
? `${billingBaseUrl}/v1/billing/${startEndpoint}` +
|
|
114
151
|
`?app_key=${encodeURIComponent(appKey)}` +
|
|
115
152
|
`&portal_id=${encodeURIComponent(portalId)}` +
|
|
116
153
|
`&price_id=${encodeURIComponent(leg.price_id)}` +
|
|
@@ -176,12 +213,22 @@ function PlanCard({
|
|
|
176
213
|
<Text key={`pad-${i}`}>{" "}</Text>
|
|
177
214
|
))}
|
|
178
215
|
|
|
179
|
-
{/* CTA. Current tier => disabled marker.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
{/* CTA. Current tier, same interval => disabled marker. Current tier,
|
|
217
|
+
toggled to the OTHER interval => actionable in-place switch (same
|
|
218
|
+
upgrade/start swap as a tier upgrade, just same-tier). Downgrade
|
|
219
|
+
(lower than current, trial picker) => disabled + Talk-to-sales (not
|
|
220
|
+
self-serve). talk_to_sales tier => contact link. Otherwise
|
|
221
|
+
pre-create-then-link checkout/upgrade for the selected interval. */}
|
|
222
|
+
{plan.current && !isIntervalSwitch ? (
|
|
184
223
|
<Button disabled>Current plan</Button>
|
|
224
|
+
) : plan.current && isIntervalSwitch ? (
|
|
225
|
+
<Button
|
|
226
|
+
href={startUrl ? { url: startUrl, external: true } : undefined}
|
|
227
|
+
disabled={!startUrl}
|
|
228
|
+
variant="primary"
|
|
229
|
+
>
|
|
230
|
+
Switch to {interval === "annual" ? "Annual" : "Monthly"}
|
|
231
|
+
</Button>
|
|
185
232
|
) : isDowngrade ? (
|
|
186
233
|
<Button
|
|
187
234
|
href={supportUrl ? { url: supportUrl, external: true } : undefined}
|