@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,162 @@
|
|
|
1
|
+
// What a plan offers, as the console shows it: which plans are offered and
|
|
2
|
+
// in what order, what each includes, and the formatting of its terms and
|
|
3
|
+
// periods.
|
|
4
|
+
//
|
|
5
|
+
// A plan's features render from its entitlements through the shared
|
|
6
|
+
// vocabulary (../internal/features.ts), never from the plan's stored
|
|
7
|
+
// description: a plan row is immutable once seeded, and the description is
|
|
8
|
+
// free text that can promise a feature before it is built. The console
|
|
9
|
+
// shows only what an organization can use today.
|
|
10
|
+
|
|
11
|
+
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
|
12
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
13
|
+
import type { Plan } from "@stigmer/protos/ai/stigmer/billing/plan/v1/api_pb";
|
|
14
|
+
import { PlanInstrument, type PlanTerms } from "@stigmer/protos/ai/stigmer/billing/plan/v1/spec_pb";
|
|
15
|
+
import { PlanLifecycle } from "@stigmer/protos/ai/stigmer/billing/plan/v1/status_pb";
|
|
16
|
+
import { Feature, type Entitlements } from "@stigmer/protos/ai/stigmer/platform/v1/entitlement_pb";
|
|
17
|
+
import { FEATURE_COPY, isNamedFeature, type NamedFeature } from "../internal/features.js";
|
|
18
|
+
import { formatCreditBalance } from "./format.js";
|
|
19
|
+
|
|
20
|
+
// BigInt(0), not a literal: the docs site compiles this source for a
|
|
21
|
+
// target below ES2020, which has no BigInt literal syntax.
|
|
22
|
+
const ZERO = BigInt(0);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Features a plan may list ahead of the gate that serves them, which
|
|
26
|
+
* Stigmer Cloud does not offer yet. The console leaves them out of every
|
|
27
|
+
* comparison. Bring-your-own provider keys leaves this set when the proxy
|
|
28
|
+
* uses an organization's own key.
|
|
29
|
+
*/
|
|
30
|
+
export const NOT_YET_OFFERED: ReadonlySet<Feature> = new Set([Feature.byo_provider_keys]);
|
|
31
|
+
|
|
32
|
+
/** One feature a plan offers today, with its words. */
|
|
33
|
+
export interface OfferedFeature {
|
|
34
|
+
readonly feature: NamedFeature;
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly description: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The features `entitlements` grant that Cloud offers today, in contract order. */
|
|
40
|
+
export function offeredFeatures(entitlements: Entitlements | undefined): readonly OfferedFeature[] {
|
|
41
|
+
const listed = entitlements?.features ?? [];
|
|
42
|
+
return [...new Set(listed)]
|
|
43
|
+
.filter((feature): feature is NamedFeature => isNamedFeature(feature) && !NOT_YET_OFFERED.has(feature))
|
|
44
|
+
.sort((a, b) => a - b)
|
|
45
|
+
.map((feature) => ({ feature, ...FEATURE_COPY[feature] }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The plans an organization can subscribe to, cheapest monthly minimum
|
|
50
|
+
* first, so a comparison reads upward from Free; equal minimums keep the
|
|
51
|
+
* catalog's order. Retired rows and prepaid instruments are left out.
|
|
52
|
+
*/
|
|
53
|
+
export function buyablePlans(plans: readonly Plan[]): readonly Plan[] {
|
|
54
|
+
return plans
|
|
55
|
+
.filter(
|
|
56
|
+
(plan) =>
|
|
57
|
+
plan.spec?.instrument === PlanInstrument.subscription && plan.status?.lifecycle === PlanLifecycle.active,
|
|
58
|
+
)
|
|
59
|
+
.map((plan, index) => ({ plan, index }))
|
|
60
|
+
.sort((a, b) => compareMinimums(a.plan, b.plan) || a.index - b.index)
|
|
61
|
+
.map(({ plan }) => plan);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The cheapest plan that can be bought and includes `feature`, if any. */
|
|
65
|
+
export function lowestPlanWith(plans: readonly Plan[], feature: Feature): Plan | undefined {
|
|
66
|
+
return buyablePlans(plans).find((plan) =>
|
|
67
|
+
offeredFeatures(plan.spec?.entitlements).some((offered) => offered.feature === feature),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What `from` offers that `to` does not: what a switch stops the organization creating. */
|
|
72
|
+
export function featuresLost(from: Plan | undefined, to: Plan): readonly OfferedFeature[] {
|
|
73
|
+
const kept = new Set(offeredFeatures(to.spec?.entitlements).map((offered) => offered.feature));
|
|
74
|
+
return offeredFeatures(from?.spec?.entitlements).filter((offered) => !kept.has(offered.feature));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compareMinimums(a: Plan, b: Plan): number {
|
|
78
|
+
const left = a.spec?.terms?.monthlyMinimumMicros ?? ZERO;
|
|
79
|
+
const right = b.spec?.terms?.monthlyMinimumMicros ?? ZERO;
|
|
80
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A plan's monthly minimum, e.g. "$99.00/month". */
|
|
84
|
+
export function formatMonthlyMinimum(terms: PlanTerms | undefined): string {
|
|
85
|
+
return `${formatCreditBalance(terms?.monthlyMinimumMicros ?? ZERO)}/month`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A plan's usage share as its terms state it, for operators who read terms,
|
|
90
|
+
* e.g. "or 10% of provider cost, whichever is greater; the commission
|
|
91
|
+
* already paid on tokens counts toward it". Empty when the plan has no
|
|
92
|
+
* usage share.
|
|
93
|
+
*/
|
|
94
|
+
export function formatUsageShare(terms: PlanTerms | undefined): string {
|
|
95
|
+
const percent = sharePercent(terms);
|
|
96
|
+
return percent === ""
|
|
97
|
+
? ""
|
|
98
|
+
: `or ${percent} of provider cost, whichever is greater; the commission already paid on tokens counts toward it`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The same share as a customer reads it: usage stays paid from credits,
|
|
103
|
+
* and the commission on it counts toward the minimum up to the spend at
|
|
104
|
+
* which the plan costs nothing extra, e.g. "Usage is still paid from
|
|
105
|
+
* credits. The 10% commission on it counts toward the minimum, so from
|
|
106
|
+
* $990.00 a month in provider costs the plan costs nothing extra." Empty
|
|
107
|
+
* when the plan has no usage share.
|
|
108
|
+
*/
|
|
109
|
+
export function explainUsageShare(terms: PlanTerms | undefined): string {
|
|
110
|
+
const percent = sharePercent(terms);
|
|
111
|
+
if (percent === "") {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
const basisPoints = BigInt(terms?.usageShareBasisPoints ?? 0);
|
|
115
|
+
// The provider cost whose share equals the minimum: past it, the share is the plan's cost and the commission pays it.
|
|
116
|
+
const breakEven = ((terms?.monthlyMinimumMicros ?? ZERO) * BigInt(10_000)) / basisPoints;
|
|
117
|
+
return (
|
|
118
|
+
`Usage is still paid from credits. The ${percent} commission on it counts toward the minimum, ` +
|
|
119
|
+
`so from ${formatCreditBalance(breakEven)} a month in provider costs the plan costs nothing extra.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The usage share as a percentage, e.g. "10%" or "12.50%"; empty when there is none. */
|
|
124
|
+
export function sharePercent(terms: PlanTerms | undefined): string {
|
|
125
|
+
const basisPoints = terms?.usageShareBasisPoints ?? 0;
|
|
126
|
+
if (basisPoints === 0) {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
return `${basisPoints % 100 === 0 ? `${basisPoints / 100}` : (basisPoints / 100).toFixed(2)}%`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The managed organizations a plan includes and what each beyond costs, or empty. */
|
|
133
|
+
export function formatManagedOrganizations(
|
|
134
|
+
entitlements: Entitlements | undefined,
|
|
135
|
+
terms: PlanTerms | undefined,
|
|
136
|
+
): string {
|
|
137
|
+
if (!(entitlements?.features ?? []).includes(Feature.managed_organizations)) {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
const included = entitlements?.limits?.includedManagedOrganizations;
|
|
141
|
+
const fee = terms?.perExtraOrganizationMicros ?? ZERO;
|
|
142
|
+
if (included === undefined) {
|
|
143
|
+
return "Unlimited managed organizations";
|
|
144
|
+
}
|
|
145
|
+
const beyond = fee > ZERO ? `, then ${formatCreditBalance(fee)}/month each` : "";
|
|
146
|
+
return `${included} managed organization${included === 1 ? "" : "s"} included${beyond}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DAY = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A period boundary as a UTC calendar day, e.g. "Feb 28, 2027". Periods
|
|
153
|
+
* run anniversary to anniversary in UTC, the rule the invoice follows.
|
|
154
|
+
*/
|
|
155
|
+
export function formatPeriodDay(timestamp: Timestamp | undefined): string {
|
|
156
|
+
return timestamp === undefined ? "" : formatDay(timestampDate(timestamp));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** An instant as its UTC calendar day, the way {@link formatPeriodDay} shows a period boundary. */
|
|
160
|
+
export function formatDay(date: Date): string {
|
|
161
|
+
return DAY.format(date);
|
|
162
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where an organization stands on its plan, and what choosing a plan
|
|
2
|
+
// would do, decided the way the server decides it (the subscription
|
|
3
|
+
// engine's moves), so the confirm step states the move the server makes.
|
|
4
|
+
//
|
|
5
|
+
// subscribe no live plan: a monthly period opens now on the plan
|
|
6
|
+
// switch a live plan, another chosen: this period closes early,
|
|
7
|
+
// billed for the time it ran, and a new one opens now
|
|
8
|
+
// resume the plan being canceled, chosen again: the cancel is
|
|
9
|
+
// withdrawn and the period runs on
|
|
10
|
+
// cancel Free chosen on a live plan: the plan runs to its period's
|
|
11
|
+
// end, then the organization is on Free
|
|
12
|
+
//
|
|
13
|
+
// Choosing the plan already in force, or Free while on Free or already
|
|
14
|
+
// canceling, is no move at all.
|
|
15
|
+
|
|
16
|
+
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
17
|
+
import type { Plan } from "@stigmer/protos/ai/stigmer/billing/plan/v1/api_pb";
|
|
18
|
+
import type { Subscription } from "@stigmer/protos/ai/stigmer/billing/subscription/v1/api_pb";
|
|
19
|
+
import { SubscriptionState } from "@stigmer/protos/ai/stigmer/billing/subscription/v1/status_pb";
|
|
20
|
+
|
|
21
|
+
/** An organization's standing on its plan. */
|
|
22
|
+
export type PlanStanding =
|
|
23
|
+
| { readonly kind: "free" }
|
|
24
|
+
| { readonly kind: "active"; readonly planId: string }
|
|
25
|
+
| { readonly kind: "past-due"; readonly planId: string }
|
|
26
|
+
| { readonly kind: "ending"; readonly planId: string; readonly endsAt: Date };
|
|
27
|
+
|
|
28
|
+
/** What choosing a plan (or Free) would do. */
|
|
29
|
+
export type PlanMove =
|
|
30
|
+
| { readonly kind: "subscribe"; readonly to: Plan }
|
|
31
|
+
| { readonly kind: "switch"; readonly fromPlanId: string; readonly to: Plan }
|
|
32
|
+
| { readonly kind: "resume"; readonly to: Plan }
|
|
33
|
+
| { readonly kind: "cancel"; readonly fromPlanId: string; readonly endsAt: Date };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The standing a stored subscription gives at `now`: the engine's live
|
|
37
|
+
* rule (active, past due, or canceled but not yet at its period's end),
|
|
38
|
+
* else Free. `null` (never subscribed) is Free.
|
|
39
|
+
*/
|
|
40
|
+
export function planStanding(subscription: Subscription | null, now: Date): PlanStanding {
|
|
41
|
+
const status = subscription?.status;
|
|
42
|
+
const planId = subscription?.spec?.planId ?? "";
|
|
43
|
+
switch (status?.state) {
|
|
44
|
+
case SubscriptionState.active:
|
|
45
|
+
return { kind: "active", planId };
|
|
46
|
+
case SubscriptionState.past_due:
|
|
47
|
+
return { kind: "past-due", planId };
|
|
48
|
+
case SubscriptionState.canceled: {
|
|
49
|
+
const end = status.currentPeriodEnd === undefined ? undefined : timestampDate(status.currentPeriodEnd);
|
|
50
|
+
return end !== undefined && now.getTime() < end.getTime()
|
|
51
|
+
? { kind: "ending", planId, endsAt: end }
|
|
52
|
+
: { kind: "free" };
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
return { kind: "free" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The move choosing `target` (a plan, or `"free"`) makes from `standing`, or `null` for none. */
|
|
60
|
+
export function planMove(
|
|
61
|
+
standing: PlanStanding,
|
|
62
|
+
target: Plan | "free",
|
|
63
|
+
periodEnd: Date | undefined,
|
|
64
|
+
): PlanMove | null {
|
|
65
|
+
if (target === "free") {
|
|
66
|
+
if (standing.kind === "active" || standing.kind === "past-due") {
|
|
67
|
+
return periodEnd === undefined ? null : { kind: "cancel", fromPlanId: standing.planId, endsAt: periodEnd };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const targetId = target.metadata?.id ?? "";
|
|
72
|
+
switch (standing.kind) {
|
|
73
|
+
case "free":
|
|
74
|
+
return { kind: "subscribe", to: target };
|
|
75
|
+
case "ending":
|
|
76
|
+
return targetId === standing.planId
|
|
77
|
+
? { kind: "resume", to: target }
|
|
78
|
+
: { kind: "switch", fromPlanId: standing.planId, to: target };
|
|
79
|
+
case "active":
|
|
80
|
+
case "past-due":
|
|
81
|
+
return targetId === standing.planId ? null : { kind: "switch", fromPlanId: standing.planId, to: target };
|
|
82
|
+
default: {
|
|
83
|
+
const exhaustive: never = standing;
|
|
84
|
+
throw new Error(`unknown plan standing ${JSON.stringify(exhaustive)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The plan id in force for `standing`, or empty on Free. */
|
|
90
|
+
export function standingPlanId(standing: PlanStanding): string {
|
|
91
|
+
return standing.kind === "free" ? "" : standing.planId;
|
|
92
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a billing flow leaves the app for a Stripe-hosted page, and where
|
|
3
|
+
* Stripe sends the person back.
|
|
4
|
+
*
|
|
5
|
+
* By default the console navigates its own window to Stripe and Stripe
|
|
6
|
+
* returns to the host's billing page on the same origin: right for a web
|
|
7
|
+
* console. A host whose window cannot host a third-party page, such as a
|
|
8
|
+
* desktop webview whose origin is not a web address Stripe can return to,
|
|
9
|
+
* passes a {@link BillingRedirect}: `openUrl` opens the page elsewhere
|
|
10
|
+
* (the system browser), and `returnUrl` names a real web page for Stripe
|
|
11
|
+
* to return to. The person finishes there; the host's reads refresh when
|
|
12
|
+
* its window regains focus, because the state lives on the server.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The host's seam for leaving the app to a Stripe-hosted page. */
|
|
16
|
+
export interface BillingRedirect {
|
|
17
|
+
/**
|
|
18
|
+
* Opens a Stripe-hosted page. Absent, the current window navigates to
|
|
19
|
+
* it. Desktop hosts open the system browser.
|
|
20
|
+
*/
|
|
21
|
+
readonly openUrl?: (url: string) => void | Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The absolute URL of the billing page Stripe returns to, without a
|
|
24
|
+
* query. Absent, the current origin's `/settings/billing`.
|
|
25
|
+
*/
|
|
26
|
+
readonly returnUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The billing page Stripe returns to, per the redirect or the current origin. */
|
|
30
|
+
export function billingReturnUrl(redirect: BillingRedirect | undefined): string {
|
|
31
|
+
if (redirect?.returnUrl !== undefined) {
|
|
32
|
+
return redirect.returnUrl;
|
|
33
|
+
}
|
|
34
|
+
const origin = typeof window === "undefined" ? "" : window.location.origin;
|
|
35
|
+
return `${origin}/settings/billing`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Leaves for a Stripe-hosted page through the redirect, or by navigating this window. */
|
|
39
|
+
export async function leaveForStripe(url: string, redirect: BillingRedirect | undefined): Promise<void> {
|
|
40
|
+
if (redirect?.openUrl !== undefined) {
|
|
41
|
+
await redirect.openUrl(url);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
window.location.href = url;
|
|
45
|
+
}
|
|
@@ -8,6 +8,16 @@ import type { BillingAccount } from "@stigmer/protos/ai/stigmer/billing/v1/billi
|
|
|
8
8
|
import { useStigmer } from "../hooks.js";
|
|
9
9
|
import { useFetch } from "../internal/useFetch.js";
|
|
10
10
|
|
|
11
|
+
/** Options for {@link useBillingAccount}. */
|
|
12
|
+
export interface UseBillingAccountOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Re-read the account when the window regains focus: for a host that
|
|
15
|
+
* sends Stripe's pages to another window (the system browser), so a card
|
|
16
|
+
* saved there shows on return. Default `false`.
|
|
17
|
+
*/
|
|
18
|
+
readonly refetchOnWindowFocus?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/** Return value of {@link useBillingAccount}. */
|
|
12
22
|
export interface UseBillingAccountReturn {
|
|
13
23
|
/** The billing account, or `null` before the first successful fetch. */
|
|
@@ -48,6 +58,7 @@ export interface UseBillingAccountReturn {
|
|
|
48
58
|
*/
|
|
49
59
|
export function useBillingAccount(
|
|
50
60
|
orgId: string | null,
|
|
61
|
+
options?: UseBillingAccountOptions,
|
|
51
62
|
): UseBillingAccountReturn {
|
|
52
63
|
const stigmer = useStigmer();
|
|
53
64
|
|
|
@@ -58,6 +69,7 @@ export function useBillingAccount(
|
|
|
58
69
|
: null,
|
|
59
70
|
[orgId, stigmer],
|
|
60
71
|
null as BillingAccount | null,
|
|
72
|
+
{ refetchOnWindowFocus: options?.refetchOnWindowFocus ?? false },
|
|
61
73
|
);
|
|
62
74
|
|
|
63
75
|
return { account, isLoading, isRefetching, error, refetch };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import type { Subscription } from "@stigmer/protos/ai/stigmer/billing/subscription/v1/api_pb";
|
|
6
|
+
import {
|
|
7
|
+
CancelSubscriptionInputSchema,
|
|
8
|
+
ChangePlanInputSchema,
|
|
9
|
+
} from "@stigmer/protos/ai/stigmer/billing/subscription/v1/io_pb";
|
|
10
|
+
import { useStigmer } from "../hooks.js";
|
|
11
|
+
import { toError } from "../internal/toError.js";
|
|
12
|
+
|
|
13
|
+
/** Return value of {@link useChangePlan}. */
|
|
14
|
+
export interface UseChangePlanReturn {
|
|
15
|
+
/**
|
|
16
|
+
* Move the organization onto a plan: subscribe (from Free), switch, or
|
|
17
|
+
* resume a canceled plan by choosing it again. A switch takes effect
|
|
18
|
+
* now: the current period closes early, billed for the time it ran, and
|
|
19
|
+
* a new monthly period opens on the new plan. Nothing is charged at the
|
|
20
|
+
* moment of the change; every period is collected when it closes.
|
|
21
|
+
*
|
|
22
|
+
* Refused with the `PAYMENT_METHOD_REQUIRED` reason when the
|
|
23
|
+
* organization has no saved card (see
|
|
24
|
+
* {@link useCreatePaymentMethodSetupSession}). Resolves with the
|
|
25
|
+
* subscription as it stands after the change.
|
|
26
|
+
*/
|
|
27
|
+
readonly changePlan: (orgId: string, planId: string) => Promise<Subscription>;
|
|
28
|
+
/** `true` while a change is in flight. */
|
|
29
|
+
readonly isSubmitting: boolean;
|
|
30
|
+
/** Error from the last failed attempt, or `null` when healthy. */
|
|
31
|
+
readonly error: Error | null;
|
|
32
|
+
/** Reset `error` to `null`. */
|
|
33
|
+
readonly clearError: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Behaviour hook that moves an organization onto a plan.
|
|
38
|
+
*
|
|
39
|
+
* The caller needs `can_manage_billing` on the organization (its admins).
|
|
40
|
+
* Refetch the subscription, entitlements and estimate after a change.
|
|
41
|
+
*/
|
|
42
|
+
export function useChangePlan(): UseChangePlanReturn {
|
|
43
|
+
const stigmer = useStigmer();
|
|
44
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
45
|
+
const [error, setError] = useState<Error | null>(null);
|
|
46
|
+
|
|
47
|
+
const changePlan = useCallback(
|
|
48
|
+
async (orgId: string, planId: string): Promise<Subscription> => {
|
|
49
|
+
setIsSubmitting(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
try {
|
|
52
|
+
return await stigmer.subscription.changePlan(create(ChangePlanInputSchema, { orgId, planId }));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
const err = toError(e);
|
|
55
|
+
setError(err);
|
|
56
|
+
throw err;
|
|
57
|
+
} finally {
|
|
58
|
+
setIsSubmitting(false);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[stigmer.subscription],
|
|
62
|
+
);
|
|
63
|
+
const clearError = useCallback(() => setError(null), []);
|
|
64
|
+
|
|
65
|
+
return useMemo(
|
|
66
|
+
() => ({ changePlan, isSubmitting, error, clearError }),
|
|
67
|
+
[changePlan, isSubmitting, error, clearError],
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Return value of {@link useCancelSubscription}. */
|
|
72
|
+
export interface UseCancelSubscriptionReturn {
|
|
73
|
+
/**
|
|
74
|
+
* Cancel the organization's subscription. It keeps its plan to the end
|
|
75
|
+
* of the current period and is on Free after it; nothing it built is
|
|
76
|
+
* deleted. Choosing the same plan again before then resumes it.
|
|
77
|
+
*/
|
|
78
|
+
readonly cancel: (orgId: string) => Promise<Subscription>;
|
|
79
|
+
/** `true` while a cancel is in flight. */
|
|
80
|
+
readonly isSubmitting: boolean;
|
|
81
|
+
/** Error from the last failed attempt, or `null` when healthy. */
|
|
82
|
+
readonly error: Error | null;
|
|
83
|
+
/** Reset `error` to `null`. */
|
|
84
|
+
readonly clearError: () => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Behaviour hook that cancels an organization's subscription at the end
|
|
89
|
+
* of its period. The caller needs `can_manage_billing` on the organization.
|
|
90
|
+
*/
|
|
91
|
+
export function useCancelSubscription(): UseCancelSubscriptionReturn {
|
|
92
|
+
const stigmer = useStigmer();
|
|
93
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
94
|
+
const [error, setError] = useState<Error | null>(null);
|
|
95
|
+
|
|
96
|
+
const cancel = useCallback(
|
|
97
|
+
async (orgId: string): Promise<Subscription> => {
|
|
98
|
+
setIsSubmitting(true);
|
|
99
|
+
setError(null);
|
|
100
|
+
try {
|
|
101
|
+
return await stigmer.subscription.cancel(create(CancelSubscriptionInputSchema, { orgId }));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
const err = toError(e);
|
|
104
|
+
setError(err);
|
|
105
|
+
throw err;
|
|
106
|
+
} finally {
|
|
107
|
+
setIsSubmitting(false);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
[stigmer.subscription],
|
|
111
|
+
);
|
|
112
|
+
const clearError = useCallback(() => setError(null), []);
|
|
113
|
+
|
|
114
|
+
return useMemo(() => ({ cancel, isSubmitting, error, clearError }), [cancel, isSubmitting, error, clearError]);
|
|
115
|
+
}
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
import { useCallback, useState } from "react";
|
|
4
4
|
import { useStigmer } from "../hooks.js";
|
|
5
5
|
import { toError } from "../internal/toError.js";
|
|
6
|
+
import { billingReturnUrl, leaveForStripe, type BillingRedirect } from "./redirect.js";
|
|
6
7
|
|
|
7
8
|
/** Return value of {@link useCreateBillingPortalSession}. */
|
|
8
9
|
export interface UseCreateBillingPortalSessionReturn {
|
|
9
10
|
/**
|
|
10
11
|
* Open the Stripe Customer Portal for payment method management.
|
|
11
12
|
*
|
|
12
|
-
* On success,
|
|
13
|
-
*
|
|
13
|
+
* On success, leaves for the Stripe-hosted portal page: through
|
|
14
|
+
* `redirect.openUrl` when given, else by navigating this window. The
|
|
15
|
+
* portal returns to `redirect.returnUrl` when given, else to the page
|
|
16
|
+
* the person is on.
|
|
14
17
|
*/
|
|
15
18
|
readonly openPortal: (orgId: string) => Promise<void>;
|
|
16
19
|
/** `true` while the portal session is being created. */
|
|
@@ -38,8 +41,12 @@ export interface UseCreateBillingPortalSessionReturn {
|
|
|
38
41
|
* </button>
|
|
39
42
|
* ```
|
|
40
43
|
*/
|
|
41
|
-
export function useCreateBillingPortalSession(
|
|
44
|
+
export function useCreateBillingPortalSession(
|
|
45
|
+
redirect?: BillingRedirect,
|
|
46
|
+
): UseCreateBillingPortalSessionReturn {
|
|
42
47
|
const stigmer = useStigmer();
|
|
48
|
+
const openUrl = redirect?.openUrl;
|
|
49
|
+
const pinnedReturnUrl = redirect?.returnUrl;
|
|
43
50
|
const [isLoading, setIsLoading] = useState(false);
|
|
44
51
|
const [error, setError] = useState<Error | null>(null);
|
|
45
52
|
|
|
@@ -52,7 +59,11 @@ export function useCreateBillingPortalSession(): UseCreateBillingPortalSessionRe
|
|
|
52
59
|
|
|
53
60
|
try {
|
|
54
61
|
const returnUrl =
|
|
55
|
-
|
|
62
|
+
pinnedReturnUrl !== undefined
|
|
63
|
+
? billingReturnUrl({ returnUrl: pinnedReturnUrl })
|
|
64
|
+
: typeof window !== "undefined"
|
|
65
|
+
? window.location.href
|
|
66
|
+
: "";
|
|
56
67
|
|
|
57
68
|
const response = await stigmer.billing.createBillingPortalSession({
|
|
58
69
|
orgId,
|
|
@@ -60,7 +71,7 @@ export function useCreateBillingPortalSession(): UseCreateBillingPortalSessionRe
|
|
|
60
71
|
});
|
|
61
72
|
|
|
62
73
|
if (response.portalUrl) {
|
|
63
|
-
|
|
74
|
+
await leaveForStripe(response.portalUrl, { openUrl });
|
|
64
75
|
}
|
|
65
76
|
} catch (err) {
|
|
66
77
|
setError(toError(err));
|
|
@@ -69,7 +80,7 @@ export function useCreateBillingPortalSession(): UseCreateBillingPortalSessionRe
|
|
|
69
80
|
setIsLoading(false);
|
|
70
81
|
}
|
|
71
82
|
},
|
|
72
|
-
[stigmer],
|
|
83
|
+
[stigmer, openUrl, pinnedReturnUrl],
|
|
73
84
|
);
|
|
74
85
|
|
|
75
86
|
return { openPortal, isLoading, error, clearError };
|
|
@@ -4,12 +4,13 @@ import { useCallback, useState } from "react";
|
|
|
4
4
|
import type { CreateCreditCheckoutSessionResponse } from "@stigmer/protos/ai/stigmer/billing/v1/io_pb";
|
|
5
5
|
import { useStigmer } from "../hooks.js";
|
|
6
6
|
import { toError } from "../internal/toError.js";
|
|
7
|
+
import { leaveForStripe, type BillingRedirect } from "./redirect.js";
|
|
7
8
|
|
|
8
9
|
/** Parameters for {@link useCreateCheckoutSession}'s `createSession` callback. */
|
|
9
10
|
export interface CreateCheckoutSessionInput {
|
|
10
11
|
/** Organization purchasing credits. */
|
|
11
12
|
readonly orgId: string;
|
|
12
|
-
/** Credit pack to purchase
|
|
13
|
+
/** Credit pack to purchase: its id, e.g. "starter", "growth" or "team" (the pack named Scale). */
|
|
13
14
|
readonly packId: string;
|
|
14
15
|
/** URL to redirect to after successful payment. */
|
|
15
16
|
readonly successUrl: string;
|
|
@@ -22,9 +23,10 @@ export interface UseCreateCheckoutSessionReturn {
|
|
|
22
23
|
/**
|
|
23
24
|
* Create a Stripe Checkout Session and redirect the user.
|
|
24
25
|
*
|
|
25
|
-
* On success,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* On success, leaves for the Stripe-hosted checkout page: through the
|
|
27
|
+
* hook's `redirect.openUrl` when given, else by navigating this window.
|
|
28
|
+
* The promise resolves with the response before the redirect occurs,
|
|
29
|
+
* allowing callers to perform cleanup if needed.
|
|
28
30
|
*/
|
|
29
31
|
readonly createSession: (
|
|
30
32
|
input: CreateCheckoutSessionInput,
|
|
@@ -60,8 +62,11 @@ export interface UseCreateCheckoutSessionReturn {
|
|
|
60
62
|
* };
|
|
61
63
|
* ```
|
|
62
64
|
*/
|
|
63
|
-
export function useCreateCheckoutSession(
|
|
65
|
+
export function useCreateCheckoutSession(
|
|
66
|
+
redirect?: BillingRedirect,
|
|
67
|
+
): UseCreateCheckoutSessionReturn {
|
|
64
68
|
const stigmer = useStigmer();
|
|
69
|
+
const openUrl = redirect?.openUrl;
|
|
65
70
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
66
71
|
const [error, setError] = useState<Error | null>(null);
|
|
67
72
|
|
|
@@ -83,7 +88,7 @@ export function useCreateCheckoutSession(): UseCreateCheckoutSessionReturn {
|
|
|
83
88
|
});
|
|
84
89
|
|
|
85
90
|
if (response.checkoutUrl) {
|
|
86
|
-
|
|
91
|
+
await leaveForStripe(response.checkoutUrl, { openUrl });
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
return response;
|
|
@@ -94,7 +99,7 @@ export function useCreateCheckoutSession(): UseCreateCheckoutSessionReturn {
|
|
|
94
99
|
setIsSubmitting(false);
|
|
95
100
|
}
|
|
96
101
|
},
|
|
97
|
-
[stigmer],
|
|
102
|
+
[stigmer, openUrl],
|
|
98
103
|
);
|
|
99
104
|
|
|
100
105
|
return { createSession, isSubmitting, error, clearError };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { useStigmer } from "../hooks.js";
|
|
5
|
+
import { toError } from "../internal/toError.js";
|
|
6
|
+
import { billingReturnUrl, leaveForStripe, type BillingRedirect } from "./redirect.js";
|
|
7
|
+
|
|
8
|
+
/** Return value of {@link useCreatePaymentMethodSetupSession}. */
|
|
9
|
+
export interface UseCreatePaymentMethodSetupSessionReturn {
|
|
10
|
+
/**
|
|
11
|
+
* Leave for Stripe's hosted page to save a card, with no charge. Stripe
|
|
12
|
+
* returns to the billing page with `?setup=success` (and `&plan=<id>`
|
|
13
|
+
* when `planId` names the plan the person was subscribing to, so the
|
|
14
|
+
* page can reopen that choice), or without a query when they leave.
|
|
15
|
+
* The saved card becomes the organization's default for every invoice.
|
|
16
|
+
*/
|
|
17
|
+
readonly addPaymentMethod: (orgId: string, planId?: string) => Promise<void>;
|
|
18
|
+
/** `true` while the setup session is being created. */
|
|
19
|
+
readonly isSubmitting: boolean;
|
|
20
|
+
/** Error from the last failed attempt, or `null` when healthy. */
|
|
21
|
+
readonly error: Error | null;
|
|
22
|
+
/** Reset `error` to `null`. */
|
|
23
|
+
readonly clearError: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Behaviour hook that saves a payment method through a Stripe-hosted setup
|
|
28
|
+
* page: the card door every subscription needs before its first period.
|
|
29
|
+
*
|
|
30
|
+
* The caller needs `can_manage_billing` on the organization. Pass the
|
|
31
|
+
* host's {@link BillingRedirect} where the window cannot host Stripe's
|
|
32
|
+
* page itself.
|
|
33
|
+
*/
|
|
34
|
+
export function useCreatePaymentMethodSetupSession(
|
|
35
|
+
redirect?: BillingRedirect,
|
|
36
|
+
): UseCreatePaymentMethodSetupSessionReturn {
|
|
37
|
+
const stigmer = useStigmer();
|
|
38
|
+
const openUrl = redirect?.openUrl;
|
|
39
|
+
const returnUrl = redirect?.returnUrl;
|
|
40
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
41
|
+
const [error, setError] = useState<Error | null>(null);
|
|
42
|
+
|
|
43
|
+
const addPaymentMethod = useCallback(
|
|
44
|
+
async (orgId: string, planId?: string): Promise<void> => {
|
|
45
|
+
setIsSubmitting(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
const billingPage = billingReturnUrl({ returnUrl });
|
|
49
|
+
const success = new URLSearchParams({ setup: "success" });
|
|
50
|
+
if (planId !== undefined && planId !== "") {
|
|
51
|
+
success.set("plan", planId);
|
|
52
|
+
}
|
|
53
|
+
const response = await stigmer.billing.createPaymentMethodSetupSession({
|
|
54
|
+
orgId,
|
|
55
|
+
successUrl: `${billingPage}?${success.toString()}`,
|
|
56
|
+
cancelUrl: billingPage,
|
|
57
|
+
});
|
|
58
|
+
if (response.setupUrl) {
|
|
59
|
+
await leaveForStripe(response.setupUrl, { openUrl });
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const err = toError(e);
|
|
63
|
+
setError(err);
|
|
64
|
+
throw err;
|
|
65
|
+
} finally {
|
|
66
|
+
setIsSubmitting(false);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[stigmer.billing, openUrl, returnUrl],
|
|
70
|
+
);
|
|
71
|
+
const clearError = useCallback(() => setError(null), []);
|
|
72
|
+
|
|
73
|
+
return useMemo(
|
|
74
|
+
() => ({ addPaymentMethod, isSubmitting, error, clearError }),
|
|
75
|
+
[addPaymentMethod, isSubmitting, error, clearError],
|
|
76
|
+
);
|
|
77
|
+
}
|