@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
|
@@ -4,6 +4,7 @@ import { useCallback, useId, useState } from "react";
|
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import { getUserMessage } from "@stigmer/sdk";
|
|
6
6
|
import { BillingAccountStatus } from "@stigmer/protos/ai/stigmer/billing/v1/enum_pb";
|
|
7
|
+
import { ManagementMode } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/enum_pb";
|
|
7
8
|
import { useDeploymentMode } from "../deployment-mode.js";
|
|
8
9
|
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice.js";
|
|
9
10
|
import { useOrg } from "../organization/OrgProvider.js";
|
|
@@ -16,6 +17,9 @@ import { AutoRechargeCard } from "./AutoRechargeCard.js";
|
|
|
16
17
|
import { CreditPackGrid } from "./CreditPackGrid.js";
|
|
17
18
|
import { CreditLedgerTable } from "./CreditLedgerTable.js";
|
|
18
19
|
import { LowBalanceBanner } from "./LowBalanceBanner.js";
|
|
20
|
+
import { PlanSection } from "./PlanSection.js";
|
|
21
|
+
import { billingReturnUrl, type BillingRedirect } from "./redirect.js";
|
|
22
|
+
import { useCreatePaymentMethodSetupSession } from "./useCreatePaymentMethodSetupSession.js";
|
|
19
23
|
|
|
20
24
|
/** Props for {@link BillingSection}. */
|
|
21
25
|
export interface BillingSectionProps {
|
|
@@ -29,6 +33,22 @@ export interface BillingSectionProps {
|
|
|
29
33
|
readonly checkoutSuccess?: boolean;
|
|
30
34
|
/** Callback to dismiss the checkout success banner. */
|
|
31
35
|
readonly onDismissCheckoutSuccess?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* The plan the person was choosing when they left to save a card: the
|
|
38
|
+
* `plan` query Stripe returns with after `?setup=success`. The plan
|
|
39
|
+
* section reopens that choice for an explicit confirm.
|
|
40
|
+
*/
|
|
41
|
+
readonly resumePlanId?: string;
|
|
42
|
+
/** Called once the resumed plan choice is taken up, so the host can clear its query. */
|
|
43
|
+
readonly onResumeHandled?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* The host's seam for Stripe-hosted pages (checkout, card setup and the
|
|
46
|
+
* billing portal). Absent, this window navigates to them and Stripe
|
|
47
|
+
* returns to this origin's `/settings/billing`. A desktop host opens the
|
|
48
|
+
* system browser and names the web console's billing page instead; its
|
|
49
|
+
* reads refresh when the window regains focus.
|
|
50
|
+
*/
|
|
51
|
+
readonly redirect?: BillingRedirect;
|
|
32
52
|
/** Additional CSS class names. */
|
|
33
53
|
readonly className?: string;
|
|
34
54
|
}
|
|
@@ -51,12 +71,16 @@ export interface BillingSectionProps {
|
|
|
51
71
|
export function BillingSection({
|
|
52
72
|
checkoutSuccess,
|
|
53
73
|
onDismissCheckoutSuccess,
|
|
74
|
+
resumePlanId,
|
|
75
|
+
onResumeHandled,
|
|
76
|
+
redirect,
|
|
54
77
|
className,
|
|
55
78
|
}: BillingSectionProps) {
|
|
56
79
|
const headingId = useId();
|
|
57
80
|
const { activeOrg } = useOrg();
|
|
58
81
|
const mode = useDeploymentMode();
|
|
59
82
|
const orgId = activeOrg?.metadata?.id ?? "";
|
|
83
|
+
const managed = activeOrg?.spec?.managementMode === ManagementMode.platform_managed;
|
|
60
84
|
|
|
61
85
|
return (
|
|
62
86
|
<section aria-labelledby={headingId} className={className}>
|
|
@@ -67,7 +91,7 @@ export function BillingSection({
|
|
|
67
91
|
Billing
|
|
68
92
|
</h2>
|
|
69
93
|
<p className="stg:text-muted-foreground stg:mb-4 stg:text-xs">
|
|
70
|
-
|
|
94
|
+
Your plan, credits, payment method and transaction history.
|
|
71
95
|
</p>
|
|
72
96
|
|
|
73
97
|
{mode === "local" ? (
|
|
@@ -83,8 +107,12 @@ export function BillingSection({
|
|
|
83
107
|
) : (
|
|
84
108
|
<BillingContent
|
|
85
109
|
orgId={orgId}
|
|
110
|
+
managed={managed}
|
|
86
111
|
checkoutSuccess={checkoutSuccess}
|
|
87
112
|
onDismissCheckoutSuccess={onDismissCheckoutSuccess}
|
|
113
|
+
resumePlanId={resumePlanId}
|
|
114
|
+
onResumeHandled={onResumeHandled}
|
|
115
|
+
redirect={redirect}
|
|
88
116
|
/>
|
|
89
117
|
)}
|
|
90
118
|
</section>
|
|
@@ -97,16 +125,28 @@ export function BillingSection({
|
|
|
97
125
|
|
|
98
126
|
function BillingContent({
|
|
99
127
|
orgId,
|
|
128
|
+
managed,
|
|
100
129
|
checkoutSuccess,
|
|
101
130
|
onDismissCheckoutSuccess,
|
|
131
|
+
resumePlanId,
|
|
132
|
+
onResumeHandled,
|
|
133
|
+
redirect,
|
|
102
134
|
}: {
|
|
103
135
|
orgId: string;
|
|
136
|
+
managed: boolean;
|
|
104
137
|
checkoutSuccess?: boolean;
|
|
105
138
|
onDismissCheckoutSuccess?: () => void;
|
|
139
|
+
resumePlanId?: string;
|
|
140
|
+
onResumeHandled?: () => void;
|
|
141
|
+
redirect?: BillingRedirect;
|
|
106
142
|
}) {
|
|
107
|
-
const { account, isLoading, error, refetch } = useBillingAccount(orgId
|
|
108
|
-
|
|
109
|
-
|
|
143
|
+
const { account, isLoading, error, refetch } = useBillingAccount(orgId, {
|
|
144
|
+
refetchOnWindowFocus: redirect?.openUrl !== undefined,
|
|
145
|
+
});
|
|
146
|
+
const { createSession, isSubmitting, error: checkoutError, clearError } = useCreateCheckoutSession(redirect);
|
|
147
|
+
const { openPortal, isLoading: isPortalLoading } = useCreateBillingPortalSession(redirect);
|
|
148
|
+
const setup = useCreatePaymentMethodSetupSession(redirect);
|
|
149
|
+
const returnUrl = redirect?.returnUrl;
|
|
110
150
|
const [purchasingPackId, setPurchasingPackId] = useState<string | null>(null);
|
|
111
151
|
|
|
112
152
|
const handlePurchase = useCallback(
|
|
@@ -114,8 +154,7 @@ function BillingContent({
|
|
|
114
154
|
setPurchasingPackId(packId);
|
|
115
155
|
clearError();
|
|
116
156
|
|
|
117
|
-
const
|
|
118
|
-
const billingPath = `${baseUrl}/settings/billing`;
|
|
157
|
+
const billingPath = billingReturnUrl({ returnUrl });
|
|
119
158
|
|
|
120
159
|
createSession({
|
|
121
160
|
orgId,
|
|
@@ -126,7 +165,7 @@ function BillingContent({
|
|
|
126
165
|
setPurchasingPackId(null);
|
|
127
166
|
});
|
|
128
167
|
},
|
|
129
|
-
[orgId, createSession, clearError],
|
|
168
|
+
[orgId, createSession, clearError, returnUrl],
|
|
130
169
|
);
|
|
131
170
|
|
|
132
171
|
if (isLoading) {
|
|
@@ -161,6 +200,8 @@ function BillingContent({
|
|
|
161
200
|
|
|
162
201
|
const isLowBalance =
|
|
163
202
|
balance.availableMicros < account.lowBalanceThresholdMicros;
|
|
203
|
+
const hasPaymentMethod =
|
|
204
|
+
account.defaultPaymentMethod != null && account.defaultPaymentMethod.paymentMethodId !== "";
|
|
164
205
|
|
|
165
206
|
return (
|
|
166
207
|
<div className="stg:space-y-6">
|
|
@@ -173,6 +214,16 @@ function BillingContent({
|
|
|
173
214
|
thresholdMicros={account.lowBalanceThresholdMicros}
|
|
174
215
|
/>
|
|
175
216
|
|
|
217
|
+
<PlanSection
|
|
218
|
+
orgId={orgId}
|
|
219
|
+
managed={managed}
|
|
220
|
+
hasPaymentMethod={hasPaymentMethod}
|
|
221
|
+
onRefreshAccount={refetch}
|
|
222
|
+
redirect={redirect}
|
|
223
|
+
resumePlanId={resumePlanId}
|
|
224
|
+
onResumeHandled={onResumeHandled}
|
|
225
|
+
/>
|
|
226
|
+
|
|
176
227
|
<CreditBalanceCard balance={balance} isLowBalance={isLowBalance} />
|
|
177
228
|
|
|
178
229
|
<PaymentMethodCard
|
|
@@ -180,15 +231,21 @@ function BillingContent({
|
|
|
180
231
|
accountStatus={account.status}
|
|
181
232
|
isPortalLoading={isPortalLoading}
|
|
182
233
|
onManage={() => openPortal(orgId)}
|
|
234
|
+
onAdd={() => {
|
|
235
|
+
setup.addPaymentMethod(orgId).catch(() => undefined);
|
|
236
|
+
}}
|
|
237
|
+
isAdding={setup.isSubmitting}
|
|
183
238
|
/>
|
|
239
|
+
{setup.error && (
|
|
240
|
+
<p className="stg:text-destructive stg:text-xs" role="alert">
|
|
241
|
+
{getUserMessage(setup.error)}
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
184
244
|
|
|
185
245
|
<AutoRechargeCard
|
|
186
246
|
orgId={orgId}
|
|
187
247
|
autoRecharge={account.autoRecharge}
|
|
188
|
-
hasPaymentMethod={
|
|
189
|
-
account.defaultPaymentMethod != null &&
|
|
190
|
-
account.defaultPaymentMethod.paymentMethodId !== ""
|
|
191
|
-
}
|
|
248
|
+
hasPaymentMethod={hasPaymentMethod}
|
|
192
249
|
accountStatus={account.status}
|
|
193
250
|
onSaved={refetch}
|
|
194
251
|
/>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { getErrorReason, getUserMessage } from "@stigmer/sdk";
|
|
5
|
+
import type { PlanTerms } from "@stigmer/protos/ai/stigmer/billing/plan/v1/spec_pb";
|
|
6
|
+
import { Button } from "../button/index.js";
|
|
7
|
+
import { DialogShell } from "../internal/DialogShell.js";
|
|
8
|
+
import { formatDay, formatMonthlyMinimum, sharePercent, type OfferedFeature } from "./plan-features.js";
|
|
9
|
+
import type { PlanMove } from "./plan-state.js";
|
|
10
|
+
|
|
11
|
+
/** The reason a plan change is refused for want of a saved card (billing/subscription/v1/command.proto). */
|
|
12
|
+
export const PAYMENT_METHOD_REQUIRED = "PAYMENT_METHOD_REQUIRED";
|
|
13
|
+
|
|
14
|
+
/** Props for {@link ChangePlanDialog}. */
|
|
15
|
+
export interface ChangePlanDialogProps {
|
|
16
|
+
/** The move to confirm; the dialog is open while it is set. */
|
|
17
|
+
readonly move: PlanMove | null;
|
|
18
|
+
/** The display name of a plan by id, for the plan being left. */
|
|
19
|
+
readonly planName: (planId: string) => string;
|
|
20
|
+
/**
|
|
21
|
+
* For a switch, what the plan being left offers that the new one does
|
|
22
|
+
* not (`featuresLost`). Empty, as for an upgrade, the dialog says
|
|
23
|
+
* nothing is lost.
|
|
24
|
+
*/
|
|
25
|
+
readonly losing?: readonly OfferedFeature[];
|
|
26
|
+
/**
|
|
27
|
+
* Whether the organization has a saved card. Every move but a cancel
|
|
28
|
+
* needs one, because every period is collected from it.
|
|
29
|
+
*/
|
|
30
|
+
readonly hasPaymentMethod: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The card was just saved on Stripe's page and is on its way to the
|
|
33
|
+
* account: say so instead of asking for one again.
|
|
34
|
+
*/
|
|
35
|
+
readonly awaitingPaymentMethod?: boolean;
|
|
36
|
+
/** `true` while the move, or the card page, is in flight. */
|
|
37
|
+
readonly isSubmitting: boolean;
|
|
38
|
+
/** Why the last attempt failed, or `null`. */
|
|
39
|
+
readonly error: Error | null;
|
|
40
|
+
/** Make the move. */
|
|
41
|
+
readonly onConfirm: () => void;
|
|
42
|
+
/** Leave to save a card, returning to this choice. */
|
|
43
|
+
readonly onAddPaymentMethod: () => void;
|
|
44
|
+
/** Close without moving. */
|
|
45
|
+
readonly onClose: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The confirm step for a plan change. It states exactly what the move does
|
|
50
|
+
* and when money moves: a subscription starts now and every period is
|
|
51
|
+
* collected when it closes; a switch closes this period early, billed for
|
|
52
|
+
* the time it ran; a cancel keeps the plan to its period's end and deletes
|
|
53
|
+
* nothing. Without a saved card it offers the card page first.
|
|
54
|
+
*/
|
|
55
|
+
export function ChangePlanDialog({
|
|
56
|
+
move,
|
|
57
|
+
planName,
|
|
58
|
+
losing = [],
|
|
59
|
+
hasPaymentMethod,
|
|
60
|
+
awaitingPaymentMethod,
|
|
61
|
+
isSubmitting,
|
|
62
|
+
error,
|
|
63
|
+
onConfirm,
|
|
64
|
+
onAddPaymentMethod,
|
|
65
|
+
onClose,
|
|
66
|
+
}: ChangePlanDialogProps) {
|
|
67
|
+
const headingId = useId();
|
|
68
|
+
const needsCard =
|
|
69
|
+
move !== null &&
|
|
70
|
+
move.kind !== "cancel" &&
|
|
71
|
+
(!hasPaymentMethod || getErrorReason(error)?.reason === PAYMENT_METHOD_REQUIRED);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<DialogShell
|
|
75
|
+
open={move !== null}
|
|
76
|
+
onOpenChange={(open) => {
|
|
77
|
+
if (!open) onClose();
|
|
78
|
+
}}
|
|
79
|
+
width="md"
|
|
80
|
+
aria-labelledby={headingId}
|
|
81
|
+
>
|
|
82
|
+
{move && (
|
|
83
|
+
<div className="stg:space-y-3 stg:p-5">
|
|
84
|
+
<h2 id={headingId} className="stg:text-sm stg:font-semibold stg:text-foreground">
|
|
85
|
+
{title(move)}
|
|
86
|
+
</h2>
|
|
87
|
+
<div className="stg:space-y-2 stg:text-xs stg:text-muted-foreground">{body(move, planName, losing)}</div>
|
|
88
|
+
|
|
89
|
+
{needsCard && (
|
|
90
|
+
<p className="stg:rounded-md stg:bg-muted-subtle stg:px-3 stg:py-2 stg:text-xs stg:text-foreground" role="status">
|
|
91
|
+
{awaitingPaymentMethod
|
|
92
|
+
? "Your card is being saved. This takes a few seconds; the choice is kept."
|
|
93
|
+
: "A saved card is needed first: every period is collected from it. You come back here after saving it."}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
{error && getErrorReason(error)?.reason !== PAYMENT_METHOD_REQUIRED && (
|
|
97
|
+
<p className="stg:text-xs stg:text-destructive" role="alert">
|
|
98
|
+
{getUserMessage(error)}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<div className="stg:flex stg:justify-end stg:gap-2 stg:pt-1">
|
|
103
|
+
<Button variant="ghost" size="sm" onClick={onClose} disabled={isSubmitting}>
|
|
104
|
+
Not now
|
|
105
|
+
</Button>
|
|
106
|
+
{needsCard ? (
|
|
107
|
+
<Button size="sm" onClick={onAddPaymentMethod} disabled={isSubmitting || awaitingPaymentMethod}>
|
|
108
|
+
Add a payment method
|
|
109
|
+
</Button>
|
|
110
|
+
) : (
|
|
111
|
+
<Button
|
|
112
|
+
variant={move.kind === "cancel" ? "destructive" : "primary"}
|
|
113
|
+
size="sm"
|
|
114
|
+
onClick={onConfirm}
|
|
115
|
+
disabled={isSubmitting}
|
|
116
|
+
>
|
|
117
|
+
{confirmLabel(move)}
|
|
118
|
+
</Button>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
</DialogShell>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function title(move: PlanMove): string {
|
|
128
|
+
switch (move.kind) {
|
|
129
|
+
case "subscribe":
|
|
130
|
+
return `Subscribe to ${move.to.metadata?.name ?? "this plan"}`;
|
|
131
|
+
case "switch":
|
|
132
|
+
return `Switch to ${move.to.metadata?.name ?? "this plan"}`;
|
|
133
|
+
case "resume":
|
|
134
|
+
return `Keep ${move.to.metadata?.name ?? "this plan"}`;
|
|
135
|
+
case "cancel":
|
|
136
|
+
return "Move to Free";
|
|
137
|
+
default: {
|
|
138
|
+
const exhaustive: never = move;
|
|
139
|
+
return String(exhaustive);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function body(move: PlanMove, planName: (planId: string) => string, losing: readonly OfferedFeature[]) {
|
|
145
|
+
switch (move.kind) {
|
|
146
|
+
case "subscribe":
|
|
147
|
+
return (
|
|
148
|
+
<>
|
|
149
|
+
<p>
|
|
150
|
+
{move.to.metadata?.name} starts now. Each monthly period is billed when it ends:{" "}
|
|
151
|
+
{periodCharge(move.to.spec?.terms)}
|
|
152
|
+
</p>
|
|
153
|
+
<p>Nothing is charged today.</p>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
case "switch":
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
<p>
|
|
160
|
+
The switch takes effect now. This period closes early and is billed for the time it ran on{" "}
|
|
161
|
+
{planName(move.fromPlanId)}; a new monthly period opens on {move.to.metadata?.name} at{" "}
|
|
162
|
+
{formatMonthlyMinimum(move.to.spec?.terms)}.
|
|
163
|
+
</p>
|
|
164
|
+
{losing.length > 0 && (
|
|
165
|
+
<p>
|
|
166
|
+
{move.to.metadata?.name} does not include {listFeatures(losing)}. Everything you built keeps working; only
|
|
167
|
+
creating more is refused.
|
|
168
|
+
</p>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
case "resume":
|
|
173
|
+
return <p>The cancellation is withdrawn, and {move.to.metadata?.name} runs on into its next period.</p>;
|
|
174
|
+
case "cancel":
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<p>
|
|
178
|
+
{planName(move.fromPlanId)} stays in force until {formatDay(move.endsAt)}, then this organization is on
|
|
179
|
+
Free. The period is billed as usual when it closes.
|
|
180
|
+
</p>
|
|
181
|
+
<p>Nothing you built is deleted. Creating what Free does not include is refused until you subscribe again.</p>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
default: {
|
|
185
|
+
const exhaustive: never = move;
|
|
186
|
+
return String(exhaustive);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function confirmLabel(move: PlanMove): string {
|
|
192
|
+
switch (move.kind) {
|
|
193
|
+
case "subscribe":
|
|
194
|
+
return "Subscribe";
|
|
195
|
+
case "switch":
|
|
196
|
+
return "Switch now";
|
|
197
|
+
case "resume":
|
|
198
|
+
return "Keep plan";
|
|
199
|
+
case "cancel":
|
|
200
|
+
return "Cancel plan";
|
|
201
|
+
default: {
|
|
202
|
+
const exhaustive: never = move;
|
|
203
|
+
return String(exhaustive);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** "Teams", "Teams and Managed organizations", "A, B and C". */
|
|
209
|
+
function listFeatures(features: readonly OfferedFeature[]): string {
|
|
210
|
+
const labels = features.map((feature) => feature.label);
|
|
211
|
+
return labels.length <= 1 ? (labels[0] ?? "") : `${labels.slice(0, -1).join(", ")} and ${labels[labels.length - 1]}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** What a period of the plan is billed at its end, in the customer's words. */
|
|
215
|
+
function periodCharge(terms: PlanTerms | undefined): string {
|
|
216
|
+
const minimum = formatMonthlyMinimum(terms);
|
|
217
|
+
const percent = sharePercent(terms);
|
|
218
|
+
return percent === ""
|
|
219
|
+
? `${minimum}.`
|
|
220
|
+
: `${minimum}, less the ${percent} commission already paid on that period's usage. Usage itself stays paid from credits.`;
|
|
221
|
+
}
|
|
@@ -24,7 +24,7 @@ export interface CreditPackGridProps {
|
|
|
24
24
|
/**
|
|
25
25
|
* Grid of credit pack cards with purchase buttons.
|
|
26
26
|
*
|
|
27
|
-
* Displays the 3 self-serve credit packs (Starter, Growth,
|
|
27
|
+
* Displays the 3 self-serve credit packs (Starter, Growth, Scale)
|
|
28
28
|
* in a responsive grid. Each card shows the pack name, price,
|
|
29
29
|
* credit count, and a buy button. Buttons are disabled when the
|
|
30
30
|
* account is suspended/closed or a purchase is in progress.
|
|
@@ -58,7 +58,7 @@ export function LowBalanceBanner({
|
|
|
58
58
|
</p>
|
|
59
59
|
<p className="stg:mt-0.5 stg:opacity-80">
|
|
60
60
|
{isZeroOrNegative
|
|
61
|
-
? "Your credit balance is zero.
|
|
61
|
+
? "Your credit balance is zero. Credits pay for agent usage on every plan; purchase credits to keep running agent executions."
|
|
62
62
|
: `Your balance (${formatCreditBalance(availableMicros)}) is below the warning threshold. Consider purchasing additional credits.`}
|
|
63
63
|
</p>
|
|
64
64
|
</div>
|
|
@@ -14,6 +14,14 @@ export interface PaymentMethodCardProps {
|
|
|
14
14
|
readonly isPortalLoading?: boolean;
|
|
15
15
|
/** Called when the user clicks "Manage payment methods". */
|
|
16
16
|
readonly onManage: () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Called when the user clicks "Add payment method" with none on file:
|
|
19
|
+
* the host opens Stripe's page to save a card without a charge. Absent,
|
|
20
|
+
* the card has no add action.
|
|
21
|
+
*/
|
|
22
|
+
readonly onAdd?: () => void;
|
|
23
|
+
/** `true` while the card page is being opened. */
|
|
24
|
+
readonly isAdding?: boolean;
|
|
17
25
|
/** Additional CSS class names. */
|
|
18
26
|
readonly className?: string;
|
|
19
27
|
}
|
|
@@ -61,6 +69,8 @@ export function PaymentMethodCard({
|
|
|
61
69
|
accountStatus,
|
|
62
70
|
isPortalLoading,
|
|
63
71
|
onManage,
|
|
72
|
+
onAdd,
|
|
73
|
+
isAdding,
|
|
64
74
|
className,
|
|
65
75
|
}: PaymentMethodCardProps) {
|
|
66
76
|
const isAccountActive =
|
|
@@ -113,9 +123,25 @@ export function PaymentMethodCard({
|
|
|
113
123
|
) : (
|
|
114
124
|
<div className="stg:mt-2">
|
|
115
125
|
<p className="stg:text-xs stg:text-muted-foreground">
|
|
116
|
-
No payment method on file. A card
|
|
117
|
-
|
|
126
|
+
No payment method on file. A saved card is needed to subscribe to
|
|
127
|
+
a plan or turn on auto-recharge; buying a credit pack also saves
|
|
128
|
+
one.
|
|
118
129
|
</p>
|
|
130
|
+
{onAdd && (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
disabled={!isAccountActive || isAdding}
|
|
134
|
+
onClick={onAdd}
|
|
135
|
+
className={cn(
|
|
136
|
+
"stg:mt-2 stg:rounded-md stg:px-2.5 stg:py-1 stg:text-xs stg:font-medium stg:transition-colors",
|
|
137
|
+
"stg:text-primary stg:hover:bg-accent",
|
|
138
|
+
"stg:disabled:pointer-events-none stg:disabled:opacity-50",
|
|
139
|
+
)}
|
|
140
|
+
aria-busy={isAdding}
|
|
141
|
+
>
|
|
142
|
+
{isAdding ? "Opening\u2026" : "Add payment method"}
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
119
145
|
</div>
|
|
120
146
|
)}
|
|
121
147
|
</div>
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@stigmer/theme";
|
|
4
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
5
|
+
import {
|
|
6
|
+
PeriodEstimateLineKind,
|
|
7
|
+
type PeriodEstimate,
|
|
8
|
+
type PeriodEstimateLine,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/billing/subscription/v1/io_pb";
|
|
10
|
+
import { StatusBadge } from "../resource-workbench/components/StatusBadge.js";
|
|
11
|
+
import { formatCreditBalance, formatLedgerAmount } from "./format.js";
|
|
12
|
+
import { formatDay, formatPeriodDay } from "./plan-features.js";
|
|
13
|
+
import type { PlanStanding } from "./plan-state.js";
|
|
14
|
+
|
|
15
|
+
/** Props for {@link PlanCard}. */
|
|
16
|
+
export interface PlanCardProps {
|
|
17
|
+
/** The plan's display name ("Free" when there is none). */
|
|
18
|
+
readonly planName: string;
|
|
19
|
+
/** Where the organization stands on it. */
|
|
20
|
+
readonly standing: PlanStanding;
|
|
21
|
+
/** The current period's estimated invoice, when a plan is live. */
|
|
22
|
+
readonly estimate?: PeriodEstimate | null;
|
|
23
|
+
/** Why the estimate could not be read, shown in its place. */
|
|
24
|
+
readonly estimateError?: Error | null;
|
|
25
|
+
/**
|
|
26
|
+
* The organization is platform-managed: it is on its integrator's plan
|
|
27
|
+
* and has no plan, period or invoice of its own.
|
|
28
|
+
*/
|
|
29
|
+
readonly managed?: boolean;
|
|
30
|
+
/** Additional CSS class names. */
|
|
31
|
+
readonly className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The organization's plan: its name, its standing (active, payment
|
|
36
|
+
* failed, or ending on a date), the period it is in, and the period's
|
|
37
|
+
* estimated invoice line by line, the way the invoice will read it.
|
|
38
|
+
*/
|
|
39
|
+
export function PlanCard({ planName, standing, estimate, estimateError, managed, className }: PlanCardProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"stg:rounded-lg stg:border stg:border-border stg:bg-card stg:px-4 stg:py-4",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<div className="stg:flex stg:items-center stg:justify-between stg:gap-3">
|
|
48
|
+
<h3 className="stg:text-xs stg:font-semibold stg:text-foreground">Plan</h3>
|
|
49
|
+
{!managed && <StandingBadge standing={standing} />}
|
|
50
|
+
</div>
|
|
51
|
+
<p className="stg:mt-2 stg:text-sm stg:font-medium stg:text-foreground">
|
|
52
|
+
{managed ? "Your integrator's plan" : planName}
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
{managed ? (
|
|
56
|
+
<p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">
|
|
57
|
+
This organization is managed by its integrator and runs on the integrator's plan. It has no plan or
|
|
58
|
+
invoice of its own.
|
|
59
|
+
</p>
|
|
60
|
+
) : (
|
|
61
|
+
<StandingDetail standing={standing} />
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{!managed && estimate && <EstimateLines planName={planName} estimate={estimate} />}
|
|
65
|
+
{!managed && !estimate && estimateError && (
|
|
66
|
+
<p className="stg:mt-3 stg:text-xs stg:text-destructive" role="alert">
|
|
67
|
+
{getUserMessage(estimateError)}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function StandingBadge({ standing }: { standing: PlanStanding }) {
|
|
75
|
+
switch (standing.kind) {
|
|
76
|
+
case "active":
|
|
77
|
+
return <StatusBadge phase="ready" label="Active" />;
|
|
78
|
+
case "past-due":
|
|
79
|
+
return <StatusBadge phase="failed" label="Payment failed" />;
|
|
80
|
+
case "ending":
|
|
81
|
+
return <StatusBadge phase="degraded" label={`Ends ${formatDay(standing.endsAt)}`} />;
|
|
82
|
+
case "free":
|
|
83
|
+
return <StatusBadge phase="disabled" label="Free" />;
|
|
84
|
+
default: {
|
|
85
|
+
const exhaustive: never = standing;
|
|
86
|
+
return exhaustive;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function StandingDetail({ standing }: { standing: PlanStanding }) {
|
|
92
|
+
switch (standing.kind) {
|
|
93
|
+
case "free":
|
|
94
|
+
return (
|
|
95
|
+
<p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">
|
|
96
|
+
Channels, sharing and unlimited members. Usage is paid from credits.
|
|
97
|
+
</p>
|
|
98
|
+
);
|
|
99
|
+
case "past-due":
|
|
100
|
+
return (
|
|
101
|
+
<p className="stg:mt-1 stg:text-xs stg:text-destructive" role="status">
|
|
102
|
+
The last period's payment failed. Stripe retries it; update the card under Payment Method. The plan stays
|
|
103
|
+
in force meanwhile.
|
|
104
|
+
</p>
|
|
105
|
+
);
|
|
106
|
+
case "ending":
|
|
107
|
+
return (
|
|
108
|
+
<p className="stg:mt-1 stg:text-xs stg:text-muted-foreground">
|
|
109
|
+
Canceled. The plan stays in force until {formatDay(standing.endsAt)}, then this
|
|
110
|
+
organization is on Free. Nothing you built is deleted.
|
|
111
|
+
</p>
|
|
112
|
+
);
|
|
113
|
+
case "active":
|
|
114
|
+
return null;
|
|
115
|
+
default: {
|
|
116
|
+
const exhaustive: never = standing;
|
|
117
|
+
return exhaustive;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function EstimateLines({ planName, estimate }: { planName: string; estimate: PeriodEstimate }) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="stg:mt-4 stg:border-t stg:border-border stg:pt-3">
|
|
125
|
+
<div className="stg:flex stg:items-baseline stg:justify-between stg:gap-3">
|
|
126
|
+
<p className="stg:text-xs stg:font-medium stg:text-foreground">
|
|
127
|
+
Estimated invoice on {formatPeriodDay(estimate.periodEnd)}
|
|
128
|
+
</p>
|
|
129
|
+
<p className="stg:text-xs stg:text-muted-foreground">
|
|
130
|
+
Period {formatPeriodDay(estimate.periodStart)} – {formatPeriodDay(estimate.periodEnd)}
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<dl className="stg:mt-2 stg:space-y-1">
|
|
134
|
+
{estimate.lines.map((line) => (
|
|
135
|
+
<div key={line.kind} className="stg:flex stg:justify-between stg:gap-3 stg:text-xs">
|
|
136
|
+
<dt className="stg:text-muted-foreground">{lineLabel(line, planName, estimate)}</dt>
|
|
137
|
+
<dd className="stg:tabular-nums stg:text-foreground">
|
|
138
|
+
{line.amountMicros < BigInt(0) ? formatLedgerAmount(line.amountMicros) : formatCreditBalance(line.amountMicros)}
|
|
139
|
+
</dd>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
<div className="stg:flex stg:justify-between stg:gap-3 stg:border-t stg:border-border stg:pt-1 stg:text-xs stg:font-medium">
|
|
143
|
+
<dt className="stg:text-foreground">Total so far</dt>
|
|
144
|
+
<dd className="stg:tabular-nums stg:text-foreground">{formatCreditBalance(estimate.totalMicros)}</dd>
|
|
145
|
+
</div>
|
|
146
|
+
</dl>
|
|
147
|
+
<p className="stg:mt-2 stg:text-xs stg:text-muted-foreground">
|
|
148
|
+
From usage so far. Usage itself is paid from credits; the commission already paid on it counts toward the
|
|
149
|
+
minimum, which covers the whole period.
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function lineLabel(line: PeriodEstimateLine, planName: string, estimate: PeriodEstimate): string {
|
|
156
|
+
switch (line.kind) {
|
|
157
|
+
case PeriodEstimateLineKind.plan:
|
|
158
|
+
return `${planName} plan`;
|
|
159
|
+
case PeriodEstimateLineKind.commission_credit:
|
|
160
|
+
return "Commission already paid on tokens";
|
|
161
|
+
case PeriodEstimateLineKind.managed_organizations:
|
|
162
|
+
return `Managed organizations beyond those included (${estimate.managedOrganizationCount} this period)`;
|
|
163
|
+
case PeriodEstimateLineKind.period_estimate_line_kind_unspecified:
|
|
164
|
+
return "Other";
|
|
165
|
+
default: {
|
|
166
|
+
const exhaustive: never = line.kind;
|
|
167
|
+
return String(exhaustive);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|