@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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 (no entitlement, or status null/
174
- // not_installed) has nothing to manage; show the calm "not ready" state.
175
- const status = state?.entitlement?.status;
176
- if (!state.entitlement || status == null || status === "not_installed") {
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 their plan
290
- // / payment method / invoices (same pattern as the trial arm). NEW plan
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={onPaidPlan ? "secondary" : "primary"}
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.