@growth-operations/ui-shared 1.5.5 → 1.5.7
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 +56 -27
package/package.json
CHANGED
|
@@ -170,10 +170,17 @@ function usePortalSession({ context, state, appKey }) {
|
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
// No Stripe customer yet → don't even try to open a portal. An install
|
|
173
|
-
// that hasn't been provisioned
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
// that hasn't been provisioned has nothing to manage; show the calm "not
|
|
174
|
+
// ready" state. The credits arm has NO `status` field (only the
|
|
175
|
+
// not_installed shell does) but DOES carry `plan` once paid — treat a set
|
|
176
|
+
// `plan` as ready so a paid credit customer gets the Manage button. Only
|
|
177
|
+
// the trial arm relies on `status`.
|
|
178
|
+
const ent = state?.entitlement;
|
|
179
|
+
const status = ent?.status;
|
|
180
|
+
const onPaidPlan = !!ent?.plan;
|
|
181
|
+
const notProvisioned =
|
|
182
|
+
!ent || status === "not_installed" || (status == null && !onPaidPlan);
|
|
183
|
+
if (notProvisioned) {
|
|
177
184
|
if (mounted.current) {
|
|
178
185
|
setNotReady(true);
|
|
179
186
|
setLoading(false);
|
|
@@ -286,34 +293,64 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
|
|
|
286
293
|
}
|
|
287
294
|
|
|
288
295
|
function CreditsBilling({ context, state, appKey }) {
|
|
289
|
-
// The Stripe portal session lets an EXISTING paid customer manage
|
|
290
|
-
//
|
|
291
|
-
// selection is the <PlanGrid> below, which pre-creates a per-tier checkout.
|
|
296
|
+
// The Stripe portal session lets an EXISTING paid customer manage/switch/cancel
|
|
297
|
+
// their plan, with proration handled by Stripe.
|
|
292
298
|
const { portalUrl, loading, error, notReady } = usePortalSession({
|
|
293
299
|
context,
|
|
294
300
|
state,
|
|
295
301
|
appKey,
|
|
296
302
|
});
|
|
297
303
|
|
|
298
|
-
const hasPlans = (state?.plans?.length ?? 0) > 0;
|
|
299
304
|
const onPaidPlan = !!state?.entitlement?.plan;
|
|
300
305
|
|
|
306
|
+
// PAID: do NOT show the plan picker. The picker's "Choose" starts a NEW Stripe
|
|
307
|
+
// Checkout subscription — clicking another tier would create a SECOND
|
|
308
|
+
// subscription (double-bill), not switch in place. Plan changes (upgrade/
|
|
309
|
+
// downgrade/cancel) go through the Stripe Customer Portal, which swaps the
|
|
310
|
+
// subscription item with proration. So on a paid plan we show the meter + a
|
|
311
|
+
// "Manage subscription" portal link only. (Matches toast/sparkfly.)
|
|
312
|
+
if (onPaidPlan) {
|
|
313
|
+
return (
|
|
314
|
+
<Flex direction="column" gap="medium">
|
|
315
|
+
<CreditMeter
|
|
316
|
+
entitlement={state?.entitlement}
|
|
317
|
+
creditMeter={state?.credit_meter}
|
|
318
|
+
/>
|
|
319
|
+
{!notReady &&
|
|
320
|
+
(error ? (
|
|
321
|
+
<Alert title="Couldn't open billing" variant="danger">
|
|
322
|
+
<Text>{error}</Text>
|
|
323
|
+
</Alert>
|
|
324
|
+
) : (
|
|
325
|
+
<LoadingButton
|
|
326
|
+
href={portalUrl ? { url: portalUrl, external: true } : undefined}
|
|
327
|
+
loading={loading}
|
|
328
|
+
disabled={!portalUrl}
|
|
329
|
+
variant="primary"
|
|
330
|
+
>
|
|
331
|
+
{portalUrl ? "Manage subscription" : "Preparing billing…"}
|
|
332
|
+
</LoadingButton>
|
|
333
|
+
))}
|
|
334
|
+
<Text format={{ fontStyle: "italic" }}>
|
|
335
|
+
Change or cancel your plan in Stripe — billing is managed across all
|
|
336
|
+
Growth Operations apps.
|
|
337
|
+
</Text>
|
|
338
|
+
</Flex>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// FREE tier: show the upgrade path (PlanGrid → first-paid Checkout). PlanGrid
|
|
343
|
+
// renders nothing if no plans are mirrored yet, so fall back to a portal CTA
|
|
344
|
+
// then so the tab isn't empty.
|
|
345
|
+
const hasPlans = (state?.plans?.length ?? 0) > 0;
|
|
301
346
|
return (
|
|
302
347
|
<Flex direction="column" gap="medium">
|
|
303
348
|
<CreditMeter
|
|
304
349
|
entitlement={state?.entitlement}
|
|
305
350
|
creditMeter={state?.credit_meter}
|
|
306
351
|
/>
|
|
307
|
-
|
|
308
|
-
{/* The dynamically-rendered upgrade path (tiers from the Stripe catalog).
|
|
309
|
-
Renders nothing if no plans are mirrored yet. */}
|
|
310
352
|
<PlanGrid context={context} state={state} appKey={appKey} />
|
|
311
|
-
|
|
312
|
-
{/* Manage-billing link: relevant once the customer is on a paid plan
|
|
313
|
-
(change/cancel/invoices in Stripe). Hidden in the not-ready
|
|
314
|
-
(un-provisioned) state. When no plans are mirrored AND not on a paid
|
|
315
|
-
plan, fall back to the portal as the only CTA so the tab isn't empty. */}
|
|
316
|
-
{!notReady && (onPaidPlan || !hasPlans) &&
|
|
353
|
+
{!notReady && !hasPlans &&
|
|
317
354
|
(error ? (
|
|
318
355
|
<Alert title="Couldn't open billing" variant="danger">
|
|
319
356
|
<Text>{error}</Text>
|
|
@@ -323,19 +360,11 @@ function CreditsBilling({ context, state, appKey }) {
|
|
|
323
360
|
href={portalUrl ? { url: portalUrl, external: true } : undefined}
|
|
324
361
|
loading={loading}
|
|
325
362
|
disabled={!portalUrl}
|
|
326
|
-
variant=
|
|
363
|
+
variant="primary"
|
|
327
364
|
>
|
|
328
|
-
{portalUrl
|
|
329
|
-
? onPaidPlan
|
|
330
|
-
? "Manage billing in Stripe"
|
|
331
|
-
: "Buy credits in Stripe"
|
|
332
|
-
: "Preparing billing…"}
|
|
365
|
+
{portalUrl ? "Buy credits in Stripe" : "Preparing billing…"}
|
|
333
366
|
</LoadingButton>
|
|
334
367
|
))}
|
|
335
|
-
|
|
336
|
-
{/* Footer note only when PlanGrid isn't rendering one (PlanGrid owns the
|
|
337
|
-
canonical "managed in Stripe" line when plans are present — avoid
|
|
338
|
-
showing it twice). */}
|
|
339
368
|
{!hasPlans && (
|
|
340
369
|
<Text format={{ fontStyle: "italic" }}>
|
|
341
370
|
Billing is managed in Stripe across all Growth Operations apps.
|