@growth-operations/ui-shared 1.5.6 → 1.5.8

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.6",
3
+ "version": "1.5.8",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -140,9 +140,10 @@ function StatusPanel({ entitlement }) {
140
140
  );
141
141
  }
142
142
 
143
- // Pre-creates a Stripe billing/checkout portal session on mount and returns
144
- // { portalUrl, loading, error }. Shared by both arms — the credits arm reuses
145
- // the same portal endpoint for v1 (see note in CreditsBilling below).
143
+ // Pre-creates a Stripe Customer Portal session on mount (in-iframe POST) and
144
+ // returns { portalUrl, loading, error, notReady }. Used by the TRIAL arm only —
145
+ // the credits arm builds a direct /v1/billing/portal/start link instead (no
146
+ // in-iframe fetch, avoiding the billing service's cold-start timeout).
146
147
  function usePortalSession({ context, state, appKey }) {
147
148
  const [loading, setLoading] = useState(true);
148
149
  const [error, setError] = useState(null);
@@ -169,8 +170,8 @@ function usePortalSession({ context, state, appKey }) {
169
170
  resetOnUnmount();
170
171
  return;
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/
173
+ // No Stripe customer yet → don't even try to open a portal. A trial
174
+ // install that hasn't been provisioned (no entitlement, or status null/
174
175
  // not_installed) has nothing to manage; show the calm "not ready" state.
175
176
  const status = state?.entitlement?.status;
176
177
  if (!state.entitlement || status == null || status === "not_installed") {
@@ -286,16 +287,28 @@ function TrialSubscriptionBilling({ context, state, appKey }) {
286
287
  }
287
288
 
288
289
  function CreditsBilling({ context, state, appKey }) {
289
- // The Stripe portal session lets an EXISTING paid customer manage/switch/cancel
290
- // their plan, with proration handled by Stripe.
291
- const { portalUrl, loading, error, notReady } = usePortalSession({
292
- context,
293
- state,
294
- appKey,
295
- });
296
-
297
290
  const onPaidPlan = !!state?.entitlement?.plan;
298
291
 
292
+ // Direct external link to the billing service's GET /v1/billing/portal/start,
293
+ // which resolves the customer + creates the Customer Portal session server-side
294
+ // and 303s to Stripe — IN THE OPENED TAB. No in-iframe portal-session fetch
295
+ // (that round trip hit the billing service's cold-start/CPU stall and surfaced
296
+ // as the iframe's 15s "Gateway took too long" error). Same pattern as
297
+ // PlanGrid's /checkout/start "Choose" button. null (button disabled) until
298
+ // billing_base_url is known.
299
+ const base = state?.billing_base_url ?? null;
300
+ const portalId = context?.portal?.id;
301
+ const returnUrl = state?.app_id
302
+ ? `https://app.hubspot.com/app/${portalId}/${state.app_id}/billing`
303
+ : "https://app.hubspot.com/";
304
+ const portalStartUrl =
305
+ base && appKey && portalId
306
+ ? `${base}/v1/billing/portal/start` +
307
+ `?app_key=${encodeURIComponent(appKey)}` +
308
+ `&portal_id=${encodeURIComponent(String(portalId))}` +
309
+ `&return_url=${encodeURIComponent(returnUrl)}`
310
+ : null;
311
+
299
312
  // PAID: do NOT show the plan picker. The picker's "Choose" starts a NEW Stripe
300
313
  // Checkout subscription — clicking another tier would create a SECOND
301
314
  // subscription (double-bill), not switch in place. Plan changes (upgrade/
@@ -309,21 +322,15 @@ function CreditsBilling({ context, state, appKey }) {
309
322
  entitlement={state?.entitlement}
310
323
  creditMeter={state?.credit_meter}
311
324
  />
312
- {!notReady &&
313
- (error ? (
314
- <Alert title="Couldn't open billing" variant="danger">
315
- <Text>{error}</Text>
316
- </Alert>
317
- ) : (
318
- <LoadingButton
319
- href={portalUrl ? { url: portalUrl, external: true } : undefined}
320
- loading={loading}
321
- disabled={!portalUrl}
322
- variant="primary"
323
- >
324
- {portalUrl ? "Manage subscription" : "Preparing billing…"}
325
- </LoadingButton>
326
- ))}
325
+ <LoadingButton
326
+ href={
327
+ portalStartUrl ? { url: portalStartUrl, external: true } : undefined
328
+ }
329
+ disabled={!portalStartUrl}
330
+ variant="primary"
331
+ >
332
+ {portalStartUrl ? "Manage subscription" : "Preparing billing…"}
333
+ </LoadingButton>
327
334
  <Text format={{ fontStyle: "italic" }}>
328
335
  Change or cancel your plan in Stripe — billing is managed across all
329
336
  Growth Operations apps.
@@ -343,21 +350,17 @@ function CreditsBilling({ context, state, appKey }) {
343
350
  creditMeter={state?.credit_meter}
344
351
  />
345
352
  <PlanGrid context={context} state={state} appKey={appKey} />
346
- {!notReady && !hasPlans &&
347
- (error ? (
348
- <Alert title="Couldn't open billing" variant="danger">
349
- <Text>{error}</Text>
350
- </Alert>
351
- ) : (
352
- <LoadingButton
353
- href={portalUrl ? { url: portalUrl, external: true } : undefined}
354
- loading={loading}
355
- disabled={!portalUrl}
356
- variant="primary"
357
- >
358
- {portalUrl ? "Buy credits in Stripe" : "Preparing billing…"}
359
- </LoadingButton>
360
- ))}
353
+ {!hasPlans && (
354
+ <LoadingButton
355
+ href={
356
+ portalStartUrl ? { url: portalStartUrl, external: true } : undefined
357
+ }
358
+ disabled={!portalStartUrl}
359
+ variant="primary"
360
+ >
361
+ {portalStartUrl ? "Buy credits in Stripe" : "Preparing billing…"}
362
+ </LoadingButton>
363
+ )}
361
364
  {!hasPlans && (
362
365
  <Text format={{ fontStyle: "italic" }}>
363
366
  Billing is managed in Stripe across all Growth Operations apps.