@growth-operations/ui-shared 1.31.1 → 1.33.0

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.31.1",
3
+ "version": "1.33.0",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -103,7 +103,7 @@ export function AutoProvisionToggle({
103
103
  <Alert variant="warning" title="Disable automatic credit provisioning?">
104
104
  <Box flex="auto">
105
105
  <Flex direction="column" gap="small">
106
- <Text>{autoProvisionConfirmBody(appName)}</Text>
106
+ <Text>{autoProvisionConfirmBody(appName, appKey)}</Text>
107
107
  <Flex direction="row" gap="small">
108
108
  <Button
109
109
  variant="destructive"
@@ -22,7 +22,7 @@ import { useStrictModeEffect } from "../lib/useStrictModeEffect";
22
22
  import { CreditMeter } from "../home/CreditMeter";
23
23
  import { PlanGrid } from "./PlanGrid";
24
24
  import { AutoProvisionToggle } from "./AutoProvisionToggle";
25
- import { resolveAutoProvision } from "../lib/autoProvision";
25
+ import { autoProvisionBlockOf, resolveAutoProvision } from "../lib/autoProvision";
26
26
  import { refreshBillingActionTokens } from "../sdk/billing";
27
27
  import { getInvoices } from "../sdk/app/topUps";
28
28
 
@@ -321,12 +321,16 @@ export function TrialSubscriptionBilling({ context, state, appKey, openIframe =
321
321
  : null;
322
322
 
323
323
  // Tier picker — every status needs ONE of two distinct modes, never both:
324
- // trialing -> UPGRADE mode: a trialing sub already
324
+ // trialing / active -> UPGRADE mode: a subscription already
325
325
  // exists, so a higher-tier click swaps its item in place
326
326
  // (/v1/billing/upgrade/start) rather than starting a second
327
327
  // subscription. Lower tiers show disabled (Talk-to-sales; downgrades
328
328
  // aren't self-serve). currentOrder is REQUIRED here so PlanCard can
329
- // tell upgrade from downgrade.
329
+ // tell upgrade from downgrade — and it's a money-safety gate, not
330
+ // caution: the billing service's direction check no-ops when the
331
+ // account's plan matches no mirrored tier, so without a stamped
332
+ // current tier we can't tell a customer what an upgrade even is, and
333
+ // the picker stays hidden (the Stripe portal link above still works).
330
334
  // pending_purchase / canceled -> CHECKOUT mode: there is NO active
331
335
  // subscription to swap — every tier is a genuinely fresh purchase
332
336
  // (/v1/billing/checkout/start, PlanGrid's default). currentOrder is
@@ -336,17 +340,19 @@ export function TrialSubscriptionBilling({ context, state, appKey, openIframe =
336
340
  // (upgrade) or doesn't (checkout) — showing the upgrade endpoint with no
337
341
  // real subscription behind it would 404/error at click time.
338
342
  const isTrialing = ent.status === "trialing";
343
+ const isActive = ent.status === "active";
339
344
  const isCanceled = ent.status === "canceled";
340
345
  const needsCheckout = ent.status === "pending_purchase" || isCanceled;
341
346
  const plans = state?.plans ?? [];
342
- const currentOrder = isTrialing
343
- ? plans.find((p) => p.current)?.tier_order
344
- : undefined;
345
- // Trialing: only once we know the current tier (currentOrder drives
347
+ const currentOrder =
348
+ isTrialing || isActive
349
+ ? plans.find((p) => p.current)?.tier_order
350
+ : undefined;
351
+ // Trialing/active: only once we know the current tier (currentOrder drives
346
352
  // upgrade-vs-downgrade per card). Checkout: as soon as there's a catalog
347
353
  // to pick from — there's no "current tier" gate to wait on.
348
354
  const showPicker =
349
- (isTrialing && currentOrder != null && plans.length > 0) ||
355
+ ((isTrialing || isActive) && currentOrder != null && plans.length > 0) ||
350
356
  (needsCheckout && plans.length > 0);
351
357
 
352
358
  // Empty-catalog restart fallback (2026-10-13 sparkfly incident): a catalog
@@ -415,10 +421,11 @@ export function TrialSubscriptionBilling({ context, state, appKey, openIframe =
415
421
  : "Billing is managed in Stripe across all Growth Operations apps. Use the link above to update your plan, payment method, or view invoices."}
416
422
  </Text>
417
423
 
418
- {/* Trialing: current (marked), higher (upgradeable), lower (disabled,
419
- Talk-to-sales) — upgrading swaps the trialing sub onto the higher
420
- tier, keeping the trial end date. pending_purchase/canceled: every
421
- tier plain-actionable (fresh checkout, PlanGrid's default props/
424
+ {/* Trialing/active: current (marked), higher (upgradeable), lower
425
+ (disabled, Talk-to-sales) — upgrading swaps the existing sub onto
426
+ the higher tier (trial end date kept while trialing; prorated
427
+ immediately once active). pending_purchase/canceled: every tier
428
+ plain-actionable (fresh checkout, PlanGrid's default props/
422
429
  endpoint) — no currentOrder means PlanCard never marks a downgrade.
423
430
  The full `plans` list is passed either way; PlanCard decides each
424
431
  card's state from plan.current + currentOrder. */}
@@ -443,7 +450,9 @@ export function TrialSubscriptionBilling({ context, state, appKey, openIframe =
443
450
  ? "Restart charges your card on file and resumes syncing immediately — no checkout page. If the card needs updating, we'll send you to Stripe to finish. You can also switch to a different plan or billing interval."
444
451
  : needsCheckout
445
452
  ? "Choose a plan to start a new subscription and resume syncing your data."
446
- : "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."
453
+ : isActive
454
+ ? "Upgrade any time — the new tier applies right away with prorated billing. To move to a lower tier, talk to sales. Use Manage billing in Stripe above to cancel, update your payment method, or view invoices."
455
+ : "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."
447
456
  }
448
457
  // Canceled only: the account's last tier is still stamped
449
458
  // `current` (account.plan persists after cancel), so its card
@@ -682,7 +691,12 @@ export function CreditsBilling({ context, state, appKey, openIframe = null }) {
682
691
  <AutoProvisionToggle
683
692
  context={context}
684
693
  appKey={appKey}
685
- enabled={resolveAutoProvision(state?.auto_provision).enabled}
694
+ // Dual-read: the backends nest auto_provision under
695
+ // payload.state — a top-level-only read would snap a disabled
696
+ // toggle back to ON on every refetch.
697
+ enabled={
698
+ resolveAutoProvision(autoProvisionBlockOf(state)).enabled
699
+ }
686
700
  appName={state?.app?.name ?? "this app"}
687
701
  // The POST's second auth dep (see setAutoProvision). `state`
688
702
  // here is BillingTab's interval-refreshed copy, so this token is
@@ -14,8 +14,19 @@ export function autoProvisionSubtext(appName) {
14
14
  return `If monthly credits are used, this will automatically purchase another pack of credits so use of ${appName} is not impacted.`;
15
15
  }
16
16
 
17
- export function autoProvisionConfirmBody(appName) {
18
- return `Are you sure you want to disable automatic credit provisioning? If monthly credits are used, ${appName} will no longer function and workflow actions will fail.`;
17
+ // The ON→OFF confirmation's ending, per app. The default names the generic
18
+ // consequence (workflow actions fail); ATS has no workflow actions — its
19
+ // credit-gated features just stop working — so Andrew's 2026-10-19 copy names
20
+ // the user-visible consequence instead.
21
+ const CONFIRM_BODY_ENDINGS = {
22
+ applicant_tracking_system:
23
+ "and features like resume parsing will stop functioning until credit renew at the start of the next bill period.",
24
+ };
25
+ const DEFAULT_CONFIRM_BODY_ENDING = "and workflow actions will fail.";
26
+
27
+ export function autoProvisionConfirmBody(appName, appKey) {
28
+ const ending = CONFIRM_BODY_ENDINGS[appKey] ?? DEFAULT_CONFIRM_BODY_ENDING;
29
+ return `Are you sure you want to disable automatic credit provisioning? If monthly credits are used, ${appName} will no longer function ${ending}`;
19
30
  }
20
31
 
21
32
  export const AUTO_PROVISION_CONFIRM_YES = "Yes, disable this feature.";
@@ -28,6 +39,15 @@ export function resolveAutoProvision(autoProvision) {
28
39
  return { enabled: autoProvision?.enabled !== false };
29
40
  }
30
41
 
42
+ // The auto_provision block off a /v1/home PAYLOAD, dual-read like
43
+ // planFitOf/periodComparisonOf (lib/homeBlocks): base-hosted and ATS backends
44
+ // nest it under payload.state; a top-level read alone silently resolves to
45
+ // the default ON for a disabled account after every refetch/remount.
46
+ export function autoProvisionBlockOf(state) {
47
+ if (!state || typeof state !== "object") return undefined;
48
+ return state.state?.auto_provision ?? state.auto_provision;
49
+ }
50
+
31
51
  // The toggle's interaction state. `enabled` itself lives in the component
32
52
  // (seeded from the payload, updated on save); this machine owns everything
33
53
  // around it: the ON→OFF confirmation and the in-flight POST.