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