@stigmer/react 3.39.0 → 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.
- package/billing/AutoRechargeCard.js +1 -1
- package/billing/AutoRechargeCard.js.map +1 -1
- package/billing/BillingSection.d.ts +18 -1
- package/billing/BillingSection.d.ts.map +1 -1
- package/billing/BillingSection.js +21 -11
- package/billing/BillingSection.js.map +1 -1
- package/billing/ChangePlanDialog.d.ts +46 -0
- package/billing/ChangePlanDialog.d.ts.map +1 -0
- package/billing/ChangePlanDialog.js +90 -0
- package/billing/ChangePlanDialog.js.map +1 -0
- package/billing/CreditPackGrid.d.ts +1 -1
- package/billing/CreditPackGrid.js +1 -1
- package/billing/LowBalanceBanner.js +1 -1
- package/billing/LowBalanceBanner.js.map +1 -1
- package/billing/PaymentMethodCard.d.ts +9 -1
- package/billing/PaymentMethodCard.d.ts.map +1 -1
- package/billing/PaymentMethodCard.js +2 -2
- package/billing/PaymentMethodCard.js.map +1 -1
- package/billing/PlanCard.d.ts +27 -0
- package/billing/PlanCard.d.ts.map +1 -0
- package/billing/PlanCard.js +68 -0
- package/billing/PlanCard.js.map +1 -0
- package/billing/PlanPicker.d.ts +34 -0
- package/billing/PlanPicker.d.ts.map +1 -0
- package/billing/PlanPicker.js +46 -0
- package/billing/PlanPicker.js.map +1 -0
- package/billing/PlanSection.d.ts +45 -0
- package/billing/PlanSection.d.ts.map +1 -0
- package/billing/PlanSection.js +136 -0
- package/billing/PlanSection.js.map +1 -0
- package/billing/UpgradeNotice.d.ts +47 -0
- package/billing/UpgradeNotice.d.ts.map +1 -0
- package/billing/UpgradeNotice.js +49 -0
- package/billing/UpgradeNotice.js.map +1 -0
- package/billing/credit-packs.d.ts.map +1 -1
- package/billing/credit-packs.js +4 -1
- package/billing/credit-packs.js.map +1 -1
- package/billing/index.d.ts +26 -1
- package/billing/index.d.ts.map +1 -1
- package/billing/index.js +12 -0
- package/billing/index.js.map +1 -1
- package/billing/plan-features.d.ts +60 -0
- package/billing/plan-features.d.ts.map +1 -0
- package/billing/plan-features.js +128 -0
- package/billing/plan-features.js.map +1 -0
- package/billing/plan-state.d.ts +43 -0
- package/billing/plan-state.d.ts.map +1 -0
- package/billing/plan-state.js +69 -0
- package/billing/plan-state.js.map +1 -0
- package/billing/redirect.d.ts +31 -0
- package/billing/redirect.d.ts.map +1 -0
- package/billing/redirect.js +30 -0
- package/billing/redirect.js.map +1 -0
- package/billing/useBillingAccount.d.ts +10 -1
- package/billing/useBillingAccount.d.ts.map +1 -1
- package/billing/useBillingAccount.js +2 -2
- package/billing/useBillingAccount.js.map +1 -1
- package/billing/useChangePlan.d.ts +51 -0
- package/billing/useChangePlan.d.ts.map +1 -0
- package/billing/useChangePlan.js +61 -0
- package/billing/useChangePlan.js.map +1 -0
- package/billing/useCreateBillingPortalSession.d.ts +6 -3
- package/billing/useCreateBillingPortalSession.d.ts.map +1 -1
- package/billing/useCreateBillingPortalSession.js +11 -4
- package/billing/useCreateBillingPortalSession.js.map +1 -1
- package/billing/useCreateCheckoutSession.d.ts +7 -5
- package/billing/useCreateCheckoutSession.d.ts.map +1 -1
- package/billing/useCreateCheckoutSession.js +5 -3
- package/billing/useCreateCheckoutSession.js.map +1 -1
- package/billing/useCreatePaymentMethodSetupSession.d.ts +28 -0
- package/billing/useCreatePaymentMethodSetupSession.d.ts.map +1 -0
- package/billing/useCreatePaymentMethodSetupSession.js +50 -0
- package/billing/useCreatePaymentMethodSetupSession.js.map +1 -0
- package/billing/useEntitlements.d.ts +46 -0
- package/billing/useEntitlements.d.ts.map +1 -0
- package/billing/useEntitlements.js +37 -0
- package/billing/useEntitlements.js.map +1 -0
- package/billing/usePeriodEstimate.d.ts +35 -0
- package/billing/usePeriodEstimate.d.ts.map +1 -0
- package/billing/usePeriodEstimate.js +29 -0
- package/billing/usePeriodEstimate.js.map +1 -0
- package/billing/usePlans.d.ts +38 -0
- package/billing/usePlans.d.ts.map +1 -0
- package/billing/usePlans.js +33 -0
- package/billing/usePlans.js.map +1 -0
- package/billing/useSubscription.d.ts +39 -0
- package/billing/useSubscription.d.ts.map +1 -0
- package/billing/useSubscription.js +40 -0
- package/billing/useSubscription.js.map +1 -0
- package/index.d.ts +4 -2
- package/index.d.ts.map +1 -1
- package/index.js +4 -1
- package/index.js.map +1 -1
- package/internal/features.d.ts +11 -0
- package/internal/features.d.ts.map +1 -0
- package/internal/features.js +42 -0
- package/internal/features.js.map +1 -0
- package/licenses/license-format.d.ts +5 -3
- package/licenses/license-format.d.ts.map +1 -1
- package/licenses/license-format.js +10 -19
- package/licenses/license-format.js.map +1 -1
- package/package.json +5 -5
- package/plan-catalog/PlanCatalogConsole.d.ts +22 -0
- package/plan-catalog/PlanCatalogConsole.d.ts.map +1 -0
- package/plan-catalog/PlanCatalogConsole.js +90 -0
- package/plan-catalog/PlanCatalogConsole.js.map +1 -0
- package/plan-catalog/PlanCreateForm.d.ts +17 -0
- package/plan-catalog/PlanCreateForm.d.ts.map +1 -0
- package/plan-catalog/PlanCreateForm.js +52 -0
- package/plan-catalog/PlanCreateForm.js.map +1 -0
- package/plan-catalog/PlansAccessNotice.d.ts +10 -0
- package/plan-catalog/PlansAccessNotice.d.ts.map +1 -0
- package/plan-catalog/PlansAccessNotice.js +13 -0
- package/plan-catalog/PlansAccessNotice.js.map +1 -0
- package/plan-catalog/index.d.ts +8 -0
- package/plan-catalog/index.d.ts.map +1 -0
- package/plan-catalog/index.js +10 -0
- package/plan-catalog/index.js.map +1 -0
- package/plan-catalog/plan-draft.d.ts +30 -0
- package/plan-catalog/plan-draft.d.ts.map +1 -0
- package/plan-catalog/plan-draft.js +91 -0
- package/plan-catalog/plan-draft.js.map +1 -0
- package/plan-catalog/usePlanMutations.d.ts +42 -0
- package/plan-catalog/usePlanMutations.d.ts.map +1 -0
- package/plan-catalog/usePlanMutations.js +57 -0
- package/plan-catalog/usePlanMutations.js.map +1 -0
- package/settings/TeamsSection.d.ts.map +1 -1
- package/settings/TeamsSection.js +19 -2
- package/settings/TeamsSection.js.map +1 -1
- package/settings/settings-nav.d.ts.map +1 -1
- package/settings/settings-nav.js +7 -1
- package/settings/settings-nav.js.map +1 -1
- package/settings/useSettingsNavGroups.d.ts.map +1 -1
- package/settings/useSettingsNavGroups.js +4 -1
- package/settings/useSettingsNavGroups.js.map +1 -1
- package/src/billing/AutoRechargeCard.tsx +2 -2
- package/src/billing/BillingSection.tsx +68 -11
- package/src/billing/ChangePlanDialog.tsx +221 -0
- package/src/billing/CreditPackGrid.tsx +1 -1
- package/src/billing/LowBalanceBanner.tsx +1 -1
- package/src/billing/PaymentMethodCard.tsx +28 -2
- package/src/billing/PlanCard.tsx +171 -0
- package/src/billing/PlanPicker.tsx +178 -0
- package/src/billing/PlanSection.tsx +234 -0
- package/src/billing/UpgradeNotice.tsx +95 -0
- package/src/billing/credit-packs.ts +4 -1
- package/src/billing/index.ts +26 -1
- package/src/billing/plan-features.ts +162 -0
- package/src/billing/plan-state.ts +92 -0
- package/src/billing/redirect.ts +45 -0
- package/src/billing/useBillingAccount.ts +12 -0
- package/src/billing/useChangePlan.ts +115 -0
- package/src/billing/useCreateBillingPortalSession.ts +17 -6
- package/src/billing/useCreateCheckoutSession.ts +12 -7
- package/src/billing/useCreatePaymentMethodSetupSession.ts +77 -0
- package/src/billing/useEntitlements.ts +80 -0
- package/src/billing/usePeriodEstimate.ts +59 -0
- package/src/billing/usePlans.ts +65 -0
- package/src/billing/useSubscription.ts +70 -0
- package/src/index.ts +54 -0
- package/src/internal/features.ts +49 -0
- package/src/licenses/license-format.ts +10 -19
- package/src/plan-catalog/PlanCatalogConsole.tsx +197 -0
- package/src/plan-catalog/PlanCreateForm.tsx +167 -0
- package/src/plan-catalog/PlansAccessNotice.tsx +20 -0
- package/src/plan-catalog/index.ts +14 -0
- package/src/plan-catalog/plan-draft.ts +116 -0
- package/src/plan-catalog/usePlanMutations.ts +94 -0
- package/src/settings/TeamsSection.tsx +20 -2
- package/src/settings/settings-nav.ts +7 -0
- package/src/settings/useSettingsNavGroups.ts +8 -1
- package/src/team/CreateTeamForm.tsx +12 -3
- package/src/team/TeamMembersPanel.tsx +6 -2
- package/team/CreateTeamForm.d.ts +6 -1
- package/team/CreateTeamForm.d.ts.map +1 -1
- package/team/CreateTeamForm.js +4 -2
- package/team/CreateTeamForm.js.map +1 -1
- package/team/TeamMembersPanel.d.ts.map +1 -1
- package/team/TeamMembersPanel.js +3 -1
- 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: "
|
|
41
|
+
displayName: "Scale",
|
|
39
42
|
description: "For production workloads",
|
|
40
43
|
priceCents: 20_000,
|
|
41
44
|
credits: 20_000,
|
package/src/billing/index.ts
CHANGED
|
@@ -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";
|