@stigmer/react 3.39.1-dev.20260928194002 → 3.40.1
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/channel-app/ChannelAppDetailPanel.d.ts +5 -0
- package/channel-app/ChannelAppDetailPanel.d.ts.map +1 -1
- package/channel-app/ChannelAppDetailPanel.js +9 -3
- package/channel-app/ChannelAppDetailPanel.js.map +1 -1
- package/channel-app/ChannelAppListPanel.d.ts +13 -4
- package/channel-app/ChannelAppListPanel.d.ts.map +1 -1
- package/channel-app/ChannelAppListPanel.js +5 -5
- package/channel-app/ChannelAppListPanel.js.map +1 -1
- package/channel-app/copy.d.ts +15 -0
- package/channel-app/copy.d.ts.map +1 -0
- package/channel-app/copy.js +15 -0
- package/channel-app/copy.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/ChannelAppsSection.d.ts +7 -0
- package/settings/ChannelAppsSection.d.ts.map +1 -1
- package/settings/ChannelAppsSection.js +15 -2
- package/settings/ChannelAppsSection.js.map +1 -1
- 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/channel-app/ChannelAppDetailPanel.tsx +179 -158
- package/src/channel-app/ChannelAppListPanel.tsx +14 -4
- package/src/channel-app/copy.ts +16 -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/ChannelAppsSection.tsx +19 -2
- 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,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";
|
|
@@ -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 };
|