@growth-operations/ui-shared 1.6.4 → 1.6.6
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/BillingTab.jsx +30 -25
- package/src/billing/PlanGrid.jsx +73 -5
package/package.json
CHANGED
|
@@ -138,7 +138,7 @@ function StatusPanel({ entitlement }) {
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
141
|
+
function TrialSubscriptionBilling({ context, state, appKey, openIframe = null }) {
|
|
142
142
|
const ent = state?.entitlement;
|
|
143
143
|
|
|
144
144
|
// Install not provisioned with a Stripe customer yet — calm "finish setup"
|
|
@@ -180,21 +180,22 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
|
180
180
|
`&return_url=${encodeURIComponent(returnUrl)}`
|
|
181
181
|
: null;
|
|
182
182
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
183
|
+
// Tier picker — shown ONLY while trialing (the lifecycle: trial starts on the
|
|
184
|
+
// entry tier at install; while trialing the customer may upgrade to a higher
|
|
185
|
+
// tier in-app; once active, plan changes go through support). We show the FULL
|
|
186
|
+
// ladder for context so the customer sees where they sit:
|
|
187
|
+
// current tier -> "Current plan" (disabled marker; PlanCard keys off
|
|
188
|
+
// plan.current, stamped server-side on the account's tier).
|
|
189
|
+
// higher tiers -> the actionable upgrade (CTA targets /v1/billing/upgrade/
|
|
190
|
+
// start, swapping the trialing sub's item — no new sub).
|
|
191
|
+
// lower tiers -> shown disabled with a Talk-to-sales CTA (downgrades aren't
|
|
192
|
+
// self-serve). PlanCard derives this from currentOrder.
|
|
189
193
|
const isTrialing = ent.status === "trialing";
|
|
190
194
|
const plans = state?.plans ?? [];
|
|
191
195
|
const currentOrder = plans.find((p) => p.current)?.tier_order;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(p) => !p.talk_to_sales && p.tier_order != null && p.tier_order > currentOrder
|
|
196
|
-
)
|
|
197
|
-
: [];
|
|
196
|
+
// Render the picker only while trialing AND once we know the current tier
|
|
197
|
+
// (currentOrder drives upgrade-vs-downgrade per card).
|
|
198
|
+
const showPicker = isTrialing && currentOrder != null && plans.length > 0;
|
|
198
199
|
|
|
199
200
|
return (
|
|
200
201
|
<Flex direction="column" gap="medium">
|
|
@@ -215,26 +216,30 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
|
215
216
|
link above to update your plan, payment method, or view invoices.
|
|
216
217
|
</Text>
|
|
217
218
|
|
|
218
|
-
{/* Trial-only
|
|
219
|
-
|
|
220
|
-
tier, keeping the trial end date.
|
|
221
|
-
|
|
219
|
+
{/* Trial-only tier ladder: current (marked), higher (upgradeable), lower
|
|
220
|
+
(disabled, Talk-to-sales). Hidden once active. Upgrading swaps the
|
|
221
|
+
trialing sub onto the higher tier, keeping the trial end date. The full
|
|
222
|
+
`plans` list is passed; PlanCard decides each card's state from
|
|
223
|
+
plan.current + currentOrder. */}
|
|
224
|
+
{showPicker && (
|
|
222
225
|
<PlanGrid
|
|
223
226
|
context={context}
|
|
224
227
|
state={state}
|
|
225
228
|
appKey={appKey}
|
|
226
|
-
plans={
|
|
229
|
+
plans={plans}
|
|
230
|
+
currentOrder={currentOrder}
|
|
227
231
|
endpoint="upgrade/start"
|
|
228
232
|
ctaLabel="Upgrade to"
|
|
229
|
-
heading="
|
|
230
|
-
footnote="Upgrade any time during your trial — your trial end date stays the same, and the new tier applies when it converts."
|
|
233
|
+
heading="Your plan"
|
|
234
|
+
footnote="Upgrade any time during your trial — your trial end date stays the same, and the new tier applies when it converts. To move to a lower tier, talk to sales."
|
|
235
|
+
openIframe={openIframe}
|
|
231
236
|
/>
|
|
232
237
|
)}
|
|
233
238
|
</Flex>
|
|
234
239
|
);
|
|
235
240
|
}
|
|
236
241
|
|
|
237
|
-
function CreditsBilling({ context, state, appKey }) {
|
|
242
|
+
function CreditsBilling({ context, state, appKey, openIframe = null }) {
|
|
238
243
|
const onPaidPlan = !!state?.entitlement?.plan;
|
|
239
244
|
|
|
240
245
|
// Direct external link to the billing service's GET /v1/billing/portal/start,
|
|
@@ -297,7 +302,7 @@ function CreditsBilling({ context, state, appKey }) {
|
|
|
297
302
|
entitlement={state?.entitlement}
|
|
298
303
|
creditMeter={state?.credit_meter}
|
|
299
304
|
/>
|
|
300
|
-
<PlanGrid context={context} state={state} appKey={appKey} />
|
|
305
|
+
<PlanGrid context={context} state={state} appKey={appKey} openIframe={openIframe} />
|
|
301
306
|
{!hasPlans && (
|
|
302
307
|
<LoadingButton
|
|
303
308
|
href={
|
|
@@ -341,7 +346,7 @@ function LegacyBilling({ state }) {
|
|
|
341
346
|
);
|
|
342
347
|
}
|
|
343
348
|
|
|
344
|
-
export function BillingTab({ context, state, appKey }) {
|
|
349
|
+
export function BillingTab({ context, state, appKey, openIframe = null }) {
|
|
345
350
|
const mode = state?.entitlement?.mode;
|
|
346
351
|
|
|
347
352
|
return (
|
|
@@ -350,9 +355,9 @@ export function BillingTab({ context, state, appKey }) {
|
|
|
350
355
|
{mode === "legacy" ? (
|
|
351
356
|
<LegacyBilling state={state} />
|
|
352
357
|
) : mode === "credits" ? (
|
|
353
|
-
<CreditsBilling context={context} state={state} appKey={appKey} />
|
|
358
|
+
<CreditsBilling context={context} state={state} appKey={appKey} openIframe={openIframe} />
|
|
354
359
|
) : (
|
|
355
|
-
<TrialSubscriptionBilling context={context} state={state} appKey={appKey} />
|
|
360
|
+
<TrialSubscriptionBilling context={context} state={state} appKey={appKey} openIframe={openIframe} />
|
|
356
361
|
)}
|
|
357
362
|
</Flex>
|
|
358
363
|
);
|
package/src/billing/PlanGrid.jsx
CHANGED
|
@@ -54,12 +54,36 @@ function PlanCard({
|
|
|
54
54
|
// to this tier (the trial-archetype in-app upgrade) — no new sub.
|
|
55
55
|
endpoint = "checkout/start",
|
|
56
56
|
ctaLabel = "Choose",
|
|
57
|
+
// Trial-picker only: tier_order of the customer's current tier. When set, a
|
|
58
|
+
// card ranked BELOW it is a downgrade — shown for context but not self-serve
|
|
59
|
+
// (downgrades go through support), so it renders disabled with a Talk-to-sales
|
|
60
|
+
// CTA. Cards ranked ABOVE are upgrades (the normal CTA). null/undefined => the
|
|
61
|
+
// credit first-purchase grid, where this relation doesn't apply.
|
|
62
|
+
currentOrder = null,
|
|
63
|
+
// actions.openIframeModal from the card root (threaded BillingTab → PlanGrid).
|
|
64
|
+
// When present, a talk_to_sales tier with plan.meetings_url opens the meetings
|
|
65
|
+
// scheduler in an IN-CARD modal (the Anvil "Meet with Andrew" experience).
|
|
66
|
+
// Absent (or no meetings_url) → fall back to a new-tab href. UI Extensions
|
|
67
|
+
// can't read whether the external meetings page is CSP-frameable, so the modal
|
|
68
|
+
// is best-effort; the href fallback always works.
|
|
69
|
+
openIframe = null,
|
|
57
70
|
}) {
|
|
58
71
|
// Default this card to annual when it offers annual (cheaper-per-month story);
|
|
59
72
|
// monthly-only tiers default to monthly. Both legs → show the toggle.
|
|
60
73
|
const hasMonthly = !!plan.monthly;
|
|
61
74
|
const hasAnnual = !!plan.annual;
|
|
62
|
-
|
|
75
|
+
// A lower-ranked tier than the customer's current one: a downgrade. Not
|
|
76
|
+
// self-serve (changes go through support) — show it for context, disabled,
|
|
77
|
+
// with a Talk-to-sales CTA. Only meaningful in the trial picker (currentOrder
|
|
78
|
+
// set); never flag the current tier itself as a downgrade.
|
|
79
|
+
const isDowngrade =
|
|
80
|
+
currentOrder != null &&
|
|
81
|
+
!plan.current &&
|
|
82
|
+
plan.tier_order != null &&
|
|
83
|
+
plan.tier_order < currentOrder;
|
|
84
|
+
// The interval toggle is only useful when the card can be acted on; a current
|
|
85
|
+
// or downgrade card is read-only, so hide its toggle.
|
|
86
|
+
const canToggle = hasMonthly && hasAnnual && !plan.current && !isDowngrade;
|
|
63
87
|
const [interval, setInterval] = useState(hasAnnual ? "annual" : "monthly");
|
|
64
88
|
const leg = interval === "annual" ? plan.annual : plan.monthly;
|
|
65
89
|
const periodLabel = interval === "annual" ? "/yr" : "/mo";
|
|
@@ -152,18 +176,54 @@ function PlanCard({
|
|
|
152
176
|
<Text key={`pad-${i}`}>{" "}</Text>
|
|
153
177
|
))}
|
|
154
178
|
|
|
155
|
-
{/* CTA. Current tier => disabled marker.
|
|
156
|
-
|
|
179
|
+
{/* CTA. Current tier => disabled marker. Downgrade (lower than current,
|
|
180
|
+
trial picker) => disabled + Talk-to-sales (not self-serve).
|
|
181
|
+
talk_to_sales tier => contact link. Otherwise pre-create-then-link
|
|
182
|
+
checkout/upgrade for the selected interval. */}
|
|
157
183
|
{plan.current ? (
|
|
158
184
|
<Button disabled>Current plan</Button>
|
|
159
|
-
) :
|
|
185
|
+
) : isDowngrade ? (
|
|
160
186
|
<Button
|
|
161
187
|
href={supportUrl ? { url: supportUrl, external: true } : undefined}
|
|
162
188
|
disabled={!supportUrl}
|
|
163
189
|
variant="secondary"
|
|
164
190
|
>
|
|
165
|
-
|
|
191
|
+
Talk to sales to switch
|
|
166
192
|
</Button>
|
|
193
|
+
) : plan.talk_to_sales ? (
|
|
194
|
+
// Talk-to-sales tier (e.g. Enterprise+): no price/checkout. If we have
|
|
195
|
+
// a meetings link AND the openIframeModal action, open the scheduler in
|
|
196
|
+
// an in-card modal (Anvil "Meet with Andrew"). Otherwise a new-tab href
|
|
197
|
+
// to the meetings link (preferred) or support page.
|
|
198
|
+
openIframe && plan.meetings_url ? (
|
|
199
|
+
<Button
|
|
200
|
+
variant="secondary"
|
|
201
|
+
onClick={() =>
|
|
202
|
+
openIframe({
|
|
203
|
+
uri: plan.meetings_url,
|
|
204
|
+
title: `Talk to sales — ${plan.name ?? plan.tier}`,
|
|
205
|
+
width: 1100,
|
|
206
|
+
height: 760,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
>
|
|
210
|
+
Talk to sales
|
|
211
|
+
</Button>
|
|
212
|
+
) : (
|
|
213
|
+
<Button
|
|
214
|
+
href={
|
|
215
|
+
plan.meetings_url
|
|
216
|
+
? { url: plan.meetings_url, external: true }
|
|
217
|
+
: supportUrl
|
|
218
|
+
? { url: supportUrl, external: true }
|
|
219
|
+
: undefined
|
|
220
|
+
}
|
|
221
|
+
disabled={!plan.meetings_url && !supportUrl}
|
|
222
|
+
variant="secondary"
|
|
223
|
+
>
|
|
224
|
+
Talk to sales
|
|
225
|
+
</Button>
|
|
226
|
+
)
|
|
167
227
|
) : !leg ? (
|
|
168
228
|
// No price for the selected interval (e.g. annual not offered) — guide
|
|
169
229
|
// the customer to the other toggle rather than dead-ending.
|
|
@@ -202,6 +262,12 @@ export function PlanGrid({
|
|
|
202
262
|
ctaLabel = "Choose",
|
|
203
263
|
heading = "Plans",
|
|
204
264
|
footnote = "Billing is managed in Stripe across all Growth Operations apps. Your new credits are added as soon as payment completes.",
|
|
265
|
+
// Trial-picker only: tier_order of the current tier, so cards below it render
|
|
266
|
+
// as disabled downgrades (Talk-to-sales). Omitted for the credit grid.
|
|
267
|
+
currentOrder = null,
|
|
268
|
+
// actions.openIframeModal (from the hosting card root) — lets a talk_to_sales
|
|
269
|
+
// tier open its meetings_url scheduler in an in-card modal. Optional.
|
|
270
|
+
openIframe = null,
|
|
205
271
|
}) {
|
|
206
272
|
const plans = plansOverride ?? state?.plans ?? [];
|
|
207
273
|
|
|
@@ -242,6 +308,8 @@ export function PlanGrid({
|
|
|
242
308
|
maxFeatures={maxFeatures}
|
|
243
309
|
endpoint={endpoint}
|
|
244
310
|
ctaLabel={ctaLabel}
|
|
311
|
+
currentOrder={currentOrder}
|
|
312
|
+
openIframe={openIframe}
|
|
245
313
|
/>
|
|
246
314
|
))}
|
|
247
315
|
</AutoGrid>
|