@stigmer/react 3.39.1-dev.20260928194002 → 3.40.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.
Files changed (180) hide show
  1. package/billing/AutoRechargeCard.js +1 -1
  2. package/billing/AutoRechargeCard.js.map +1 -1
  3. package/billing/BillingSection.d.ts +18 -1
  4. package/billing/BillingSection.d.ts.map +1 -1
  5. package/billing/BillingSection.js +21 -11
  6. package/billing/BillingSection.js.map +1 -1
  7. package/billing/ChangePlanDialog.d.ts +46 -0
  8. package/billing/ChangePlanDialog.d.ts.map +1 -0
  9. package/billing/ChangePlanDialog.js +90 -0
  10. package/billing/ChangePlanDialog.js.map +1 -0
  11. package/billing/CreditPackGrid.d.ts +1 -1
  12. package/billing/CreditPackGrid.js +1 -1
  13. package/billing/LowBalanceBanner.js +1 -1
  14. package/billing/LowBalanceBanner.js.map +1 -1
  15. package/billing/PaymentMethodCard.d.ts +9 -1
  16. package/billing/PaymentMethodCard.d.ts.map +1 -1
  17. package/billing/PaymentMethodCard.js +2 -2
  18. package/billing/PaymentMethodCard.js.map +1 -1
  19. package/billing/PlanCard.d.ts +27 -0
  20. package/billing/PlanCard.d.ts.map +1 -0
  21. package/billing/PlanCard.js +68 -0
  22. package/billing/PlanCard.js.map +1 -0
  23. package/billing/PlanPicker.d.ts +34 -0
  24. package/billing/PlanPicker.d.ts.map +1 -0
  25. package/billing/PlanPicker.js +46 -0
  26. package/billing/PlanPicker.js.map +1 -0
  27. package/billing/PlanSection.d.ts +45 -0
  28. package/billing/PlanSection.d.ts.map +1 -0
  29. package/billing/PlanSection.js +136 -0
  30. package/billing/PlanSection.js.map +1 -0
  31. package/billing/UpgradeNotice.d.ts +47 -0
  32. package/billing/UpgradeNotice.d.ts.map +1 -0
  33. package/billing/UpgradeNotice.js +49 -0
  34. package/billing/UpgradeNotice.js.map +1 -0
  35. package/billing/credit-packs.d.ts.map +1 -1
  36. package/billing/credit-packs.js +4 -1
  37. package/billing/credit-packs.js.map +1 -1
  38. package/billing/index.d.ts +26 -1
  39. package/billing/index.d.ts.map +1 -1
  40. package/billing/index.js +12 -0
  41. package/billing/index.js.map +1 -1
  42. package/billing/plan-features.d.ts +60 -0
  43. package/billing/plan-features.d.ts.map +1 -0
  44. package/billing/plan-features.js +128 -0
  45. package/billing/plan-features.js.map +1 -0
  46. package/billing/plan-state.d.ts +43 -0
  47. package/billing/plan-state.d.ts.map +1 -0
  48. package/billing/plan-state.js +69 -0
  49. package/billing/plan-state.js.map +1 -0
  50. package/billing/redirect.d.ts +31 -0
  51. package/billing/redirect.d.ts.map +1 -0
  52. package/billing/redirect.js +30 -0
  53. package/billing/redirect.js.map +1 -0
  54. package/billing/useBillingAccount.d.ts +10 -1
  55. package/billing/useBillingAccount.d.ts.map +1 -1
  56. package/billing/useBillingAccount.js +2 -2
  57. package/billing/useBillingAccount.js.map +1 -1
  58. package/billing/useChangePlan.d.ts +51 -0
  59. package/billing/useChangePlan.d.ts.map +1 -0
  60. package/billing/useChangePlan.js +61 -0
  61. package/billing/useChangePlan.js.map +1 -0
  62. package/billing/useCreateBillingPortalSession.d.ts +6 -3
  63. package/billing/useCreateBillingPortalSession.d.ts.map +1 -1
  64. package/billing/useCreateBillingPortalSession.js +11 -4
  65. package/billing/useCreateBillingPortalSession.js.map +1 -1
  66. package/billing/useCreateCheckoutSession.d.ts +7 -5
  67. package/billing/useCreateCheckoutSession.d.ts.map +1 -1
  68. package/billing/useCreateCheckoutSession.js +5 -3
  69. package/billing/useCreateCheckoutSession.js.map +1 -1
  70. package/billing/useCreatePaymentMethodSetupSession.d.ts +28 -0
  71. package/billing/useCreatePaymentMethodSetupSession.d.ts.map +1 -0
  72. package/billing/useCreatePaymentMethodSetupSession.js +50 -0
  73. package/billing/useCreatePaymentMethodSetupSession.js.map +1 -0
  74. package/billing/useEntitlements.d.ts +46 -0
  75. package/billing/useEntitlements.d.ts.map +1 -0
  76. package/billing/useEntitlements.js +37 -0
  77. package/billing/useEntitlements.js.map +1 -0
  78. package/billing/usePeriodEstimate.d.ts +35 -0
  79. package/billing/usePeriodEstimate.d.ts.map +1 -0
  80. package/billing/usePeriodEstimate.js +29 -0
  81. package/billing/usePeriodEstimate.js.map +1 -0
  82. package/billing/usePlans.d.ts +38 -0
  83. package/billing/usePlans.d.ts.map +1 -0
  84. package/billing/usePlans.js +33 -0
  85. package/billing/usePlans.js.map +1 -0
  86. package/billing/useSubscription.d.ts +39 -0
  87. package/billing/useSubscription.d.ts.map +1 -0
  88. package/billing/useSubscription.js +40 -0
  89. package/billing/useSubscription.js.map +1 -0
  90. package/index.d.ts +4 -2
  91. package/index.d.ts.map +1 -1
  92. package/index.js +4 -1
  93. package/index.js.map +1 -1
  94. package/internal/features.d.ts +11 -0
  95. package/internal/features.d.ts.map +1 -0
  96. package/internal/features.js +42 -0
  97. package/internal/features.js.map +1 -0
  98. package/licenses/license-format.d.ts +5 -3
  99. package/licenses/license-format.d.ts.map +1 -1
  100. package/licenses/license-format.js +10 -19
  101. package/licenses/license-format.js.map +1 -1
  102. package/package.json +5 -5
  103. package/plan-catalog/PlanCatalogConsole.d.ts +22 -0
  104. package/plan-catalog/PlanCatalogConsole.d.ts.map +1 -0
  105. package/plan-catalog/PlanCatalogConsole.js +90 -0
  106. package/plan-catalog/PlanCatalogConsole.js.map +1 -0
  107. package/plan-catalog/PlanCreateForm.d.ts +17 -0
  108. package/plan-catalog/PlanCreateForm.d.ts.map +1 -0
  109. package/plan-catalog/PlanCreateForm.js +52 -0
  110. package/plan-catalog/PlanCreateForm.js.map +1 -0
  111. package/plan-catalog/PlansAccessNotice.d.ts +10 -0
  112. package/plan-catalog/PlansAccessNotice.d.ts.map +1 -0
  113. package/plan-catalog/PlansAccessNotice.js +13 -0
  114. package/plan-catalog/PlansAccessNotice.js.map +1 -0
  115. package/plan-catalog/index.d.ts +8 -0
  116. package/plan-catalog/index.d.ts.map +1 -0
  117. package/plan-catalog/index.js +10 -0
  118. package/plan-catalog/index.js.map +1 -0
  119. package/plan-catalog/plan-draft.d.ts +30 -0
  120. package/plan-catalog/plan-draft.d.ts.map +1 -0
  121. package/plan-catalog/plan-draft.js +91 -0
  122. package/plan-catalog/plan-draft.js.map +1 -0
  123. package/plan-catalog/usePlanMutations.d.ts +42 -0
  124. package/plan-catalog/usePlanMutations.d.ts.map +1 -0
  125. package/plan-catalog/usePlanMutations.js +57 -0
  126. package/plan-catalog/usePlanMutations.js.map +1 -0
  127. package/settings/TeamsSection.d.ts.map +1 -1
  128. package/settings/TeamsSection.js +19 -2
  129. package/settings/TeamsSection.js.map +1 -1
  130. package/settings/settings-nav.d.ts.map +1 -1
  131. package/settings/settings-nav.js +7 -1
  132. package/settings/settings-nav.js.map +1 -1
  133. package/settings/useSettingsNavGroups.d.ts.map +1 -1
  134. package/settings/useSettingsNavGroups.js +4 -1
  135. package/settings/useSettingsNavGroups.js.map +1 -1
  136. package/src/billing/AutoRechargeCard.tsx +2 -2
  137. package/src/billing/BillingSection.tsx +68 -11
  138. package/src/billing/ChangePlanDialog.tsx +221 -0
  139. package/src/billing/CreditPackGrid.tsx +1 -1
  140. package/src/billing/LowBalanceBanner.tsx +1 -1
  141. package/src/billing/PaymentMethodCard.tsx +28 -2
  142. package/src/billing/PlanCard.tsx +171 -0
  143. package/src/billing/PlanPicker.tsx +178 -0
  144. package/src/billing/PlanSection.tsx +234 -0
  145. package/src/billing/UpgradeNotice.tsx +95 -0
  146. package/src/billing/credit-packs.ts +4 -1
  147. package/src/billing/index.ts +26 -1
  148. package/src/billing/plan-features.ts +162 -0
  149. package/src/billing/plan-state.ts +92 -0
  150. package/src/billing/redirect.ts +45 -0
  151. package/src/billing/useBillingAccount.ts +12 -0
  152. package/src/billing/useChangePlan.ts +115 -0
  153. package/src/billing/useCreateBillingPortalSession.ts +17 -6
  154. package/src/billing/useCreateCheckoutSession.ts +12 -7
  155. package/src/billing/useCreatePaymentMethodSetupSession.ts +77 -0
  156. package/src/billing/useEntitlements.ts +80 -0
  157. package/src/billing/usePeriodEstimate.ts +59 -0
  158. package/src/billing/usePlans.ts +65 -0
  159. package/src/billing/useSubscription.ts +70 -0
  160. package/src/index.ts +54 -0
  161. package/src/internal/features.ts +49 -0
  162. package/src/licenses/license-format.ts +10 -19
  163. package/src/plan-catalog/PlanCatalogConsole.tsx +197 -0
  164. package/src/plan-catalog/PlanCreateForm.tsx +167 -0
  165. package/src/plan-catalog/PlansAccessNotice.tsx +20 -0
  166. package/src/plan-catalog/index.ts +14 -0
  167. package/src/plan-catalog/plan-draft.ts +116 -0
  168. package/src/plan-catalog/usePlanMutations.ts +94 -0
  169. package/src/settings/TeamsSection.tsx +20 -2
  170. package/src/settings/settings-nav.ts +7 -0
  171. package/src/settings/useSettingsNavGroups.ts +8 -1
  172. package/src/team/CreateTeamForm.tsx +12 -3
  173. package/src/team/TeamMembersPanel.tsx +6 -2
  174. package/team/CreateTeamForm.d.ts +6 -1
  175. package/team/CreateTeamForm.d.ts.map +1 -1
  176. package/team/CreateTeamForm.js +4 -2
  177. package/team/CreateTeamForm.js.map +1 -1
  178. package/team/TeamMembersPanel.d.ts.map +1 -1
  179. package/team/TeamMembersPanel.js +3 -1
  180. package/team/TeamMembersPanel.js.map +1 -1
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+ import type { Plan } from "@stigmer/protos/ai/stigmer/billing/plan/v1/api_pb";
5
+ import { Button } from "../button/index.js";
6
+ import { UNSTYLED_LIST } from "../internal/element-resets.js";
7
+ import {
8
+ formatManagedOrganizations,
9
+ formatMonthlyMinimum,
10
+ explainUsageShare,
11
+ offeredFeatures,
12
+ } from "./plan-features.js";
13
+ import { planMove, type PlanMove, type PlanStanding } from "./plan-state.js";
14
+
15
+ /** Where Enterprise enquiries go by default. */
16
+ export const ENTERPRISE_CONTACT_URL = "https://stigmer.ai/contact-sales";
17
+
18
+ /** Props for {@link PlanPicker}. */
19
+ export interface PlanPickerProps {
20
+ /** The plans that can be bought, cheapest first, as `buyablePlans` orders them. */
21
+ readonly plans: readonly Plan[];
22
+ /** Where the organization stands. */
23
+ readonly standing: PlanStanding;
24
+ /** The live period's end, which a cancel runs to. */
25
+ readonly periodEnd?: Date;
26
+ /**
27
+ * Whether the viewer may change the plan (`can_manage_billing`). Without
28
+ * it the comparison is read-only.
29
+ */
30
+ readonly canManage: boolean;
31
+ /** Called with the move a choice makes; the host confirms it. */
32
+ readonly onChoose: (move: PlanMove) => void;
33
+ /** Disables every choice, e.g. while a change is in flight. */
34
+ readonly disabled?: boolean;
35
+ /** Where "Talk to us" for Enterprise links. Defaults to {@link ENTERPRISE_CONTACT_URL}. */
36
+ readonly enterpriseHref?: string;
37
+ /** Additional CSS class names. */
38
+ readonly className?: string;
39
+ }
40
+
41
+ /**
42
+ * The plan comparison: Free, each plan that can be bought, and
43
+ * Enterprise, each with its price and what it offers today. A choice
44
+ * reports the move it makes (subscribe, switch, resume or cancel) for the
45
+ * host to confirm; the plan in force reads "Current plan".
46
+ */
47
+ export function PlanPicker({
48
+ plans,
49
+ standing,
50
+ periodEnd,
51
+ canManage,
52
+ onChoose,
53
+ disabled,
54
+ enterpriseHref = ENTERPRISE_CONTACT_URL,
55
+ className,
56
+ }: PlanPickerProps) {
57
+ const freeMove = planMove(standing, "free", periodEnd);
58
+ return (
59
+ <ul className={cn(UNSTYLED_LIST, "stg:grid stg:gap-3 stg:sm:grid-cols-2", className)} aria-label="Plans">
60
+ <PlanOption
61
+ name="Free"
62
+ price="$0/month"
63
+ detail="Usage is paid from credits."
64
+ features={["Channels", "Sharing", "Unlimited members"]}
65
+ current={standing.kind === "free"}
66
+ action={canManage && freeMove !== null ? { label: "Move to Free", move: freeMove } : undefined}
67
+ disabled={disabled}
68
+ onChoose={onChoose}
69
+ />
70
+ {plans.map((plan) => {
71
+ const move = planMove(standing, plan, periodEnd);
72
+ const managed = formatManagedOrganizations(plan.spec?.entitlements, plan.spec?.terms);
73
+ return (
74
+ <PlanOption
75
+ key={plan.metadata?.id ?? plan.metadata?.slug}
76
+ name={plan.metadata?.name ?? ""}
77
+ price={formatMonthlyMinimum(plan.spec?.terms)}
78
+ detail={explainUsageShare(plan.spec?.terms)}
79
+ features={[
80
+ ...offeredFeatures(plan.spec?.entitlements).map((feature) => feature.label),
81
+ ...(managed === "" ? [] : [managed]),
82
+ ]}
83
+ current={move === null && standing.kind !== "free"}
84
+ action={canManage && move !== null ? { label: actionLabel(move), move } : undefined}
85
+ disabled={disabled}
86
+ onChoose={onChoose}
87
+ />
88
+ );
89
+ })}
90
+ <li className="stg:flex stg:flex-col stg:rounded-lg stg:border stg:border-border stg:bg-card stg:p-4">
91
+ <p className="stg:text-sm stg:font-medium stg:text-foreground">Enterprise</p>
92
+ <p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">
93
+ A dedicated deployment, SSO enforcement, an SLA and annual invoicing, quoted for your organization.
94
+ </p>
95
+ <a
96
+ href={enterpriseHref}
97
+ target="_blank"
98
+ rel="noreferrer"
99
+ className="stg:mt-3 stg:self-start stg:text-xs stg:font-medium stg:text-primary stg:hover:underline"
100
+ >
101
+ Talk to us
102
+ </a>
103
+ </li>
104
+ </ul>
105
+ );
106
+ }
107
+
108
+ function PlanOption({
109
+ name,
110
+ price,
111
+ detail,
112
+ features,
113
+ current,
114
+ action,
115
+ disabled,
116
+ onChoose,
117
+ }: {
118
+ readonly name: string;
119
+ readonly price: string;
120
+ readonly detail: string;
121
+ readonly features: readonly string[];
122
+ readonly current: boolean;
123
+ readonly action?: { readonly label: string; readonly move: PlanMove };
124
+ readonly disabled?: boolean;
125
+ readonly onChoose: (move: PlanMove) => void;
126
+ }) {
127
+ return (
128
+ <li
129
+ className={cn(
130
+ "stg:flex stg:flex-col stg:rounded-lg stg:border stg:bg-card stg:p-4",
131
+ current ? "stg:border-primary" : "stg:border-border",
132
+ )}
133
+ aria-current={current ? "true" : undefined}
134
+ >
135
+ <div className="stg:flex stg:items-baseline stg:justify-between stg:gap-2">
136
+ <p className="stg:text-sm stg:font-medium stg:text-foreground">{name}</p>
137
+ <p className="stg:text-xs stg:tabular-nums stg:text-foreground">{price}</p>
138
+ </div>
139
+ {detail !== "" && <p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">{detail}</p>}
140
+ <ul className={cn(UNSTYLED_LIST, "stg:mt-2 stg:space-y-0.5 stg:text-xs stg:text-muted-foreground")}>
141
+ {features.map((feature) => (
142
+ <li key={feature}>{feature}</li>
143
+ ))}
144
+ </ul>
145
+ <div className="stg:mt-auto stg:pt-3">
146
+ {current ? (
147
+ <span className="stg:text-xs stg:font-medium stg:text-primary">Current plan</span>
148
+ ) : action ? (
149
+ <Button
150
+ variant={action.move.kind === "cancel" ? "outline" : "primary"}
151
+ size="xs"
152
+ disabled={disabled}
153
+ onClick={() => onChoose(action.move)}
154
+ >
155
+ {action.label}
156
+ </Button>
157
+ ) : null}
158
+ </div>
159
+ </li>
160
+ );
161
+ }
162
+
163
+ function actionLabel(move: PlanMove): string {
164
+ switch (move.kind) {
165
+ case "subscribe":
166
+ return "Subscribe";
167
+ case "switch":
168
+ return `Switch to ${move.to.metadata?.name ?? "this plan"}`;
169
+ case "resume":
170
+ return `Keep ${move.to.metadata?.name ?? "this plan"}`;
171
+ case "cancel":
172
+ return "Move to Free";
173
+ default: {
174
+ const exhaustive: never = move;
175
+ return String(exhaustive);
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { timestampDate } from "@bufbuild/protobuf/wkt";
5
+ import { cn } from "@stigmer/theme";
6
+ import { getUserMessage } from "@stigmer/sdk";
7
+ import { ApiResourceKind, useResourceAvailable } from "../deployment-mode.js";
8
+ import { useCheckPermission } from "../iam-policy/useCheckPermission.js";
9
+ import { ChangePlanDialog } from "./ChangePlanDialog.js";
10
+ import { PlanCard } from "./PlanCard.js";
11
+ import { PlanPicker } from "./PlanPicker.js";
12
+ import { buyablePlans, featuresLost } from "./plan-features.js";
13
+ import { planMove, planStanding, standingPlanId, type PlanMove } from "./plan-state.js";
14
+ import type { BillingRedirect } from "./redirect.js";
15
+ import { useCancelSubscription, useChangePlan } from "./useChangePlan.js";
16
+ import { useCreatePaymentMethodSetupSession } from "./useCreatePaymentMethodSetupSession.js";
17
+ import { usePeriodEstimate } from "./usePeriodEstimate.js";
18
+ import { usePlans } from "./usePlans.js";
19
+ import { useSubscription } from "./useSubscription.js";
20
+
21
+ /** How often, and how long, the section waits for a just-saved card to reach the account. */
22
+ const CARD_POLL_MS = 2_000;
23
+ const CARD_WAIT_MS = 30_000;
24
+
25
+ /** Props for {@link PlanSection}. */
26
+ export interface PlanSectionProps {
27
+ /** The organization whose plan is shown. */
28
+ readonly orgId: string;
29
+ /**
30
+ * The organization is platform-managed: it runs on its integrator's
31
+ * plan, and the section says so instead of offering plans.
32
+ */
33
+ readonly managed?: boolean;
34
+ /** Whether the organization's billing account holds a saved card. */
35
+ readonly hasPaymentMethod: boolean;
36
+ /** Re-read the billing account, so a card saved on Stripe's page shows. */
37
+ readonly onRefreshAccount?: () => void;
38
+ /** The host's seam for Stripe-hosted pages (see {@link BillingRedirect}). */
39
+ readonly redirect?: BillingRedirect;
40
+ /**
41
+ * The plan the person was choosing when they left to save a card (the
42
+ * `plan` query Stripe returns with). The section reopens that choice for
43
+ * an explicit confirm; it never subscribes on its own.
44
+ */
45
+ readonly resumePlanId?: string;
46
+ /** Called once the resumed choice has been taken up, so the host can clear its query. */
47
+ readonly onResumeHandled?: () => void;
48
+ /** The clock the standing reads. Defaults to each render's time. */
49
+ readonly now?: Date;
50
+ /** Additional CSS class names. */
51
+ readonly className?: string;
52
+ }
53
+
54
+ /**
55
+ * An organization's plan on Stigmer Cloud: the current plan with its
56
+ * standing and the period's estimated invoice, the plan comparison, and
57
+ * the confirm step for a subscribe, switch, resume or cancel.
58
+ *
59
+ * Everyone who can view the organization's billing reads it; only those
60
+ * who can manage billing (its admins) see the choices. Renders nothing on
61
+ * an edition that serves no subscriptions.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * <PlanSection orgId={orgId} hasPaymentMethod={hasCard} />
66
+ * ```
67
+ */
68
+ export function PlanSection({
69
+ orgId,
70
+ managed = false,
71
+ hasPaymentMethod,
72
+ onRefreshAccount,
73
+ redirect,
74
+ resumePlanId,
75
+ onResumeHandled,
76
+ now,
77
+ className,
78
+ }: PlanSectionProps) {
79
+ const available = useResourceAvailable(ApiResourceKind.subscription);
80
+ const renderNow = now ?? new Date();
81
+ const catalog = usePlans({ includeRetired: true, enabled: available });
82
+ const current = useSubscription(available ? orgId : null);
83
+ const standing = planStanding(current.subscription, renderNow);
84
+ const livePlanId = standingPlanId(standing);
85
+ const estimate = usePeriodEstimate(orgId, { enabled: available && !managed && livePlanId !== "" });
86
+ const manage = useCheckPermission(available && orgId ? { kind: "organization", id: orgId } : null, "can_manage_billing");
87
+ const canManage = !managed && !manage.isLoading && manage.allowed;
88
+
89
+ const changer = useChangePlan();
90
+ const canceler = useCancelSubscription();
91
+ const setup = useCreatePaymentMethodSetupSession(redirect);
92
+ const [move, setMove] = useState<PlanMove | null>(null);
93
+ const [awaitingCard, setAwaitingCard] = useState(false);
94
+
95
+ const buyable = useMemo(() => buyablePlans(catalog.plans ?? []), [catalog.plans]);
96
+ const planById = useCallback(
97
+ (planId: string) => catalog.plans?.find((plan) => plan.metadata?.id === planId),
98
+ [catalog.plans],
99
+ );
100
+ const planName = useCallback((planId: string) => planById(planId)?.metadata?.name ?? "your plan", [planById]);
101
+ const periodEndStamp = current.subscription?.status?.currentPeriodEnd;
102
+ const periodEnd = periodEndStamp === undefined ? undefined : timestampDate(periodEndStamp);
103
+
104
+ // Back from Stripe's card page: reopen the choice the person was making.
105
+ // Keyed by the plan resumed, and cleared once the host drops the query,
106
+ // because a desktop host stays mounted across returns and may resume again.
107
+ const resumedFor = useRef<string | null>(null);
108
+ useEffect(() => {
109
+ if (resumePlanId === undefined || resumePlanId === "") {
110
+ resumedFor.current = null;
111
+ return;
112
+ }
113
+ if (resumedFor.current === resumePlanId || catalog.plans === null || current.isLoading) {
114
+ return;
115
+ }
116
+ resumedFor.current = resumePlanId;
117
+ const target = buyable.find((plan) => plan.metadata?.id === resumePlanId);
118
+ const resumedMove = target === undefined ? null : planMove(standing, target, periodEnd);
119
+ if (resumedMove !== null) {
120
+ setMove(resumedMove);
121
+ setAwaitingCard(!hasPaymentMethod);
122
+ }
123
+ onResumeHandled?.();
124
+ }, [resumePlanId, catalog.plans, current.isLoading, buyable, standing, periodEnd, hasPaymentMethod, onResumeHandled]);
125
+
126
+ // The card lands on the account by webhook a few seconds after Stripe's
127
+ // page returns; poll the account until it does, bounded.
128
+ useEffect(() => {
129
+ if (!awaitingCard) return;
130
+ if (hasPaymentMethod) {
131
+ setAwaitingCard(false);
132
+ return;
133
+ }
134
+ const started = Date.now();
135
+ const timer = setInterval(() => {
136
+ if (Date.now() - started > CARD_WAIT_MS) {
137
+ setAwaitingCard(false);
138
+ clearInterval(timer);
139
+ return;
140
+ }
141
+ onRefreshAccount?.();
142
+ }, CARD_POLL_MS);
143
+ return () => clearInterval(timer);
144
+ }, [awaitingCard, hasPaymentMethod, onRefreshAccount]);
145
+
146
+ const close = useCallback(() => {
147
+ setMove(null);
148
+ setAwaitingCard(false);
149
+ changer.clearError();
150
+ canceler.clearError();
151
+ setup.clearError();
152
+ }, [changer.clearError, canceler.clearError, setup.clearError]);
153
+
154
+ const refreshAll = useCallback(() => {
155
+ current.refetch();
156
+ estimate.refetch();
157
+ }, [current.refetch, estimate.refetch]);
158
+
159
+ const confirm = useCallback(() => {
160
+ if (move === null) return;
161
+ const done = move.kind === "cancel" ? canceler.cancel(orgId) : changer.changePlan(orgId, move.to.metadata?.id ?? "");
162
+ done.then(
163
+ () => {
164
+ setMove(null);
165
+ refreshAll();
166
+ },
167
+ () => undefined,
168
+ );
169
+ }, [move, orgId, canceler.cancel, changer.changePlan, refreshAll]);
170
+
171
+ const addPaymentMethod = useCallback(() => {
172
+ const planId = move !== null && move.kind !== "cancel" ? move.to.metadata?.id : undefined;
173
+ setup.addPaymentMethod(orgId, planId).catch(() => undefined);
174
+ }, [move, orgId, setup.addPaymentMethod]);
175
+
176
+ if (!available) {
177
+ return null;
178
+ }
179
+
180
+ if (current.isLoading || catalog.isLoading) {
181
+ return (
182
+ <div className={cn("stg:space-y-3", className)} aria-busy="true" aria-label="Loading plan">
183
+ <div className="stg:h-24 stg:animate-pulse stg:rounded-lg stg:bg-muted-subtle" />
184
+ </div>
185
+ );
186
+ }
187
+
188
+ const readError = current.error ?? catalog.error;
189
+ if (readError) {
190
+ return (
191
+ <p className={cn("stg:text-xs stg:text-destructive", className)} role="alert">
192
+ {getUserMessage(readError)}
193
+ </p>
194
+ );
195
+ }
196
+
197
+ const nameInForce = livePlanId === "" ? "Free" : planName(livePlanId);
198
+ const moveError = move?.kind === "cancel" ? canceler.error : (changer.error ?? setup.error);
199
+ const losing = move?.kind === "switch" ? featuresLost(planById(move.fromPlanId), move.to) : [];
200
+
201
+ return (
202
+ <section className={cn("stg:space-y-3", className)} aria-label="Plan">
203
+ <PlanCard
204
+ planName={nameInForce}
205
+ standing={standing}
206
+ estimate={estimate.estimate}
207
+ estimateError={estimate.error}
208
+ managed={managed}
209
+ />
210
+ {!managed && (
211
+ <PlanPicker
212
+ plans={buyable}
213
+ standing={standing}
214
+ periodEnd={periodEnd}
215
+ canManage={canManage}
216
+ onChoose={setMove}
217
+ disabled={changer.isSubmitting || canceler.isSubmitting}
218
+ />
219
+ )}
220
+ <ChangePlanDialog
221
+ move={move}
222
+ planName={planName}
223
+ losing={losing}
224
+ hasPaymentMethod={hasPaymentMethod}
225
+ awaitingPaymentMethod={awaitingCard}
226
+ isSubmitting={changer.isSubmitting || canceler.isSubmitting || setup.isSubmitting}
227
+ error={moveError}
228
+ onConfirm={confirm}
229
+ onAddPaymentMethod={addPaymentMethod}
230
+ onClose={close}
231
+ />
232
+ </section>
233
+ );
234
+ }
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ import { cn } from "@stigmer/theme";
4
+ import { getErrorReason, getUserMessage } from "@stigmer/sdk";
5
+ import { Feature } from "@stigmer/protos/ai/stigmer/platform/v1/entitlement_pb";
6
+ import { FEATURE_COPY, isNamedFeature } from "../internal/features.js";
7
+
8
+ /**
9
+ * The reason Stigmer Cloud refuses an act the organization's plan does not
10
+ * include (iam/team/v1 and tenancy/organization/v1 command.proto).
11
+ */
12
+ export const PLAN_UPGRADE_REQUIRED = "PLAN_UPGRADE_REQUIRED";
13
+
14
+ /** The billing page the notice links to by default: the settings route every client serves. */
15
+ export const DEFAULT_BILLING_HREF = "/settings/billing";
16
+
17
+ /** The wire names the refusal's `feature` metadata carries. */
18
+ const FEATURE_BY_WIRE_NAME: Readonly<Record<string, Feature>> = {
19
+ teams: Feature.teams,
20
+ managed_organizations: Feature.managed_organizations,
21
+ };
22
+
23
+ /**
24
+ * The feature a refusal says the organization's plan lacks, when `error`
25
+ * is Stigmer Cloud's `PLAN_UPGRADE_REQUIRED`; `null` for any other error.
26
+ */
27
+ export function planUpgradeFeature(error: unknown): Feature | null {
28
+ const reason = getErrorReason(error);
29
+ if (reason?.reason !== PLAN_UPGRADE_REQUIRED) {
30
+ return null;
31
+ }
32
+ return FEATURE_BY_WIRE_NAME[reason.metadata.feature ?? ""] ?? null;
33
+ }
34
+
35
+ /** Props for {@link UpgradeNotice}. */
36
+ export interface UpgradeNoticeProps {
37
+ /** The feature the organization's plan lacks. */
38
+ readonly feature: Feature;
39
+ /**
40
+ * The refusal, when there was one: its message is the server's own
41
+ * copy, which names the plan that unlocks the act. Absent, the notice is
42
+ * shown ahead of an attempt.
43
+ */
44
+ readonly error?: Error | null;
45
+ /**
46
+ * The plan that unlocks the feature, named ahead of an attempt, e.g. the
47
+ * cheapest one that includes it (`lowestPlanWith`). With a refusal,
48
+ * the server's copy names it instead.
49
+ */
50
+ readonly unlockingPlanName?: string;
51
+ /** Where the plans are, in the host's routing. Defaults to {@link DEFAULT_BILLING_HREF}. */
52
+ readonly billingHref?: string;
53
+ /** Additional CSS class names. */
54
+ readonly className?: string;
55
+ }
56
+
57
+ /**
58
+ * Says an act needs a plan that includes it, and where to change the plan,
59
+ * in place of a bare refusal. Everything the organization already has keeps
60
+ * working; only creating more is refused.
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const feature = planUpgradeFeature(error);
65
+ * if (feature !== null) return <UpgradeNotice feature={feature} error={error} />;
66
+ * ```
67
+ */
68
+ export function UpgradeNotice({
69
+ feature,
70
+ error,
71
+ unlockingPlanName,
72
+ billingHref = DEFAULT_BILLING_HREF,
73
+ className,
74
+ }: UpgradeNoticeProps) {
75
+ const label = isNamedFeature(feature) ? FEATURE_COPY[feature].label : "This feature";
76
+ const ahead =
77
+ unlockingPlanName === undefined || unlockingPlanName === ""
78
+ ? `${label} are not included in this organization's plan.`
79
+ : `${label} need the ${unlockingPlanName} plan or above.`;
80
+ const message = error ? getUserMessage(error) : ahead;
81
+ return (
82
+ <div
83
+ className={cn(
84
+ "stg:rounded-md stg:border stg:border-border stg:bg-muted-subtle stg:px-3 stg:py-2 stg:text-xs",
85
+ className,
86
+ )}
87
+ role="status"
88
+ >
89
+ <p className="stg:text-foreground">{message}</p>
90
+ <a href={billingHref} className="stg:mt-1 stg:inline-block stg:font-medium stg:text-primary stg:hover:underline">
91
+ View plans
92
+ </a>
93
+ </div>
94
+ );
95
+ }
@@ -34,8 +34,11 @@ export const CREDIT_PACKS: readonly CreditPackInfo[] = [
34
34
  credits: 5_000,
35
35
  },
36
36
  {
37
+ // Named "Scale", not "Team", so it never reads as the Team plan on the
38
+ // page that sells both; its id stays "team", the wire value every
39
+ // purchase row and checkout call carries.
37
40
  packId: "team",
38
- displayName: "Team",
41
+ displayName: "Scale",
39
42
  description: "For production workloads",
40
43
  priceCents: 20_000,
41
44
  credits: 20_000,
@@ -5,13 +5,21 @@
5
5
 
6
6
  // Data hooks
7
7
  export { useBillingAccount } from "./useBillingAccount.js";
8
- export type { UseBillingAccountReturn } from "./useBillingAccount.js";
8
+ export type { UseBillingAccountOptions, UseBillingAccountReturn } from "./useBillingAccount.js";
9
9
  export { useCreditLedger } from "./useCreditLedger.js";
10
10
  export type { UseCreditLedgerReturn, UseCreditLedgerOptions } from "./useCreditLedger.js";
11
11
  export { useBillingUsageReport } from "./useBillingUsageReport.js";
12
12
  export type { UseBillingUsageReportReturn } from "./useBillingUsageReport.js";
13
13
  export { useCustomerModelPricing } from "./useCustomerModelPricing.js";
14
14
  export type { UseCustomerModelPricingReturn } from "./useCustomerModelPricing.js";
15
+ export { usePlans } from "./usePlans.js";
16
+ export type { UsePlansOptions, UsePlansReturn } from "./usePlans.js";
17
+ export { useSubscription } from "./useSubscription.js";
18
+ export type { UseSubscriptionReturn } from "./useSubscription.js";
19
+ export { useEntitlements } from "./useEntitlements.js";
20
+ export type { UseEntitlementsOptions, UseEntitlementsReturn } from "./useEntitlements.js";
21
+ export { usePeriodEstimate } from "./usePeriodEstimate.js";
22
+ export type { UsePeriodEstimateOptions, UsePeriodEstimateReturn } from "./usePeriodEstimate.js";
15
23
 
16
24
  // Behavior hooks
17
25
  export { useCreateCheckoutSession } from "./useCreateCheckoutSession.js";
@@ -21,6 +29,13 @@ export type {
21
29
  } from "./useCreateCheckoutSession.js";
22
30
  export { useCreateBillingPortalSession } from "./useCreateBillingPortalSession.js";
23
31
  export type { UseCreateBillingPortalSessionReturn } from "./useCreateBillingPortalSession.js";
32
+ export { useChangePlan, useCancelSubscription } from "./useChangePlan.js";
33
+ export type { UseChangePlanReturn, UseCancelSubscriptionReturn } from "./useChangePlan.js";
34
+ export { useCreatePaymentMethodSetupSession } from "./useCreatePaymentMethodSetupSession.js";
35
+ export type { UseCreatePaymentMethodSetupSessionReturn } from "./useCreatePaymentMethodSetupSession.js";
36
+ export type { BillingRedirect } from "./redirect.js";
37
+ export { planStanding, planMove } from "./plan-state.js";
38
+ export type { PlanStanding, PlanMove } from "./plan-state.js";
24
39
  export { useSetAutoRechargeConfig } from "./useSetAutoRechargeConfig.js";
25
40
  export type {
26
41
  SetAutoRechargeConfigInput,
@@ -42,6 +57,16 @@ export { CreditLedgerTable } from "./CreditLedgerTable.js";
42
57
  export type { CreditLedgerTableProps } from "./CreditLedgerTable.js";
43
58
  export { LowBalanceBanner } from "./LowBalanceBanner.js";
44
59
  export type { LowBalanceBannerProps } from "./LowBalanceBanner.js";
60
+ export { PlanSection } from "./PlanSection.js";
61
+ export type { PlanSectionProps } from "./PlanSection.js";
62
+ export { PlanCard } from "./PlanCard.js";
63
+ export type { PlanCardProps } from "./PlanCard.js";
64
+ export { PlanPicker, ENTERPRISE_CONTACT_URL } from "./PlanPicker.js";
65
+ export type { PlanPickerProps } from "./PlanPicker.js";
66
+ export { ChangePlanDialog, PAYMENT_METHOD_REQUIRED } from "./ChangePlanDialog.js";
67
+ export type { ChangePlanDialogProps } from "./ChangePlanDialog.js";
68
+ export { UpgradeNotice, planUpgradeFeature, PLAN_UPGRADE_REQUIRED, DEFAULT_BILLING_HREF } from "./UpgradeNotice.js";
69
+ export type { UpgradeNoticeProps } from "./UpgradeNotice.js";
45
70
 
46
71
  // Credit pack catalog and formatting utilities
47
72
  export { CREDIT_PACKS, formatPackPrice, formatCreditCount } from "./credit-packs.js";