@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
|
@@ -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
|
|
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 =
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
419
|
-
Talk-to-sales) — upgrading swaps the
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
package/src/lib/autoProvision.js
CHANGED
|
@@ -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
|
-
|
|
18
|
-
|
|
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.
|