@zuplo/zudoku-plugin-monetization 0.0.41 → 0.0.43
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/dist/{PricingTable-DNop2iX9.mjs → PricingTable-WkG2n7V-.mjs} +434 -140
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +822 -833
- package/dist/pricing-ui.d.mts +126 -31
- package/dist/pricing-ui.mjs +2 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as formatDurationAdjective, E as formatDuration, O as formatDurationInterval, T as formatPrice, _ as formatPlanPrice, a as planHasDefaultTaxBehavior, b as sameEntitlementSet, c as getPlanPriceSchedule, d as PlanEntitlements, f as PlanPhaseHeader, l as PlanPriceTag, o as subscriptionTaxLegendSentence, p as EntitlementList, r as isCustomPlan, t as PricingTable, u as PlanPriceSchedule, w as formatMinorCurrencyAmount, x as categorizeRateCards, y as comparePlanEntitlements } from "./PricingTable-WkG2n7V-.mjs";
|
|
2
2
|
import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
|
|
3
3
|
import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
|
|
4
|
-
import {
|
|
4
|
+
import { Link, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
|
|
5
5
|
import { useAuth, useZudoku } from "zudoku/hooks";
|
|
6
6
|
import { QueryClient, QueryClientProvider, queryOptions, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
|
|
7
|
-
import { Link as Link$1,
|
|
7
|
+
import { Button, ClientOnly, Head, Heading, Link as Link$1, Slot } from "zudoku/components";
|
|
8
|
+
import { createContext, use, useEffect, useMemo, useState } from "react";
|
|
9
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
10
|
import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
|
|
9
11
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "zudoku/ui/Card";
|
|
10
12
|
import { Separator } from "zudoku/ui/Separator";
|
|
11
|
-
import { createContext, use, useEffect, useMemo, useState } from "react";
|
|
12
|
-
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { Button as Button$1 } from "zudoku/ui/Button";
|
|
14
14
|
import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
|
|
15
15
|
import { ActionButton } from "zudoku/ui/ActionButton";
|
|
@@ -22,6 +22,23 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|
|
22
22
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "zudoku/ui/Dialog";
|
|
23
23
|
import { Input } from "zudoku/ui/Input";
|
|
24
24
|
import { Progress } from "zudoku/ui/Progress";
|
|
25
|
+
//#region src/utils/purchaseDetails.ts
|
|
26
|
+
const getPlanFromPurchaseDetails = (response) => {
|
|
27
|
+
return response;
|
|
28
|
+
};
|
|
29
|
+
const getTaxAmountFromPurchaseDetails = (response) => {
|
|
30
|
+
const taxAmount = response?.tax?.items[0]?.taxAmount;
|
|
31
|
+
const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
|
|
32
|
+
if (!Number.isFinite(numericAmount)) return;
|
|
33
|
+
return numericAmount;
|
|
34
|
+
};
|
|
35
|
+
const getTaxLabelFromPurchaseDetails = (response) => {
|
|
36
|
+
return (response.tax?.taxes ?? []).some((tax) => tax.taxType?.toLowerCase() === "vat") ? "VAT" : "tax";
|
|
37
|
+
};
|
|
38
|
+
const isTaxInclusiveFromPurchaseDetails = (response) => {
|
|
39
|
+
return response.tax?.taxInclusive === true;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
25
42
|
//#region src/hooks/useDeploymentName.ts
|
|
26
43
|
const useDeploymentName = () => {
|
|
27
44
|
const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
|
|
@@ -38,36 +55,26 @@ const usePurchaseDetails = (planId) => {
|
|
|
38
55
|
});
|
|
39
56
|
};
|
|
40
57
|
//#endregion
|
|
58
|
+
//#region src/hooks/usePurchaseSummary.ts
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a plan's checkout summary — the selected plan plus its tax preview —
|
|
61
|
+
* from the purchase-details endpoint. Shared by the checkout and plan-change
|
|
62
|
+
* confirmation pages so both read the plan and tax the same way.
|
|
63
|
+
*/
|
|
64
|
+
const usePurchaseSummary = (planId) => {
|
|
65
|
+
const { data } = usePurchaseDetails(planId);
|
|
66
|
+
return {
|
|
67
|
+
selectedPlan: getPlanFromPurchaseDetails(data),
|
|
68
|
+
taxAmount: getTaxAmountFromPurchaseDetails(data),
|
|
69
|
+
taxLabel: getTaxLabelFromPurchaseDetails(data),
|
|
70
|
+
taxInclusive: isTaxInclusiveFromPurchaseDetails(data)
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
41
74
|
//#region src/MonetizationContext.tsx
|
|
42
75
|
const MonetizationContext = createContext({});
|
|
43
76
|
const useMonetizationConfig = () => use(MonetizationContext);
|
|
44
77
|
//#endregion
|
|
45
|
-
//#region src/utils/formatBillingCycle.ts
|
|
46
|
-
const formatBillingCycle = (duration) => {
|
|
47
|
-
if (duration === "month") return "monthly";
|
|
48
|
-
if (duration === "year") return "annually";
|
|
49
|
-
if (duration === "week") return "weekly";
|
|
50
|
-
if (duration === "day") return "daily";
|
|
51
|
-
return `every ${duration}`;
|
|
52
|
-
};
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/utils/purchaseDetails.ts
|
|
55
|
-
const getPlanFromPurchaseDetails = (response) => {
|
|
56
|
-
return response;
|
|
57
|
-
};
|
|
58
|
-
const getTaxAmountFromPurchaseDetails = (response) => {
|
|
59
|
-
const taxAmount = response?.tax?.items[0]?.taxAmount;
|
|
60
|
-
const numericAmount = typeof taxAmount === "number" ? taxAmount : Number.parseFloat(taxAmount ?? "");
|
|
61
|
-
if (!Number.isFinite(numericAmount)) return;
|
|
62
|
-
return numericAmount;
|
|
63
|
-
};
|
|
64
|
-
const getTaxLabelFromPurchaseDetails = (response) => {
|
|
65
|
-
return (response.tax?.taxes ?? []).some((tax) => tax.taxType?.toLowerCase() === "vat") ? "VAT" : "tax";
|
|
66
|
-
};
|
|
67
|
-
const isTaxInclusiveFromPurchaseDetails = (response) => {
|
|
68
|
-
return response.tax?.taxInclusive === true;
|
|
69
|
-
};
|
|
70
|
-
//#endregion
|
|
71
78
|
//#region src/ZuploMonetizationWrapper.tsx
|
|
72
79
|
const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
|
|
73
80
|
const getBaseUrl = (context) => {
|
|
@@ -139,24 +146,18 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
|
|
|
139
146
|
})
|
|
140
147
|
});
|
|
141
148
|
//#endregion
|
|
142
|
-
//#region src/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
149
|
+
//#region src/hooks/useSubscriptionConfirmMutation.ts
|
|
150
|
+
/**
|
|
151
|
+
* Confirm a subscription purchase or plan change: POST `{ planId }` to the
|
|
152
|
+
* metering `endpoint`, invalidate cached queries, then navigate to the resulting
|
|
153
|
+
* subscription. Shared by the checkout and plan-change confirmation pages, which
|
|
154
|
+
* differ only by `endpoint` and the optional navigation `state`.
|
|
155
|
+
*/
|
|
156
|
+
const useSubscriptionConfirmMutation = ({ endpoint, planId, navigateState }) => {
|
|
146
157
|
const zudoku = useZudoku();
|
|
147
|
-
const deploymentName = useDeploymentName();
|
|
148
158
|
const navigate = useNavigate();
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const purchaseDetails = usePurchaseDetails(planId);
|
|
152
|
-
const selectedPlan = getPlanFromPurchaseDetails(purchaseDetails.data);
|
|
153
|
-
const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
|
|
154
|
-
const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
|
|
155
|
-
const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
|
|
156
|
-
const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
|
|
157
|
-
const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
|
|
158
|
-
const createSubscriptionMutation = useMutation({
|
|
159
|
-
mutationKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
|
|
159
|
+
return useMutation({
|
|
160
|
+
mutationKey: [`/v3/zudoku-metering/${useDeploymentName()}/${endpoint}`],
|
|
160
161
|
meta: {
|
|
161
162
|
context: zudoku,
|
|
162
163
|
request: {
|
|
@@ -166,18 +167,29 @@ const CheckoutConfirmPage = () => {
|
|
|
166
167
|
},
|
|
167
168
|
onSuccess: async (subscription) => {
|
|
168
169
|
await queryClient.invalidateQueries();
|
|
169
|
-
navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}
|
|
170
|
+
navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, navigateState ? { state: navigateState } : void 0);
|
|
170
171
|
}
|
|
171
172
|
});
|
|
173
|
+
};
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/pages/components/ConfirmationScreen.tsx
|
|
176
|
+
/**
|
|
177
|
+
* Shared chrome for the checkout and plan-change confirmation pages: the
|
|
178
|
+
* centered card with a check-mark header, title/message, a summary slot
|
|
179
|
+
* (`children`, typically a {@link PlanSummaryCard}), confirm/cancel actions
|
|
180
|
+
* and a terms note. Keeping this in one place means both confirmation flows
|
|
181
|
+
* stay visually and behaviorally consistent.
|
|
182
|
+
*/
|
|
183
|
+
const ConfirmationScreen = ({ title, message, errorMessage, children, confirmLabel, pendingLabel, onConfirm, isPending, confirmDisabled = false, cancelTo, cancelLabel = "Cancel", termsNote, footer }) => {
|
|
172
184
|
return /* @__PURE__ */ jsx("div", {
|
|
173
185
|
className: "w-full bg-muted min-h-screen flex items-center justify-center px-4 py-12 gap-4",
|
|
174
186
|
children: /* @__PURE__ */ jsxs("div", {
|
|
175
187
|
className: "max-w-2xl w-full",
|
|
176
188
|
children: [
|
|
177
|
-
|
|
189
|
+
errorMessage && /* @__PURE__ */ jsxs(Alert, {
|
|
178
190
|
className: "mb-4",
|
|
179
191
|
variant: "destructive",
|
|
180
|
-
children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children:
|
|
192
|
+
children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Error" }), /* @__PURE__ */ jsx(AlertDescription, { children: errorMessage })]
|
|
181
193
|
}),
|
|
182
194
|
/* @__PURE__ */ jsxs(Card, {
|
|
183
195
|
className: "p-8 w-full max-w-7xl",
|
|
@@ -193,85 +205,25 @@ const CheckoutConfirmPage = () => {
|
|
|
193
205
|
className: "text-center mb-8",
|
|
194
206
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
195
207
|
className: "text-2xl font-bold text-card-foreground mb-3",
|
|
196
|
-
children:
|
|
197
|
-
}),
|
|
198
|
-
className: "text-muted-foreground text-base",
|
|
199
|
-
children: "Please confirm the details below before completing your purchase."
|
|
200
|
-
})]
|
|
201
|
-
}),
|
|
202
|
-
selectedPlan && /* @__PURE__ */ jsxs(Card, {
|
|
203
|
-
className: "bg-muted/50",
|
|
204
|
-
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
|
|
205
|
-
className: "flex justify-between items-start",
|
|
206
|
-
children: [
|
|
207
|
-
/* @__PURE__ */ jsxs("div", {
|
|
208
|
-
className: "flex items-center gap-3",
|
|
209
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
210
|
-
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
211
|
-
children: selectedPlan.name.at(0)?.toUpperCase()
|
|
212
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
213
|
-
className: "flex flex-col",
|
|
214
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
215
|
-
className: "text-lg font-bold",
|
|
216
|
-
children: selectedPlan.name
|
|
217
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
218
|
-
className: "text-sm font-normal text-muted-foreground",
|
|
219
|
-
children: selectedPlan.description || "Selected plan"
|
|
220
|
-
})]
|
|
221
|
-
})]
|
|
222
|
-
}),
|
|
223
|
-
price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
224
|
-
className: "text-right",
|
|
225
|
-
children: [
|
|
226
|
-
/* @__PURE__ */ jsx("div", {
|
|
227
|
-
className: "text-2xl font-bold",
|
|
228
|
-
children: formatPrice(price.monthly, selectedPlan?.currency)
|
|
229
|
-
}),
|
|
230
|
-
taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
231
|
-
className: "text-sm font-normal mt-1",
|
|
232
|
-
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel}`
|
|
233
|
-
}),
|
|
234
|
-
billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
235
|
-
className: "text-sm text-muted-foreground font-normal",
|
|
236
|
-
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
237
|
-
})
|
|
238
|
-
]
|
|
239
|
-
}),
|
|
240
|
-
price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
|
|
241
|
-
className: "text-2xl text-muted-foreground font-bold",
|
|
242
|
-
children: "Free"
|
|
243
|
-
})
|
|
244
|
-
]
|
|
245
|
-
}) }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
246
|
-
/* @__PURE__ */ jsx(Separator, {}),
|
|
247
|
-
/* @__PURE__ */ jsx("div", {
|
|
248
|
-
className: "text-sm font-medium mb-3 mt-3",
|
|
249
|
-
children: "What's included:"
|
|
250
|
-
}),
|
|
251
|
-
/* @__PURE__ */ jsx(PlanEntitlements, {
|
|
252
|
-
phases: selectedPlan.phases,
|
|
253
|
-
currency: selectedPlan.currency,
|
|
254
|
-
billingCadence: selectedPlan.billingCadence,
|
|
255
|
-
units: pricing?.units,
|
|
256
|
-
itemClassName: "text-muted-foreground"
|
|
257
|
-
})
|
|
258
|
-
] })]
|
|
208
|
+
children: title
|
|
209
|
+
}), message]
|
|
259
210
|
}),
|
|
211
|
+
children,
|
|
260
212
|
/* @__PURE__ */ jsxs("div", {
|
|
261
213
|
className: "space-y-3 mt-4",
|
|
262
214
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
263
215
|
className: "w-full",
|
|
264
|
-
onClick:
|
|
265
|
-
disabled:
|
|
266
|
-
children:
|
|
216
|
+
onClick: onConfirm,
|
|
217
|
+
disabled: isPending || confirmDisabled,
|
|
218
|
+
children: isPending ? pendingLabel : confirmLabel
|
|
267
219
|
}), /* @__PURE__ */ jsx(Button, {
|
|
268
220
|
variant: "ghost",
|
|
269
221
|
className: "w-full",
|
|
270
|
-
disabled:
|
|
271
|
-
asChild: !
|
|
272
|
-
children: /* @__PURE__ */ jsx(Link
|
|
273
|
-
to:
|
|
274
|
-
children:
|
|
222
|
+
disabled: isPending,
|
|
223
|
+
asChild: !isPending,
|
|
224
|
+
children: isPending ? cancelLabel : /* @__PURE__ */ jsx(Link, {
|
|
225
|
+
to: cancelTo,
|
|
226
|
+
children: cancelLabel
|
|
275
227
|
})
|
|
276
228
|
})]
|
|
277
229
|
}),
|
|
@@ -279,20 +231,147 @@ const CheckoutConfirmPage = () => {
|
|
|
279
231
|
className: "mt-6 pt-6 border-t text-center",
|
|
280
232
|
children: /* @__PURE__ */ jsx("p", {
|
|
281
233
|
className: "text-xs text-muted-foreground",
|
|
282
|
-
children:
|
|
234
|
+
children: termsNote
|
|
283
235
|
})
|
|
284
236
|
})
|
|
285
237
|
]
|
|
286
238
|
}),
|
|
287
|
-
|
|
288
|
-
className: "flex items-center gap-2 text-muted-foreground text-xs item-center justify-center pt-4",
|
|
289
|
-
children: [/* @__PURE__ */ jsx(LockIcon, { className: "size-3" }), "Your payment is secured by Stripe"]
|
|
290
|
-
})
|
|
239
|
+
footer
|
|
291
240
|
]
|
|
292
241
|
})
|
|
293
242
|
});
|
|
294
243
|
};
|
|
295
244
|
//#endregion
|
|
245
|
+
//#region src/utils/formatBillingCycle.ts
|
|
246
|
+
const formatBillingCycle = (duration) => {
|
|
247
|
+
if (duration === "month") return "monthly";
|
|
248
|
+
if (duration === "year") return "annually";
|
|
249
|
+
if (duration === "week") return "weekly";
|
|
250
|
+
if (duration === "day") return "daily";
|
|
251
|
+
return `every ${duration}`;
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/pages/components/PlanSummaryCard.tsx
|
|
255
|
+
/**
|
|
256
|
+
* Plan summary shown on the checkout and plan-change confirmation pages: an
|
|
257
|
+
* avatar + name/description on the left and the headline price (or
|
|
258
|
+
* "Free" / "Pay as you go") plus tax and billing cadence on the right,
|
|
259
|
+
* followed by the plan's included entitlements. Multi-phase ramp plans render
|
|
260
|
+
* a full-width per-phase price schedule beneath the title instead of the
|
|
261
|
+
* single right-column price.
|
|
262
|
+
*
|
|
263
|
+
* The price is derived from the plan's rate cards via {@link formatPlanPrice}
|
|
264
|
+
* and rendered in the plan's own billing cadence, so it stays correct for any
|
|
265
|
+
* cadence (e.g. `$2.99/hour`).
|
|
266
|
+
*/
|
|
267
|
+
const PlanSummaryCard = ({ plan, descriptionFallback, taxAmount, taxLabel, taxInclusive, units, entitlementsItemClassName }) => {
|
|
268
|
+
const priceLabel = formatPlanPrice(plan);
|
|
269
|
+
const schedule = getPlanPriceSchedule(plan);
|
|
270
|
+
const billingCycle = plan.billingCadence ? formatDuration(plan.billingCadence) : null;
|
|
271
|
+
const taxLine = taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
272
|
+
className: "text-sm font-normal mt-1",
|
|
273
|
+
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel}`
|
|
274
|
+
});
|
|
275
|
+
const billedLine = billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
276
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
277
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
278
|
+
});
|
|
279
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
280
|
+
className: "bg-muted/50",
|
|
281
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsxs(CardTitle, {
|
|
282
|
+
className: "flex justify-between items-start",
|
|
283
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
284
|
+
className: "flex items-center gap-3",
|
|
285
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
286
|
+
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
287
|
+
children: plan.name.at(0)?.toUpperCase()
|
|
288
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
289
|
+
className: "flex flex-col",
|
|
290
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
291
|
+
className: "text-lg font-bold",
|
|
292
|
+
children: plan.name
|
|
293
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
294
|
+
className: "text-sm font-normal text-muted-foreground",
|
|
295
|
+
children: plan.description || descriptionFallback
|
|
296
|
+
})]
|
|
297
|
+
})]
|
|
298
|
+
}), !schedule && /* @__PURE__ */ jsxs("div", {
|
|
299
|
+
className: "text-right",
|
|
300
|
+
children: [/* @__PURE__ */ jsx(PlanPriceTag, {
|
|
301
|
+
label: priceLabel,
|
|
302
|
+
currency: plan.currency,
|
|
303
|
+
size: "lg",
|
|
304
|
+
description: true
|
|
305
|
+
}), priceLabel.type === "priced" && /* @__PURE__ */ jsxs(Fragment$1, { children: [taxLine, billedLine] })]
|
|
306
|
+
})]
|
|
307
|
+
}), schedule && /* @__PURE__ */ jsxs("div", {
|
|
308
|
+
className: "mt-3 font-normal",
|
|
309
|
+
children: [/* @__PURE__ */ jsx(PlanPriceSchedule, {
|
|
310
|
+
schedule,
|
|
311
|
+
currency: plan.currency,
|
|
312
|
+
billingCadence: plan.billingCadence
|
|
313
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
314
|
+
className: "text-right",
|
|
315
|
+
children: [taxLine, billedLine]
|
|
316
|
+
})]
|
|
317
|
+
})] }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
318
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
319
|
+
/* @__PURE__ */ jsx("div", {
|
|
320
|
+
className: "text-sm font-medium mb-3 mt-3",
|
|
321
|
+
children: "What's included:"
|
|
322
|
+
}),
|
|
323
|
+
/* @__PURE__ */ jsx(PlanEntitlements, {
|
|
324
|
+
phases: plan.phases,
|
|
325
|
+
currency: plan.currency,
|
|
326
|
+
billingCadence: plan.billingCadence,
|
|
327
|
+
units,
|
|
328
|
+
itemClassName: entitlementsItemClassName
|
|
329
|
+
})
|
|
330
|
+
] })]
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/pages/CheckoutConfirmPage.tsx
|
|
335
|
+
const CheckoutConfirmPage = () => {
|
|
336
|
+
const [search] = useSearchParams();
|
|
337
|
+
const planId = search.get("planId");
|
|
338
|
+
const { pricing } = useMonetizationConfig();
|
|
339
|
+
if (!planId) throw new Error("Parameter `planId` missing");
|
|
340
|
+
const { selectedPlan, taxAmount, taxLabel, taxInclusive } = usePurchaseSummary(planId);
|
|
341
|
+
const createSubscriptionMutation = useSubscriptionConfirmMutation({
|
|
342
|
+
endpoint: "subscriptions",
|
|
343
|
+
planId
|
|
344
|
+
});
|
|
345
|
+
return /* @__PURE__ */ jsx(ConfirmationScreen, {
|
|
346
|
+
title: "Review your subscription",
|
|
347
|
+
message: /* @__PURE__ */ jsx("p", {
|
|
348
|
+
className: "text-muted-foreground text-base",
|
|
349
|
+
children: "Please confirm the details below before completing your purchase."
|
|
350
|
+
}),
|
|
351
|
+
errorMessage: createSubscriptionMutation.isError ? createSubscriptionMutation.error.message : void 0,
|
|
352
|
+
confirmLabel: "Confirm & Subscribe",
|
|
353
|
+
pendingLabel: "Processing Payment...",
|
|
354
|
+
onConfirm: () => createSubscriptionMutation.mutate(),
|
|
355
|
+
isPending: createSubscriptionMutation.isPending,
|
|
356
|
+
confirmDisabled: !selectedPlan,
|
|
357
|
+
cancelTo: "/pricing",
|
|
358
|
+
termsNote: "By confirming, you agree to our Terms of Service and Privacy Policy. You can cancel anytime.",
|
|
359
|
+
footer: /* @__PURE__ */ jsxs("div", {
|
|
360
|
+
className: "flex items-center gap-2 text-muted-foreground text-xs item-center justify-center pt-4",
|
|
361
|
+
children: [/* @__PURE__ */ jsx(LockIcon, { className: "size-3" }), "Your payment is secured by Stripe"]
|
|
362
|
+
}),
|
|
363
|
+
children: selectedPlan && /* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
364
|
+
plan: selectedPlan,
|
|
365
|
+
descriptionFallback: "Selected plan",
|
|
366
|
+
taxAmount,
|
|
367
|
+
taxLabel,
|
|
368
|
+
taxInclusive,
|
|
369
|
+
units: pricing?.units,
|
|
370
|
+
entitlementsItemClassName: "text-muted-foreground"
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
296
375
|
//#region src/components/RedirectPage.tsx
|
|
297
376
|
const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
298
377
|
useEffect(() => {
|
|
@@ -391,7 +470,7 @@ const CheckoutPage = () => {
|
|
|
391
470
|
variant: "outline",
|
|
392
471
|
size: "xs",
|
|
393
472
|
asChild: true,
|
|
394
|
-
children: /* @__PURE__ */ jsx(Link
|
|
473
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
395
474
|
to: "/subscriptions",
|
|
396
475
|
children: "Back"
|
|
397
476
|
})
|
|
@@ -433,7 +512,7 @@ const ManagePaymentPage = () => {
|
|
|
433
512
|
variant: "outline",
|
|
434
513
|
size: "xs",
|
|
435
514
|
asChild: true,
|
|
436
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
515
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
437
516
|
to: "/subscriptions",
|
|
438
517
|
children: "Back"
|
|
439
518
|
})
|
|
@@ -494,19 +573,18 @@ const PricingPage = () => {
|
|
|
494
573
|
}),
|
|
495
574
|
/* @__PURE__ */ jsx(PricingTable, {
|
|
496
575
|
plans: pricingTable.items,
|
|
497
|
-
showYearlyPrice: pricing?.showYearlyPrice !== false,
|
|
498
576
|
units: pricing?.units,
|
|
499
577
|
renderAction: (plan, isPopular) => isSubscribed ? /* @__PURE__ */ jsx(Button, {
|
|
500
578
|
variant: isPopular ? "default" : "outline",
|
|
501
579
|
asChild: true,
|
|
502
|
-
children: /* @__PURE__ */ jsx(Link
|
|
580
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
503
581
|
to: `/subscriptions#manage`,
|
|
504
582
|
children: "Manage Subscriptions"
|
|
505
583
|
})
|
|
506
584
|
}) : /* @__PURE__ */ jsx(Button, {
|
|
507
585
|
variant: isPopular ? "default" : "outline",
|
|
508
586
|
asChild: true,
|
|
509
|
-
children: /* @__PURE__ */ jsx(Link
|
|
587
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
510
588
|
to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
|
|
511
589
|
children: "Subscribe"
|
|
512
590
|
})
|
|
@@ -517,6 +595,284 @@ const PricingPage = () => {
|
|
|
517
595
|
});
|
|
518
596
|
};
|
|
519
597
|
//#endregion
|
|
598
|
+
//#region src/hooks/useChangeCreditEstimate.ts
|
|
599
|
+
/**
|
|
600
|
+
* Preview the proration credit for changing a subscription's plan, via the
|
|
601
|
+
* Zuplo metering `.../change/estimate-credit` endpoint. Failures are swallowed
|
|
602
|
+
* (`retry: false`, `throwOnError: false`) so the confirmation page never breaks
|
|
603
|
+
* when the preview is unavailable.
|
|
604
|
+
*/
|
|
605
|
+
const useChangeCreditEstimate = (subscriptionId, timing) => {
|
|
606
|
+
const zudoku = useZudoku();
|
|
607
|
+
return useQuery({
|
|
608
|
+
queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/subscriptions/${subscriptionId}/change/estimate-credit`, timing],
|
|
609
|
+
meta: {
|
|
610
|
+
context: zudoku,
|
|
611
|
+
request: {
|
|
612
|
+
method: "POST",
|
|
613
|
+
body: JSON.stringify({ timing })
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
retry: false,
|
|
617
|
+
throwOnError: false
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
/**
|
|
621
|
+
* Extract a positive credit amount from the estimate, or `undefined` when there
|
|
622
|
+
* isn't a usable one (so the UI only shows a credit when there actually is one).
|
|
623
|
+
*/
|
|
624
|
+
const getEstimatedCreditAmount = (estimate) => {
|
|
625
|
+
if (!estimate) return void 0;
|
|
626
|
+
const amount = Number.parseFloat(estimate.creditAmount ?? "");
|
|
627
|
+
if (!Number.isFinite(amount) || amount <= 0) return void 0;
|
|
628
|
+
return {
|
|
629
|
+
amount,
|
|
630
|
+
currency: estimate.currency
|
|
631
|
+
};
|
|
632
|
+
};
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/utils/formatDateTime.ts
|
|
635
|
+
/**
|
|
636
|
+
* Format an ISO timestamp with the time of day included, e.g.
|
|
637
|
+
* "Jun 3, 2026, 2:00 PM". Subscriptions can bill on sub-day cadences, so the
|
|
638
|
+
* time is what disambiguates a period's boundaries and the next renewal.
|
|
639
|
+
*/
|
|
640
|
+
const formatDateTime = (dateString) => new Date(dateString).toLocaleString("en-US", {
|
|
641
|
+
month: "short",
|
|
642
|
+
day: "numeric",
|
|
643
|
+
year: "numeric",
|
|
644
|
+
hour: "numeric",
|
|
645
|
+
minute: "2-digit"
|
|
646
|
+
});
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/utils/billables.ts
|
|
649
|
+
const getActivePhase = (sub) => {
|
|
650
|
+
const now = Date.now();
|
|
651
|
+
return sub.phases.filter((p) => new Date(p.activeFrom).getTime() <= now && (!p.activeTo || new Date(p.activeTo).getTime() >= now)).sort((a, b) => new Date(b.activeFrom).getTime() - new Date(a.activeFrom).getTime())[0];
|
|
652
|
+
};
|
|
653
|
+
const activePhaseHasBillables = (sub) => getActivePhase(sub)?.items.some((i) => i.price != null) ?? false;
|
|
654
|
+
const hasFutureBillables = (sub) => {
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
return sub.phases.filter((p) => new Date(p.activeFrom).getTime() > now).some((p) => p.items.some((i) => i.price != null));
|
|
657
|
+
};
|
|
658
|
+
//#endregion
|
|
659
|
+
//#region src/utils/subscriptionEntitlements.ts
|
|
660
|
+
const toRateCardPrice = (price) => {
|
|
661
|
+
if (!price) return null;
|
|
662
|
+
if (price.type === "tiered" && price.tiers) return {
|
|
663
|
+
type: "tiered",
|
|
664
|
+
mode: price.mode === "volume" ? "volume" : "graduated",
|
|
665
|
+
tiers: price.tiers.map((t) => ({
|
|
666
|
+
upToAmount: t.upToAmount,
|
|
667
|
+
flatPrice: t.flatPrice ? { amount: t.flatPrice.amount } : void 0,
|
|
668
|
+
unitPrice: t.unitPrice ? { amount: t.unitPrice.amount } : void 0
|
|
669
|
+
}))
|
|
670
|
+
};
|
|
671
|
+
if (price.type === "unit" && price.amount != null) return {
|
|
672
|
+
type: "unit",
|
|
673
|
+
amount: price.amount
|
|
674
|
+
};
|
|
675
|
+
if (price.type === "flat" && price.amount != null) return {
|
|
676
|
+
type: "flat",
|
|
677
|
+
amount: price.amount,
|
|
678
|
+
paymentTerm: price.paymentTerm === "in_advance" || price.paymentTerm === "in_arrears" ? price.paymentTerm : void 0
|
|
679
|
+
};
|
|
680
|
+
return null;
|
|
681
|
+
};
|
|
682
|
+
const flatOrNull = (price) => price?.type === "flat" ? price : null;
|
|
683
|
+
/**
|
|
684
|
+
* Convert an actual provisioned subscription line item into the `RateCard`
|
|
685
|
+
* shape so it can run through the same {@link categorizeRateCards} logic the
|
|
686
|
+
* pricing card uses. This is the key to showing a subscription's *real*
|
|
687
|
+
* included quota (e.g. `10 / hour`) — the catalog plan reports `0` for a
|
|
688
|
+
* priced first tier, but the subscription item carries the true
|
|
689
|
+
* `issueAfterReset`.
|
|
690
|
+
*/
|
|
691
|
+
const itemToRateCard = (item) => {
|
|
692
|
+
const ent = item.included?.entitlement;
|
|
693
|
+
const name = item.name ?? item.included?.feature?.name ?? item.featureKey ?? item.key;
|
|
694
|
+
const base = {
|
|
695
|
+
key: item.key,
|
|
696
|
+
name,
|
|
697
|
+
featureKey: item.featureKey
|
|
698
|
+
};
|
|
699
|
+
const price = toRateCardPrice(item.price);
|
|
700
|
+
const billingCadence = item.billingCadence ?? ent?.usagePeriod?.intervalISO ?? null;
|
|
701
|
+
if (ent?.type === "metered") {
|
|
702
|
+
const entitlementTemplate = {
|
|
703
|
+
type: "metered",
|
|
704
|
+
isSoftLimit: ent.isSoftLimit,
|
|
705
|
+
issueAfterReset: ent.issueAfterReset,
|
|
706
|
+
usagePeriod: ent.usagePeriod?.intervalISO
|
|
707
|
+
};
|
|
708
|
+
if (price?.type === "flat") return {
|
|
709
|
+
...base,
|
|
710
|
+
type: "flat_fee",
|
|
711
|
+
billingCadence,
|
|
712
|
+
price,
|
|
713
|
+
entitlementTemplate
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...base,
|
|
717
|
+
type: "usage_based",
|
|
718
|
+
billingCadence: billingCadence ?? "P1M",
|
|
719
|
+
price,
|
|
720
|
+
entitlementTemplate
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (ent?.type === "boolean") return {
|
|
724
|
+
...base,
|
|
725
|
+
type: "flat_fee",
|
|
726
|
+
billingCadence,
|
|
727
|
+
price: flatOrNull(price),
|
|
728
|
+
entitlementTemplate: { type: "boolean" }
|
|
729
|
+
};
|
|
730
|
+
if (ent?.type === "static") return {
|
|
731
|
+
...base,
|
|
732
|
+
type: "flat_fee",
|
|
733
|
+
billingCadence,
|
|
734
|
+
price: flatOrNull(price),
|
|
735
|
+
entitlementTemplate: {
|
|
736
|
+
type: "static",
|
|
737
|
+
config: ent.config ?? ""
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
return {
|
|
741
|
+
...base,
|
|
742
|
+
type: "flat_fee",
|
|
743
|
+
billingCadence,
|
|
744
|
+
price: flatOrNull(price)
|
|
745
|
+
};
|
|
746
|
+
};
|
|
747
|
+
/**
|
|
748
|
+
* Categorize a subscription phase's actual items into `{ quotas, features }`,
|
|
749
|
+
* reusing {@link categorizeRateCards} so the subscription view stays
|
|
750
|
+
* consistent with the pricing card.
|
|
751
|
+
*/
|
|
752
|
+
const categorizeSubscriptionItems = (items, options) => categorizeRateCards(items.map(itemToRateCard), options);
|
|
753
|
+
/** Map a subscription phase's items to rate cards (for price/entitlement derivation). */
|
|
754
|
+
const subscriptionItemsToRateCards = (items) => items.map(itemToRateCard);
|
|
755
|
+
/**
|
|
756
|
+
* Resolve a subscription's headline price and entitlements, preferring the
|
|
757
|
+
* active phase's ACTUAL provisioned items (the authoritative source — real
|
|
758
|
+
* included quotas and recurring fees) and falling back to the catalog plan's
|
|
759
|
+
* rate cards only when items aren't populated. Keeping both the price and the
|
|
760
|
+
* entitlements on the same source avoids showing "Free" for a paid plan whose
|
|
761
|
+
* embedded snapshot is incomplete.
|
|
762
|
+
*/
|
|
763
|
+
const getSubscriptionPlanView = (subscription, options) => {
|
|
764
|
+
const plan = subscription.plan;
|
|
765
|
+
const currency = subscription.currency ?? plan.currency;
|
|
766
|
+
const billingCadence = subscription.billingCadence ?? plan.billingCadence;
|
|
767
|
+
const items = getActivePhase(subscription)?.items ?? [];
|
|
768
|
+
if (items.length > 0) {
|
|
769
|
+
const rateCards = subscriptionItemsToRateCards(items);
|
|
770
|
+
return {
|
|
771
|
+
priceLabel: formatPlanPrice({
|
|
772
|
+
...plan,
|
|
773
|
+
billingCadence,
|
|
774
|
+
phases: [{
|
|
775
|
+
key: "active",
|
|
776
|
+
name: "Active",
|
|
777
|
+
rateCards
|
|
778
|
+
}]
|
|
779
|
+
}),
|
|
780
|
+
entitlements: categorizeRateCards(rateCards, {
|
|
781
|
+
currency,
|
|
782
|
+
units: options?.units,
|
|
783
|
+
planBillingCadence: billingCadence
|
|
784
|
+
}),
|
|
785
|
+
fallbackPhases: [],
|
|
786
|
+
usingItems: true,
|
|
787
|
+
billingCadence,
|
|
788
|
+
currency
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
priceLabel: formatPlanPrice(plan),
|
|
793
|
+
entitlements: {
|
|
794
|
+
quotas: [],
|
|
795
|
+
features: []
|
|
796
|
+
},
|
|
797
|
+
fallbackPhases: plan.phases ?? [],
|
|
798
|
+
usingItems: false,
|
|
799
|
+
billingCadence,
|
|
800
|
+
currency
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
/** Whether a resolved view has any entitlements to render. */
|
|
804
|
+
const hasSubscriptionEntitlements = (view) => view.usingItems ? view.entitlements.quotas.length > 0 || view.entitlements.features.length > 0 : view.fallbackPhases.some((p) => p.rateCards?.some((rc) => rc.entitlementTemplate));
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/pages/components/SubscriptionEntitlements.tsx
|
|
807
|
+
/**
|
|
808
|
+
* Render a subscription's entitlements from a resolved
|
|
809
|
+
* {@link SubscriptionPlanView}: the active phase's *provisioned* items when
|
|
810
|
+
* present (the real included quotas + per-unit prices), otherwise the catalog
|
|
811
|
+
* plan's phases. Centralizes the "items, else fall back to plan phases" branch
|
|
812
|
+
* shared by the subscription details page and the Switch Plan baseline.
|
|
813
|
+
*
|
|
814
|
+
* Renders `null` when there's nothing to show, so callers can wrap it with
|
|
815
|
+
* {@link hasSubscriptionEntitlements} (for a section header / border) without
|
|
816
|
+
* leaving an empty container behind.
|
|
817
|
+
*/
|
|
818
|
+
const SubscriptionEntitlements = ({ view, currency, billingCadence, units, itemClassName }) => {
|
|
819
|
+
if (view.usingItems) {
|
|
820
|
+
const { quotas, features } = view.entitlements;
|
|
821
|
+
return /* @__PURE__ */ jsx(EntitlementList, {
|
|
822
|
+
quotas,
|
|
823
|
+
features,
|
|
824
|
+
itemClassName
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
if (view.fallbackPhases.length === 0) return null;
|
|
828
|
+
return /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
829
|
+
phases: view.fallbackPhases,
|
|
830
|
+
currency,
|
|
831
|
+
billingCadence,
|
|
832
|
+
units,
|
|
833
|
+
itemClassName
|
|
834
|
+
});
|
|
835
|
+
};
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/pages/components/CurrentPlanBaseline.tsx
|
|
838
|
+
/**
|
|
839
|
+
* Baseline shown at the top of the Switch Plan modal: the current plan's price
|
|
840
|
+
* and what it actually includes, so users have a concrete reference to compare
|
|
841
|
+
* targets against. Both the price and the entitlements come from the
|
|
842
|
+
* subscription's *provisioned* items (real included quotas + recurring fees),
|
|
843
|
+
* falling back to the plan's rate cards only when items aren't present — see
|
|
844
|
+
* {@link getSubscriptionPlanView}.
|
|
845
|
+
*/
|
|
846
|
+
const CurrentPlanBaseline = ({ subscription, units }) => {
|
|
847
|
+
const plan = subscription.plan;
|
|
848
|
+
const view = getSubscriptionPlanView(subscription, { units });
|
|
849
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
850
|
+
className: "border rounded-lg p-4",
|
|
851
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
852
|
+
className: "flex items-start justify-between gap-3 flex-wrap",
|
|
853
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
854
|
+
className: "text-xs font-medium text-muted-foreground uppercase tracking-wide",
|
|
855
|
+
children: "Current Plan"
|
|
856
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
857
|
+
className: "text-lg font-bold text-foreground",
|
|
858
|
+
children: plan.name
|
|
859
|
+
})] }), /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
860
|
+
label: view.priceLabel,
|
|
861
|
+
currency: view.currency,
|
|
862
|
+
billingCadence: view.billingCadence
|
|
863
|
+
})]
|
|
864
|
+
}), hasSubscriptionEntitlements(view) && /* @__PURE__ */ jsx("div", {
|
|
865
|
+
className: "mt-3 pt-3 border-t",
|
|
866
|
+
children: /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
867
|
+
view,
|
|
868
|
+
currency: view.currency,
|
|
869
|
+
billingCadence: view.billingCadence,
|
|
870
|
+
units
|
|
871
|
+
})
|
|
872
|
+
})]
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
//#endregion
|
|
520
876
|
//#region src/pages/SubscriptionChangeConfirmPage.tsx
|
|
521
877
|
const SubscriptionChangeConfirmPage = () => {
|
|
522
878
|
const [search] = useSearchParams();
|
|
@@ -524,152 +880,68 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
524
880
|
const subscriptionId = search.get("subscriptionId");
|
|
525
881
|
const mode = search.get("mode");
|
|
526
882
|
const zudoku = useZudoku();
|
|
527
|
-
const deploymentName = useDeploymentName();
|
|
528
|
-
const navigate = useNavigate();
|
|
529
883
|
const { pricing } = useMonetizationConfig();
|
|
530
884
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
531
885
|
if (!subscriptionId) throw new Error("Parameter `subscriptionId` missing");
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
const
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
context: zudoku,
|
|
544
|
-
request: {
|
|
545
|
-
method: "POST",
|
|
546
|
-
body: JSON.stringify({ planId })
|
|
547
|
-
}
|
|
548
|
-
},
|
|
549
|
-
onSuccess: async (subscription) => {
|
|
550
|
-
await queryClient.invalidateQueries();
|
|
551
|
-
navigate(`/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`, { state: { planSwitched: { newPlanName: selectedPlan?.name } } });
|
|
552
|
-
}
|
|
886
|
+
const { selectedPlan, taxAmount, taxLabel, taxInclusive } = usePurchaseSummary(planId);
|
|
887
|
+
const { data: subscriptionsData } = useQuery(subscriptionsQuery(zudoku));
|
|
888
|
+
const currentSubscription = subscriptionsData?.items.find((s) => s.id === subscriptionId);
|
|
889
|
+
const isDowngrade = mode === "downgrade";
|
|
890
|
+
const credit = getEstimatedCreditAmount(useChangeCreditEstimate(subscriptionId, isDowngrade ? "next_billing_cycle" : "immediate").data);
|
|
891
|
+
const nextCycleEnd = currentSubscription?.alignment?.currentAlignedBillingPeriod?.to;
|
|
892
|
+
const effectiveText = isDowngrade ? nextCycleEnd ? `Takes effect ${formatDateTime(nextCycleEnd)}, at the start of your next billing cycle` : "Takes effect at the start of your next billing cycle" : "Takes effect immediately";
|
|
893
|
+
const changeMutation = useSubscriptionConfirmMutation({
|
|
894
|
+
endpoint: `subscriptions/${subscriptionId}/change`,
|
|
895
|
+
planId,
|
|
896
|
+
navigateState: { planSwitched: { newPlanName: selectedPlan?.name } }
|
|
553
897
|
});
|
|
554
|
-
return /* @__PURE__ */ jsx(
|
|
555
|
-
|
|
898
|
+
return /* @__PURE__ */ jsx(ConfirmationScreen, {
|
|
899
|
+
title: "Confirm plan change",
|
|
900
|
+
message: /* @__PURE__ */ jsx("p", {
|
|
901
|
+
className: "text-muted-foreground text-base",
|
|
902
|
+
children: "Review the change below before confirming."
|
|
903
|
+
}),
|
|
904
|
+
errorMessage: changeMutation.isError ? changeMutation.error.message : void 0,
|
|
905
|
+
confirmLabel: "Confirm & Change Plan",
|
|
906
|
+
pendingLabel: "Changing plan...",
|
|
907
|
+
onConfirm: () => changeMutation.mutate(),
|
|
908
|
+
isPending: changeMutation.isPending,
|
|
909
|
+
cancelTo: `/subscriptions?${new URLSearchParams({ subscriptionId })}`,
|
|
910
|
+
termsNote: "By confirming, you agree to our Terms of Service and Privacy Policy.",
|
|
556
911
|
children: /* @__PURE__ */ jsxs("div", {
|
|
557
|
-
className: "
|
|
558
|
-
children: [
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
912
|
+
className: "space-y-3",
|
|
913
|
+
children: [
|
|
914
|
+
currentSubscription && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CurrentPlanBaseline, {
|
|
915
|
+
subscription: currentSubscription,
|
|
916
|
+
units: pricing?.units
|
|
917
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
918
|
+
className: "flex items-center justify-center gap-1.5 text-sm text-muted-foreground",
|
|
919
|
+
children: [/* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4" }), " Changing to"]
|
|
920
|
+
})] }),
|
|
921
|
+
selectedPlan && /* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
922
|
+
plan: selectedPlan,
|
|
923
|
+
descriptionFallback: "New plan",
|
|
924
|
+
taxAmount,
|
|
925
|
+
taxLabel,
|
|
926
|
+
taxInclusive,
|
|
927
|
+
units: pricing?.units
|
|
928
|
+
}),
|
|
929
|
+
/* @__PURE__ */ jsxs("div", {
|
|
930
|
+
className: "rounded-lg bg-muted/50 p-3 text-sm space-y-1",
|
|
931
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
932
|
+
className: "font-medium text-card-foreground",
|
|
933
|
+
children: effectiveText
|
|
934
|
+
}), credit && /* @__PURE__ */ jsxs("div", {
|
|
935
|
+
className: "text-muted-foreground",
|
|
574
936
|
children: [
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
/* @__PURE__ */ jsx("p", {
|
|
580
|
-
className: "text-muted-foreground text-base",
|
|
581
|
-
children: effectiveChangeMessage
|
|
582
|
-
}),
|
|
583
|
-
/* @__PURE__ */ jsx("p", {
|
|
584
|
-
className: "text-muted-foreground text-base",
|
|
585
|
-
children: "Please confirm the details below to change your subscription."
|
|
586
|
-
})
|
|
937
|
+
"You'll be credited ",
|
|
938
|
+
formatPrice(credit.amount, credit.currency),
|
|
939
|
+
" ",
|
|
940
|
+
"for unused time on your current plan."
|
|
587
941
|
]
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
|
|
592
|
-
className: "flex justify-between items-start",
|
|
593
|
-
children: [
|
|
594
|
-
/* @__PURE__ */ jsxs("div", {
|
|
595
|
-
className: "flex items-center gap-3",
|
|
596
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
597
|
-
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
598
|
-
children: selectedPlan.name.at(0)?.toUpperCase()
|
|
599
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
600
|
-
className: "flex flex-col",
|
|
601
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
602
|
-
className: "text-lg font-bold",
|
|
603
|
-
children: selectedPlan.name
|
|
604
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
605
|
-
className: "text-sm font-normal text-muted-foreground",
|
|
606
|
-
children: selectedPlan.description || "New plan"
|
|
607
|
-
})]
|
|
608
|
-
})]
|
|
609
|
-
}),
|
|
610
|
-
price && price.monthly > 0 && /* @__PURE__ */ jsxs("div", {
|
|
611
|
-
className: "text-right",
|
|
612
|
-
children: [
|
|
613
|
-
/* @__PURE__ */ jsx("div", {
|
|
614
|
-
className: "text-2xl font-bold",
|
|
615
|
-
children: formatPrice(price.monthly, selectedPlan?.currency)
|
|
616
|
-
}),
|
|
617
|
-
taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
618
|
-
className: "text-sm font-normal mt-1",
|
|
619
|
-
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, selectedPlan?.currency)} ${taxLabel}`
|
|
620
|
-
}),
|
|
621
|
-
billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
622
|
-
className: "text-sm text-muted-foreground font-normal",
|
|
623
|
-
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
624
|
-
})
|
|
625
|
-
]
|
|
626
|
-
}),
|
|
627
|
-
price && price.monthly === 0 && /* @__PURE__ */ jsx("div", {
|
|
628
|
-
className: "text-2xl text-muted-foreground font-bold",
|
|
629
|
-
children: "Free"
|
|
630
|
-
})
|
|
631
|
-
]
|
|
632
|
-
}) }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
633
|
-
/* @__PURE__ */ jsx(Separator, {}),
|
|
634
|
-
/* @__PURE__ */ jsx("div", {
|
|
635
|
-
className: "text-sm font-medium mb-3 mt-3",
|
|
636
|
-
children: "What's included:"
|
|
637
|
-
}),
|
|
638
|
-
/* @__PURE__ */ jsx(PlanEntitlements, {
|
|
639
|
-
phases: selectedPlan.phases,
|
|
640
|
-
currency: selectedPlan.currency,
|
|
641
|
-
billingCadence: selectedPlan.billingCadence,
|
|
642
|
-
units: pricing?.units
|
|
643
|
-
})
|
|
644
|
-
] })]
|
|
645
|
-
}),
|
|
646
|
-
/* @__PURE__ */ jsxs("div", {
|
|
647
|
-
className: "space-y-3 mt-4",
|
|
648
|
-
children: [/* @__PURE__ */ jsx(Button, {
|
|
649
|
-
className: "w-full",
|
|
650
|
-
onClick: () => changeMutation.mutate(),
|
|
651
|
-
disabled: changeMutation.isPending,
|
|
652
|
-
children: changeMutation.isPending ? "Changing plan..." : "Confirm & Change Plan"
|
|
653
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
654
|
-
variant: "ghost",
|
|
655
|
-
className: "w-full",
|
|
656
|
-
disabled: changeMutation.isPending,
|
|
657
|
-
asChild: !changeMutation.isPending,
|
|
658
|
-
children: /* @__PURE__ */ jsx(Link$1, {
|
|
659
|
-
to: `/subscriptions?${new URLSearchParams({ subscriptionId: subscriptionId ?? "" })}`,
|
|
660
|
-
children: "Cancel"
|
|
661
|
-
})
|
|
662
|
-
})]
|
|
663
|
-
}),
|
|
664
|
-
/* @__PURE__ */ jsx("div", {
|
|
665
|
-
className: "mt-6 pt-6 border-t text-center",
|
|
666
|
-
children: /* @__PURE__ */ jsx("p", {
|
|
667
|
-
className: "text-xs text-muted-foreground",
|
|
668
|
-
children: "By confirming, you agree to our Terms of Service and Privacy Policy."
|
|
669
|
-
})
|
|
670
|
-
})
|
|
671
|
-
]
|
|
672
|
-
})]
|
|
942
|
+
})]
|
|
943
|
+
})
|
|
944
|
+
]
|
|
673
945
|
})
|
|
674
946
|
});
|
|
675
947
|
};
|
|
@@ -689,17 +961,6 @@ const useSubscriptions = () => {
|
|
|
689
961
|
});
|
|
690
962
|
};
|
|
691
963
|
//#endregion
|
|
692
|
-
//#region src/utils/billables.ts
|
|
693
|
-
const getActivePhase = (sub) => {
|
|
694
|
-
const now = Date.now();
|
|
695
|
-
return sub.phases.filter((p) => new Date(p.activeFrom).getTime() <= now && (!p.activeTo || new Date(p.activeTo).getTime() >= now)).sort((a, b) => new Date(b.activeFrom).getTime() - new Date(a.activeFrom).getTime())[0];
|
|
696
|
-
};
|
|
697
|
-
const activePhaseHasBillables = (sub) => getActivePhase(sub)?.items.some((i) => i.price != null) ?? false;
|
|
698
|
-
const hasFutureBillables = (sub) => {
|
|
699
|
-
const now = Date.now();
|
|
700
|
-
return sub.phases.filter((p) => new Date(p.activeFrom).getTime() > now).some((p) => p.items.some((i) => i.price != null));
|
|
701
|
-
};
|
|
702
|
-
//#endregion
|
|
703
964
|
//#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
|
|
704
965
|
const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
|
|
705
966
|
return /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
|
|
@@ -712,7 +973,7 @@ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
|
|
|
712
973
|
};
|
|
713
974
|
//#endregion
|
|
714
975
|
//#region src/pages/subscriptions/ApiKey.tsx
|
|
715
|
-
const formatDate$
|
|
976
|
+
const formatDate$1 = (dateString) => {
|
|
716
977
|
if (!dateString) return "";
|
|
717
978
|
return new Date(dateString).toLocaleDateString("en-US", {
|
|
718
979
|
month: "short",
|
|
@@ -731,7 +992,7 @@ const getTimeAgo = (dateString) => {
|
|
|
731
992
|
const diffInDays = Math.floor(diffInHours / 24);
|
|
732
993
|
if (diffInDays === 1) return "1 day ago";
|
|
733
994
|
if (diffInDays < 30) return `${diffInDays} days ago`;
|
|
734
|
-
return formatDate$
|
|
995
|
+
return formatDate$1(dateString);
|
|
735
996
|
};
|
|
736
997
|
const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
|
|
737
998
|
const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
|
|
@@ -788,7 +1049,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
|
|
|
788
1049
|
children: [
|
|
789
1050
|
/* @__PURE__ */ jsxs("div", {
|
|
790
1051
|
className: "flex items-center gap-1.5",
|
|
791
|
-
children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$
|
|
1052
|
+
children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(createdAt)] })]
|
|
792
1053
|
}),
|
|
793
1054
|
/* @__PURE__ */ jsx("span", {
|
|
794
1055
|
className: "text-muted-foreground/40",
|
|
@@ -803,7 +1064,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
|
|
|
803
1064
|
children: [
|
|
804
1065
|
isExpired ? "Expired" : "Expires",
|
|
805
1066
|
" on ",
|
|
806
|
-
formatDate$
|
|
1067
|
+
formatDate$1(expiresOn)
|
|
807
1068
|
]
|
|
808
1069
|
})] })
|
|
809
1070
|
]
|
|
@@ -918,7 +1179,7 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
|
|
|
918
1179
|
/* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
|
|
919
1180
|
/* @__PURE__ */ jsx(AlertDescription, { children: (() => {
|
|
920
1181
|
const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
|
|
921
|
-
return deletedKey ? `API key created ${formatDate$
|
|
1182
|
+
return deletedKey ? `API key created ${formatDate$1(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
|
|
922
1183
|
})() }),
|
|
923
1184
|
/* @__PURE__ */ jsx(DismissibleAlertAction, {})
|
|
924
1185
|
]
|
|
@@ -1041,7 +1302,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
|
|
|
1041
1302
|
" subscription?"
|
|
1042
1303
|
] }), /* @__PURE__ */ jsx(AlertDescription, { children: "Your subscription will end immediately. You'll lose access to its entitlements right away." })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }), /* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
1043
1304
|
"You'll retain access until ",
|
|
1044
|
-
formatDate$
|
|
1305
|
+
formatDate$1(billingPeriodEnd),
|
|
1045
1306
|
". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
|
|
1046
1307
|
] })] })]
|
|
1047
1308
|
}),
|
|
@@ -1050,7 +1311,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
|
|
|
1050
1311
|
children: [/* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }), isImmediateCancel ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AlertTitle, { children: "You can subscribe again at any time" }), /* @__PURE__ */ jsx(AlertDescription, { children: "After canceling, you can return to the pricing page and start a new subscription whenever you're ready." })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AlertTitle, { children: "You can still resume before then" }), /* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
1051
1312
|
"If you change your mind you have until",
|
|
1052
1313
|
" ",
|
|
1053
|
-
formatDate$
|
|
1314
|
+
formatDate$1(billingPeriodEnd),
|
|
1054
1315
|
" to remove this cancellation from Manage subscription."
|
|
1055
1316
|
] })] })]
|
|
1056
1317
|
}),
|
|
@@ -1152,7 +1413,7 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1152
1413
|
className: "space-y-2",
|
|
1153
1414
|
children: [/* @__PURE__ */ jsxs("p", { children: [
|
|
1154
1415
|
"Your access stays in place until ",
|
|
1155
|
-
formatDate$
|
|
1416
|
+
formatDate$1(billingPeriodEnd),
|
|
1156
1417
|
" ",
|
|
1157
1418
|
"either way."
|
|
1158
1419
|
] }), /* @__PURE__ */ jsx("p", { children: "Confirming will remove the pending cancellation. Your subscription will remain active and continue to renew on your normal billing schedule, and charges will apply as usual." })]
|
|
@@ -1185,326 +1446,210 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1185
1446
|
});
|
|
1186
1447
|
};
|
|
1187
1448
|
//#endregion
|
|
1188
|
-
//#region src/pages/
|
|
1189
|
-
const
|
|
1190
|
-
const quotaKeys = /* @__PURE__ */ new Set();
|
|
1191
|
-
const featureKeys = /* @__PURE__ */ new Set();
|
|
1192
|
-
for (const phase of plan.phases) {
|
|
1193
|
-
const { quotas, features } = categorizeRateCards(phase.rateCards, {
|
|
1194
|
-
currency: plan.currency,
|
|
1195
|
-
units,
|
|
1196
|
-
planBillingCadence: plan.billingCadence
|
|
1197
|
-
});
|
|
1198
|
-
for (const q of quotas) quotaKeys.add(q.key);
|
|
1199
|
-
for (const f of features) featureKeys.add(f.key);
|
|
1200
|
-
}
|
|
1201
|
-
return {
|
|
1202
|
-
quotaKeys,
|
|
1203
|
-
featureKeys
|
|
1204
|
-
};
|
|
1205
|
-
};
|
|
1206
|
-
const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units) => {
|
|
1207
|
-
const isUpgrade = targetIndex > currentIndex;
|
|
1208
|
-
const currentPhase = currentPlan?.phases.at(-1);
|
|
1209
|
-
const targetPhase = targetPlan.phases.at(-1);
|
|
1210
|
-
const { quotas: currentQuotas, features: currentFeatures } = currentPhase ? categorizeRateCards(currentPhase.rateCards, {
|
|
1211
|
-
currency: currentPlan?.currency,
|
|
1212
|
-
units,
|
|
1213
|
-
planBillingCadence: currentPlan?.billingCadence
|
|
1214
|
-
}) : {
|
|
1215
|
-
quotas: [],
|
|
1216
|
-
features: []
|
|
1217
|
-
};
|
|
1218
|
-
const { quotas: targetQuotas, features: targetFeatures } = targetPhase ? categorizeRateCards(targetPhase.rateCards, {
|
|
1219
|
-
currency: targetPlan.currency,
|
|
1220
|
-
units,
|
|
1221
|
-
planBillingCadence: targetPlan.billingCadence
|
|
1222
|
-
}) : {
|
|
1223
|
-
quotas: [],
|
|
1224
|
-
features: []
|
|
1225
|
-
};
|
|
1226
|
-
const currentAllKeys = currentPlan ? getAllKeysAcrossPhases(currentPlan, units) : {
|
|
1227
|
-
quotaKeys: /* @__PURE__ */ new Set(),
|
|
1228
|
-
featureKeys: /* @__PURE__ */ new Set()
|
|
1229
|
-
};
|
|
1230
|
-
const targetAllKeys = getAllKeysAcrossPhases(targetPlan, units);
|
|
1231
|
-
const quotaChanges = [];
|
|
1232
|
-
const allQuotaKeys = new Set([...currentQuotas.map((q) => q.key), ...targetQuotas.map((q) => q.key)]);
|
|
1233
|
-
for (const key of allQuotaKeys) {
|
|
1234
|
-
const current = currentQuotas.find((q) => q.key === key);
|
|
1235
|
-
const target = targetQuotas.find((q) => q.key === key);
|
|
1236
|
-
if (current && target) {
|
|
1237
|
-
let change = "same";
|
|
1238
|
-
if (target.limit > current.limit) change = "increase";
|
|
1239
|
-
else if (target.limit < current.limit) change = "decrease";
|
|
1240
|
-
quotaChanges.push({
|
|
1241
|
-
key: key ?? "",
|
|
1242
|
-
name: target.name,
|
|
1243
|
-
currentValue: current.limit,
|
|
1244
|
-
newValue: target.limit,
|
|
1245
|
-
period: target.period,
|
|
1246
|
-
change
|
|
1247
|
-
});
|
|
1248
|
-
} else if (target && !current) {
|
|
1249
|
-
if (currentAllKeys.featureKeys.has(key)) {
|
|
1250
|
-
quotaChanges.push({
|
|
1251
|
-
key: key ?? "",
|
|
1252
|
-
name: target.name,
|
|
1253
|
-
currentValue: null,
|
|
1254
|
-
newValue: target.limit,
|
|
1255
|
-
period: target.period,
|
|
1256
|
-
change: "same"
|
|
1257
|
-
});
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
quotaChanges.push({
|
|
1261
|
-
key: key ?? "",
|
|
1262
|
-
name: target.name,
|
|
1263
|
-
currentValue: null,
|
|
1264
|
-
newValue: target.limit,
|
|
1265
|
-
period: target.period,
|
|
1266
|
-
change: "added"
|
|
1267
|
-
});
|
|
1268
|
-
} else if (current && !target) {
|
|
1269
|
-
if (targetAllKeys.featureKeys.has(key)) continue;
|
|
1270
|
-
quotaChanges.push({
|
|
1271
|
-
key: key ?? "",
|
|
1272
|
-
name: current.name,
|
|
1273
|
-
currentValue: current.limit,
|
|
1274
|
-
newValue: null,
|
|
1275
|
-
period: current.period,
|
|
1276
|
-
change: "removed"
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
const featureChanges = [];
|
|
1281
|
-
const allFeatureKeys = new Set([...currentFeatures.map((f) => f.key), ...targetFeatures.map((f) => f.key)]);
|
|
1282
|
-
for (const key of allFeatureKeys) {
|
|
1283
|
-
const current = currentFeatures.find((f) => f.key === key);
|
|
1284
|
-
const target = targetFeatures.find((f) => f.key === key);
|
|
1285
|
-
if (current && target) {
|
|
1286
|
-
let change = "same";
|
|
1287
|
-
if (current.value !== void 0 && target.value !== void 0 && current.value !== target.value) change = isUpgrade ? "upgraded" : "downgraded";
|
|
1288
|
-
featureChanges.push({
|
|
1289
|
-
key: key ?? "",
|
|
1290
|
-
name: target.name,
|
|
1291
|
-
currentValue: current.value ?? true,
|
|
1292
|
-
newValue: target.value ?? true,
|
|
1293
|
-
change
|
|
1294
|
-
});
|
|
1295
|
-
} else if (target && !current) {
|
|
1296
|
-
if (currentAllKeys.quotaKeys.has(key)) {
|
|
1297
|
-
featureChanges.push({
|
|
1298
|
-
key: key ?? "",
|
|
1299
|
-
name: target.name,
|
|
1300
|
-
currentValue: true,
|
|
1301
|
-
newValue: target.value ?? true,
|
|
1302
|
-
change: "same"
|
|
1303
|
-
});
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
featureChanges.push({
|
|
1307
|
-
key: key ?? "",
|
|
1308
|
-
name: target.name,
|
|
1309
|
-
currentValue: null,
|
|
1310
|
-
newValue: target.value ?? true,
|
|
1311
|
-
change: "added"
|
|
1312
|
-
});
|
|
1313
|
-
} else if (current && !target) {
|
|
1314
|
-
if (targetAllKeys.quotaKeys.has(key)) continue;
|
|
1315
|
-
featureChanges.push({
|
|
1316
|
-
key: key ?? "",
|
|
1317
|
-
name: current.name,
|
|
1318
|
-
currentValue: current.value ?? true,
|
|
1319
|
-
newValue: null,
|
|
1320
|
-
change: "removed"
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
return {
|
|
1325
|
-
plan: targetPlan,
|
|
1326
|
-
isUpgrade,
|
|
1327
|
-
isNewerVersion: false,
|
|
1328
|
-
quotaChanges,
|
|
1329
|
-
featureChanges
|
|
1330
|
-
};
|
|
1331
|
-
};
|
|
1332
|
-
const ChangeIndicator = ({ change }) => {
|
|
1333
|
-
if (change === "increase" || change === "added" || change === "upgraded") return /* @__PURE__ */ jsx(ArrowUpIcon, { className: "w-4 h-4 text-primary shrink-0" });
|
|
1334
|
-
if (change === "decrease" || change === "removed" || change === "downgraded") return /* @__PURE__ */ jsx(ArrowDownIcon, { className: "w-4 h-4 text-amber-600 shrink-0" });
|
|
1335
|
-
return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
|
|
1336
|
-
};
|
|
1337
|
-
const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
|
|
1338
|
-
const planVersion = (plan) => plan.version ?? 1;
|
|
1339
|
-
const isNewerPlanVersion = (subscribedPlan, target) => target.key === subscribedPlan.key && planVersion(target) > planVersion(subscribedPlan);
|
|
1340
|
-
/** Baseline for comparisons: catalog entry when present, else subscription plan. */
|
|
1341
|
-
const resolvePlanForComparison = (subscribedPlan, catalogItems) => catalogItems?.find((p) => p.id === subscribedPlan.id) ?? subscribedPlan;
|
|
1342
|
-
const resolveIsUpgrade = ({ target, targetIndex, subscribedPlan, currentIndex }) => {
|
|
1343
|
-
if (target.key === subscribedPlan.key) return planVersion(target) > planVersion(subscribedPlan);
|
|
1344
|
-
return targetIndex > currentIndex;
|
|
1345
|
-
};
|
|
1346
|
-
const modeLabelMap = {
|
|
1449
|
+
//#region src/pages/components/PlanChangeCard.tsx
|
|
1450
|
+
const MODE_LABEL = {
|
|
1347
1451
|
upgrade: "Upgrade",
|
|
1348
1452
|
downgrade: "Downgrade",
|
|
1349
1453
|
private: "Switch"
|
|
1350
1454
|
};
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1455
|
+
const ChangeRow = ({ change }) => {
|
|
1456
|
+
const arrow = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1457
|
+
className: "text-muted-foreground",
|
|
1458
|
+
children: change.currentValue
|
|
1459
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1460
|
+
className: "text-muted-foreground",
|
|
1461
|
+
children: "→"
|
|
1462
|
+
})] });
|
|
1463
|
+
let icon;
|
|
1464
|
+
let body;
|
|
1465
|
+
switch (change.change) {
|
|
1466
|
+
case "added":
|
|
1467
|
+
icon = /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 text-green-600 shrink-0 mt-0.5" });
|
|
1468
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1469
|
+
/* @__PURE__ */ jsx("span", {
|
|
1470
|
+
className: "font-medium",
|
|
1471
|
+
children: change.label
|
|
1472
|
+
}),
|
|
1473
|
+
change.targetValue && change.targetValue !== "Included" ? /* @__PURE__ */ jsxs("span", {
|
|
1474
|
+
className: "text-muted-foreground",
|
|
1475
|
+
children: [": ", change.targetValue]
|
|
1476
|
+
}) : null,
|
|
1477
|
+
/* @__PURE__ */ jsx("span", {
|
|
1478
|
+
className: "text-green-600",
|
|
1479
|
+
children: " — now included"
|
|
1480
|
+
})
|
|
1481
|
+
] });
|
|
1482
|
+
break;
|
|
1483
|
+
case "removed":
|
|
1484
|
+
icon = /* @__PURE__ */ jsx(XIcon, { className: "size-4 text-destructive shrink-0 mt-0.5" });
|
|
1485
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1486
|
+
className: "font-medium",
|
|
1487
|
+
children: change.label
|
|
1488
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1489
|
+
className: "text-destructive",
|
|
1490
|
+
children: " — no longer included"
|
|
1491
|
+
})] });
|
|
1492
|
+
break;
|
|
1493
|
+
case "increase":
|
|
1494
|
+
icon = /* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-4 text-primary shrink-0 mt-0.5" });
|
|
1495
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1496
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1497
|
+
className: "font-medium",
|
|
1498
|
+
children: [change.label, ":"]
|
|
1499
|
+
}),
|
|
1500
|
+
" ",
|
|
1501
|
+
arrow,
|
|
1502
|
+
/* @__PURE__ */ jsx("span", {
|
|
1503
|
+
className: "font-medium text-primary",
|
|
1504
|
+
children: change.targetValue
|
|
1505
|
+
})
|
|
1506
|
+
] });
|
|
1507
|
+
break;
|
|
1508
|
+
case "decrease":
|
|
1509
|
+
icon = /* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4 text-amber-600 shrink-0 mt-0.5" });
|
|
1510
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1511
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1512
|
+
className: "font-medium",
|
|
1513
|
+
children: [change.label, ":"]
|
|
1514
|
+
}),
|
|
1515
|
+
" ",
|
|
1516
|
+
arrow,
|
|
1517
|
+
/* @__PURE__ */ jsx("span", {
|
|
1518
|
+
className: "font-medium text-amber-600",
|
|
1519
|
+
children: change.targetValue
|
|
1520
|
+
})
|
|
1521
|
+
] });
|
|
1522
|
+
break;
|
|
1523
|
+
case "same":
|
|
1524
|
+
icon = /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 text-muted-foreground shrink-0 mt-0.5" });
|
|
1525
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { children: change.label }), change.targetValue && change.targetValue !== "Included" && /* @__PURE__ */ jsxs("span", {
|
|
1526
|
+
className: "text-muted-foreground",
|
|
1527
|
+
children: [": ", change.targetValue]
|
|
1528
|
+
})] });
|
|
1529
|
+
break;
|
|
1530
|
+
default:
|
|
1531
|
+
icon = /* @__PURE__ */ jsx(ArrowLeftRightIcon, { className: "size-4 text-muted-foreground shrink-0 mt-0.5" });
|
|
1532
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1533
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1534
|
+
className: "font-medium",
|
|
1535
|
+
children: [change.label, ":"]
|
|
1536
|
+
}),
|
|
1537
|
+
" ",
|
|
1538
|
+
change.currentValue !== change.targetValue && arrow,
|
|
1539
|
+
/* @__PURE__ */ jsx("span", {
|
|
1540
|
+
className: "font-medium",
|
|
1541
|
+
children: change.targetValue
|
|
1542
|
+
})
|
|
1543
|
+
] });
|
|
1544
|
+
}
|
|
1545
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1546
|
+
className: "flex items-start gap-2 text-sm",
|
|
1547
|
+
children: [icon, /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
1548
|
+
className: "flex flex-wrap items-baseline gap-1",
|
|
1549
|
+
children: body
|
|
1550
|
+
}), change.tierPrices && change.tierPrices.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1551
|
+
className: "text-xs text-muted-foreground mt-1 space-y-0.5",
|
|
1552
|
+
children: change.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
1553
|
+
})] })]
|
|
1554
|
+
});
|
|
1355
1555
|
};
|
|
1356
|
-
const
|
|
1357
|
-
const
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1556
|
+
const PlanChangeCard = ({ plan, mode, currentEntitlements, isNewerVersion, isSwitching, units, onSwitch }) => {
|
|
1557
|
+
const isCustom = isCustomPlan(plan);
|
|
1558
|
+
const priceLabel = formatPlanPrice(plan);
|
|
1559
|
+
const schedule = isCustom ? void 0 : getPlanPriceSchedule(plan);
|
|
1560
|
+
const phaseChangeGroups = useMemo(() => {
|
|
1561
|
+
const diff = (target) => {
|
|
1562
|
+
const changes = comparePlanEntitlements(currentEntitlements, target);
|
|
1563
|
+
return [...changes.filter((c) => c.change !== "removed"), ...changes.filter((c) => c.change === "removed")];
|
|
1564
|
+
};
|
|
1565
|
+
const sets = plan.phases.map((phase) => categorizeRateCards(phase.rateCards, {
|
|
1566
|
+
currency: plan.currency,
|
|
1567
|
+
units,
|
|
1568
|
+
planBillingCadence: plan.billingCadence
|
|
1569
|
+
}));
|
|
1570
|
+
return (plan.phases.length <= 1 || sets.every((set) => sameEntitlementSet(set, sets[0])) ? [{ changes: diff(sets.at(-1) ?? {
|
|
1571
|
+
quotas: [],
|
|
1572
|
+
features: []
|
|
1573
|
+
}) }] : plan.phases.flatMap((phase, idx) => {
|
|
1574
|
+
const set = sets[idx];
|
|
1575
|
+
if (set.quotas.length === 0 && set.features.length === 0) return [];
|
|
1576
|
+
return [{
|
|
1577
|
+
phase,
|
|
1578
|
+
changes: diff(set)
|
|
1579
|
+
}];
|
|
1580
|
+
})).filter((group) => group.changes.length > 0);
|
|
1581
|
+
}, [
|
|
1582
|
+
plan,
|
|
1583
|
+
currentEntitlements,
|
|
1584
|
+
units
|
|
1585
|
+
]);
|
|
1361
1586
|
return /* @__PURE__ */ jsxs("div", {
|
|
1362
1587
|
className: "border rounded-lg p-4",
|
|
1363
|
-
children: [
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
className: "flex items-baseline gap-2 flex-wrap",
|
|
1588
|
+
children: [
|
|
1589
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1590
|
+
className: "flex items-center justify-between gap-3 mb-2",
|
|
1367
1591
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1368
|
-
className: "flex items-
|
|
1369
|
-
children: [/* @__PURE__ */
|
|
1370
|
-
className: "
|
|
1371
|
-
children:
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1592
|
+
className: "flex items-baseline gap-2 flex-wrap",
|
|
1593
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1594
|
+
className: "flex items-center gap-2",
|
|
1595
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
1596
|
+
className: "font-semibold text-foreground",
|
|
1597
|
+
children: plan.name
|
|
1598
|
+
}), isNewerVersion && /* @__PURE__ */ jsx(Badge, {
|
|
1599
|
+
variant: "outline",
|
|
1600
|
+
className: "rounded-full border-primary/30 bg-primary/10 text-primary font-medium",
|
|
1601
|
+
children: "New version"
|
|
1602
|
+
})]
|
|
1603
|
+
}), isCustom ? /* @__PURE__ */ jsx("span", {
|
|
1604
|
+
className: "text-primary font-medium",
|
|
1605
|
+
children: "Custom"
|
|
1606
|
+
}) : !schedule && /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1607
|
+
label: priceLabel,
|
|
1608
|
+
currency: plan.currency,
|
|
1609
|
+
billingCadence: plan.billingCadence
|
|
1376
1610
|
})]
|
|
1377
|
-
}), isCustom ? /* @__PURE__ */ jsx(
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
"/",
|
|
1388
|
-
formatDuration(comparison.plan.billingCadence)
|
|
1389
|
-
]
|
|
1611
|
+
}), isCustom ? /* @__PURE__ */ jsx(Button$1, {
|
|
1612
|
+
variant: "default",
|
|
1613
|
+
size: "sm",
|
|
1614
|
+
children: "Contact Sales"
|
|
1615
|
+
}) : /* @__PURE__ */ jsx(Button$1, {
|
|
1616
|
+
variant: mode === "upgrade" ? "default" : "outline",
|
|
1617
|
+
onClick: onSwitch,
|
|
1618
|
+
size: "sm",
|
|
1619
|
+
disabled: isSwitching,
|
|
1620
|
+
children: MODE_LABEL[mode]
|
|
1390
1621
|
})]
|
|
1391
|
-
}),
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
children: [
|
|
1411
|
-
/* @__PURE__ */ jsx(ChangeIndicator, { change: quota.change }),
|
|
1412
|
-
/* @__PURE__ */ jsxs("span", {
|
|
1413
|
-
className: "font-medium",
|
|
1414
|
-
children: [quota.name, ":"]
|
|
1415
|
-
}),
|
|
1416
|
-
quota.change === "same" ? /* @__PURE__ */ jsxs("span", {
|
|
1417
|
-
className: "text-muted-foreground",
|
|
1418
|
-
children: [
|
|
1419
|
-
(quota.newValue ?? quota.currentValue)?.toLocaleString(),
|
|
1420
|
-
"/",
|
|
1421
|
-
quota.period
|
|
1422
|
-
]
|
|
1423
|
-
}) : quota.change === "added" ? /* @__PURE__ */ jsx("span", {
|
|
1424
|
-
className: "text-green-600",
|
|
1425
|
-
children: "Now included"
|
|
1426
|
-
}) : quota.change === "removed" ? /* @__PURE__ */ jsx("span", {
|
|
1427
|
-
className: "text-destructive",
|
|
1428
|
-
children: "No longer included"
|
|
1429
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1430
|
-
/* @__PURE__ */ jsxs("span", {
|
|
1431
|
-
className: "text-muted-foreground",
|
|
1432
|
-
children: [
|
|
1433
|
-
quota.currentValue?.toLocaleString(),
|
|
1434
|
-
"/",
|
|
1435
|
-
quota.period
|
|
1436
|
-
]
|
|
1437
|
-
}),
|
|
1438
|
-
/* @__PURE__ */ jsx("span", {
|
|
1439
|
-
className: "text-muted-foreground",
|
|
1440
|
-
children: "→"
|
|
1441
|
-
}),
|
|
1442
|
-
/* @__PURE__ */ jsxs("span", {
|
|
1443
|
-
className: cn("font-medium", quota.change === "increase" ? "text-primary" : "text-amber-600"),
|
|
1444
|
-
children: [
|
|
1445
|
-
quota.newValue?.toLocaleString(),
|
|
1446
|
-
"/",
|
|
1447
|
-
quota.period
|
|
1448
|
-
]
|
|
1449
|
-
})
|
|
1450
|
-
] })
|
|
1451
|
-
]
|
|
1452
|
-
}, quota.key)), comparison.featureChanges.map((feature) => /* @__PURE__ */ jsx("div", {
|
|
1453
|
-
className: "flex items-center gap-2 text-sm",
|
|
1454
|
-
children: feature.change === "same" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }), /* @__PURE__ */ jsxs("span", {
|
|
1455
|
-
className: "text-muted-foreground",
|
|
1456
|
-
children: [feature.name, typeof feature.newValue === "string" ? `: ${feature.newValue}` : typeof feature.currentValue === "string" ? `: ${feature.currentValue}` : ""]
|
|
1457
|
-
})] }) : feature.change === "added" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1458
|
-
/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" }),
|
|
1459
|
-
/* @__PURE__ */ jsx("span", {
|
|
1460
|
-
className: "text-muted-foreground font-medium",
|
|
1461
|
-
children: feature.name
|
|
1462
|
-
}),
|
|
1463
|
-
/* @__PURE__ */ jsx("span", {
|
|
1464
|
-
className: "text-green-600",
|
|
1465
|
-
children: "—"
|
|
1466
|
-
}),
|
|
1467
|
-
/* @__PURE__ */ jsx("span", {
|
|
1468
|
-
className: "text-green-600",
|
|
1469
|
-
children: "Now included"
|
|
1470
|
-
})
|
|
1471
|
-
] }) : feature.change === "removed" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1472
|
-
/* @__PURE__ */ jsx(XIcon, { className: "w-4 h-4 text-destructive shrink-0" }),
|
|
1473
|
-
/* @__PURE__ */ jsx("span", {
|
|
1474
|
-
className: "font-medium",
|
|
1475
|
-
children: feature.name
|
|
1476
|
-
}),
|
|
1477
|
-
/* @__PURE__ */ jsx("span", {
|
|
1478
|
-
className: "text-destructive",
|
|
1479
|
-
children: "—"
|
|
1480
|
-
}),
|
|
1481
|
-
/* @__PURE__ */ jsx("span", {
|
|
1482
|
-
className: "text-destructive",
|
|
1483
|
-
children: "No longer included"
|
|
1484
|
-
})
|
|
1485
|
-
] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1486
|
-
/* @__PURE__ */ jsx(ChangeIndicator, { change: feature.change }),
|
|
1487
|
-
/* @__PURE__ */ jsxs("span", {
|
|
1488
|
-
className: "",
|
|
1489
|
-
children: [feature.name, ":"]
|
|
1490
|
-
}),
|
|
1491
|
-
/* @__PURE__ */ jsx("span", {
|
|
1492
|
-
className: "text-muted-foreground",
|
|
1493
|
-
children: typeof feature.currentValue === "string" ? feature.currentValue : "Included"
|
|
1494
|
-
}),
|
|
1495
|
-
/* @__PURE__ */ jsx("span", {
|
|
1496
|
-
className: "text-muted-foreground",
|
|
1497
|
-
children: "→"
|
|
1498
|
-
}),
|
|
1499
|
-
/* @__PURE__ */ jsx("span", {
|
|
1500
|
-
className: cn(feature.change === "upgraded" ? "text-green-600" : "text-destructive"),
|
|
1501
|
-
children: typeof feature.newValue === "string" ? feature.newValue : "Included"
|
|
1502
|
-
})
|
|
1503
|
-
] })
|
|
1504
|
-
}, feature.key))]
|
|
1505
|
-
})]
|
|
1622
|
+
}),
|
|
1623
|
+
schedule && /* @__PURE__ */ jsx(PlanPriceSchedule, {
|
|
1624
|
+
schedule,
|
|
1625
|
+
currency: plan.currency,
|
|
1626
|
+
billingCadence: plan.billingCadence,
|
|
1627
|
+
className: "mb-2"
|
|
1628
|
+
}),
|
|
1629
|
+
phaseChangeGroups.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1630
|
+
className: "space-y-3",
|
|
1631
|
+
children: phaseChangeGroups.map((group, idx) => /* @__PURE__ */ jsxs("div", {
|
|
1632
|
+
className: "space-y-1.5",
|
|
1633
|
+
children: [group.phase && /* @__PURE__ */ jsx(PlanPhaseHeader, {
|
|
1634
|
+
phase: group.phase,
|
|
1635
|
+
currency: plan.currency,
|
|
1636
|
+
billingCadence: plan.billingCadence
|
|
1637
|
+
}), group.changes.map((change) => /* @__PURE__ */ jsx(ChangeRow, { change }, `${change.kind}:${change.key}`))]
|
|
1638
|
+
}, group.phase?.key ?? String(idx)))
|
|
1639
|
+
})
|
|
1640
|
+
]
|
|
1506
1641
|
});
|
|
1507
1642
|
};
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/pages/subscriptions/SwitchPlanModal.tsx
|
|
1645
|
+
const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
|
|
1646
|
+
const planVersion = (plan) => plan.version ?? 1;
|
|
1647
|
+
const isNewerPlanVersion = (subscribedPlan, target) => target.key === subscribedPlan.key && planVersion(target) > planVersion(subscribedPlan);
|
|
1648
|
+
const resolveIsUpgrade = ({ target, targetIndex, subscribedPlan, currentIndex }) => {
|
|
1649
|
+
if (target.key === subscribedPlan.key) return planVersion(target) > planVersion(subscribedPlan);
|
|
1650
|
+
return targetIndex >= currentIndex;
|
|
1651
|
+
};
|
|
1652
|
+
const isSwitchPlanTarget = (value) => typeof value === "object" && value !== null && "subscriptionId" in value && "plan" in value && "mode" in value;
|
|
1508
1653
|
const SwitchPlanModal = ({ subscription, children }) => {
|
|
1509
1654
|
const [open, setOpen] = useState(false);
|
|
1510
1655
|
const { data: plansData } = usePlans();
|
|
@@ -1539,6 +1684,27 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1539
1684
|
}
|
|
1540
1685
|
});
|
|
1541
1686
|
const subscribedPlan = subscription.plan;
|
|
1687
|
+
const currentEntitlements = useMemo(() => {
|
|
1688
|
+
const currency = subscription.currency ?? subscribedPlan.currency;
|
|
1689
|
+
const activePhase = getActivePhase(subscription);
|
|
1690
|
+
if (activePhase && (activePhase.items?.length ?? 0) > 0) return categorizeSubscriptionItems(activePhase.items, {
|
|
1691
|
+
currency,
|
|
1692
|
+
units: pricing?.units
|
|
1693
|
+
});
|
|
1694
|
+
const lastPhase = subscribedPlan.phases?.at(-1);
|
|
1695
|
+
return lastPhase ? categorizeRateCards(lastPhase.rateCards, {
|
|
1696
|
+
currency,
|
|
1697
|
+
units: pricing?.units,
|
|
1698
|
+
planBillingCadence: subscribedPlan.billingCadence
|
|
1699
|
+
}) : {
|
|
1700
|
+
quotas: [],
|
|
1701
|
+
features: []
|
|
1702
|
+
};
|
|
1703
|
+
}, [
|
|
1704
|
+
subscription,
|
|
1705
|
+
subscribedPlan,
|
|
1706
|
+
pricing?.units
|
|
1707
|
+
]);
|
|
1542
1708
|
const { upgrades, downgrades, privatePlans } = useMemo(() => {
|
|
1543
1709
|
const catalogItems = plansData?.items;
|
|
1544
1710
|
if (!catalogItems?.length) return {
|
|
@@ -1546,13 +1712,12 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1546
1712
|
downgrades: [],
|
|
1547
1713
|
privatePlans: []
|
|
1548
1714
|
};
|
|
1549
|
-
const planForComparison = resolvePlanForComparison(subscribedPlan, catalogItems);
|
|
1550
1715
|
const currentIndex = catalogItems.some((p) => p.id === subscribedPlan.id) ? catalogItems.findIndex((p) => p.id === subscribedPlan.id) : -1;
|
|
1551
1716
|
const subscribedIsPrivate = isPrivatePlan(subscribedPlan);
|
|
1552
|
-
const
|
|
1717
|
+
const entries = catalogItems.flatMap((plan, targetIndex) => {
|
|
1553
1718
|
if (plan.id === subscribedPlan.id) return [];
|
|
1554
1719
|
return [{
|
|
1555
|
-
|
|
1720
|
+
plan,
|
|
1556
1721
|
isUpgrade: resolveIsUpgrade({
|
|
1557
1722
|
target: plan,
|
|
1558
1723
|
targetIndex,
|
|
@@ -1563,20 +1728,32 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1563
1728
|
}];
|
|
1564
1729
|
});
|
|
1565
1730
|
if (subscribedIsPrivate) return {
|
|
1566
|
-
upgrades:
|
|
1731
|
+
upgrades: entries.filter((c) => !isPrivatePlan(c.plan)),
|
|
1567
1732
|
downgrades: [],
|
|
1568
|
-
privatePlans:
|
|
1733
|
+
privatePlans: entries.filter((c) => isPrivatePlan(c.plan))
|
|
1569
1734
|
};
|
|
1570
1735
|
return {
|
|
1571
|
-
upgrades:
|
|
1572
|
-
downgrades:
|
|
1573
|
-
privatePlans:
|
|
1736
|
+
upgrades: entries.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1737
|
+
downgrades: entries.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1738
|
+
privatePlans: entries.filter((c) => isPrivatePlan(c.plan))
|
|
1574
1739
|
};
|
|
1575
|
-
}, [
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1740
|
+
}, [plansData?.items, subscribedPlan]);
|
|
1741
|
+
const renderCards = (entries, mode) => /* @__PURE__ */ jsx("div", {
|
|
1742
|
+
className: "space-y-3",
|
|
1743
|
+
children: entries.map(({ plan, isNewerVersion }) => /* @__PURE__ */ jsx(PlanChangeCard, {
|
|
1744
|
+
plan,
|
|
1745
|
+
mode,
|
|
1746
|
+
currentEntitlements,
|
|
1747
|
+
isNewerVersion,
|
|
1748
|
+
isSwitching: switchPlanMutation.isPending,
|
|
1749
|
+
units: pricing?.units,
|
|
1750
|
+
onSwitch: () => switchPlanMutation.mutate({
|
|
1751
|
+
subscriptionId: subscription.id,
|
|
1752
|
+
plan,
|
|
1753
|
+
mode
|
|
1754
|
+
})
|
|
1755
|
+
}, plan.id))
|
|
1756
|
+
});
|
|
1580
1757
|
return /* @__PURE__ */ jsxs(Dialog, {
|
|
1581
1758
|
open,
|
|
1582
1759
|
onOpenChange: setOpen,
|
|
@@ -1605,12 +1782,9 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1605
1782
|
children: switchPlanMutation.error.message
|
|
1606
1783
|
})
|
|
1607
1784
|
}),
|
|
1608
|
-
/* @__PURE__ */ jsx(
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
className: "text-lg font-bold",
|
|
1612
|
-
children: subscribedPlan.name
|
|
1613
|
-
})] })
|
|
1785
|
+
/* @__PURE__ */ jsx(CurrentPlanBaseline, {
|
|
1786
|
+
subscription,
|
|
1787
|
+
units: pricing?.units
|
|
1614
1788
|
}),
|
|
1615
1789
|
upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1616
1790
|
className: "flex items-center justify-between mb-3",
|
|
@@ -1624,16 +1798,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1624
1798
|
className: "text-sm text-muted-foreground",
|
|
1625
1799
|
children: "Takes effect immediately"
|
|
1626
1800
|
})]
|
|
1627
|
-
}),
|
|
1628
|
-
className: "space-y-3",
|
|
1629
|
-
children: upgrades.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
|
|
1630
|
-
comparison,
|
|
1631
|
-
subscriptionId: subscription.id,
|
|
1632
|
-
mode: "upgrade",
|
|
1633
|
-
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1634
|
-
isSwitching: switchPlanMutation.isPending
|
|
1635
|
-
}, comparison.plan.id))
|
|
1636
|
-
})] }),
|
|
1801
|
+
}), renderCards(upgrades, "upgrade")] }),
|
|
1637
1802
|
downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1638
1803
|
className: "flex items-center justify-between mb-3",
|
|
1639
1804
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1644,18 +1809,9 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1644
1809
|
})]
|
|
1645
1810
|
}), /* @__PURE__ */ jsx("span", {
|
|
1646
1811
|
className: "text-sm text-muted-foreground",
|
|
1647
|
-
children: "Takes effect next billing cycle"
|
|
1812
|
+
children: "Takes effect at your next billing cycle"
|
|
1648
1813
|
})]
|
|
1649
|
-
}),
|
|
1650
|
-
className: "space-y-3",
|
|
1651
|
-
children: downgrades.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
|
|
1652
|
-
comparison,
|
|
1653
|
-
subscriptionId: subscription.id,
|
|
1654
|
-
mode: "downgrade",
|
|
1655
|
-
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1656
|
-
isSwitching: switchPlanMutation.isPending
|
|
1657
|
-
}, comparison.plan.id))
|
|
1658
|
-
})] }),
|
|
1814
|
+
}), renderCards(downgrades, "downgrade")] }),
|
|
1659
1815
|
privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1660
1816
|
className: "flex items-center justify-between mb-3",
|
|
1661
1817
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1668,16 +1824,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1668
1824
|
className: "text-sm text-muted-foreground",
|
|
1669
1825
|
children: "Takes effect immediately"
|
|
1670
1826
|
})]
|
|
1671
|
-
}),
|
|
1672
|
-
className: "space-y-3",
|
|
1673
|
-
children: privatePlans.map((comparison) => /* @__PURE__ */ jsx(PlanComparisonItem, {
|
|
1674
|
-
comparison,
|
|
1675
|
-
subscriptionId: subscription.id,
|
|
1676
|
-
mode: "private",
|
|
1677
|
-
onRequestChange: (target) => switchPlanMutation.mutate(target),
|
|
1678
|
-
isSwitching: switchPlanMutation.isPending
|
|
1679
|
-
}, comparison.plan.id))
|
|
1680
|
-
})] })
|
|
1827
|
+
}), renderCards(privatePlans, "private")] })
|
|
1681
1828
|
]
|
|
1682
1829
|
})]
|
|
1683
1830
|
}) })]
|
|
@@ -1733,7 +1880,7 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1733
1880
|
variant: "outline",
|
|
1734
1881
|
size: "sm",
|
|
1735
1882
|
asChild: true,
|
|
1736
|
-
children: /* @__PURE__ */ jsxs(Link, {
|
|
1883
|
+
children: /* @__PURE__ */ jsxs(Link$1, {
|
|
1737
1884
|
to: "/pricing",
|
|
1738
1885
|
children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
|
|
1739
1886
|
})
|
|
@@ -1755,7 +1902,7 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1755
1902
|
asChild: true,
|
|
1756
1903
|
size: "sm",
|
|
1757
1904
|
variant: "secondary",
|
|
1758
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1905
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1759
1906
|
to: "/manage-payment",
|
|
1760
1907
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1761
1908
|
className: "flex items-center gap-2",
|
|
@@ -1780,140 +1927,14 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1780
1927
|
//#region src/pages/subscriptions/SubscriptionPlanDetails.tsx
|
|
1781
1928
|
const detailLabelClassName = "text-sm font-semibold tracking-wide mb-1";
|
|
1782
1929
|
const sectionLabelClassName = "text-base font-semibold tracking-wide mb-3 mt-2";
|
|
1783
|
-
const
|
|
1784
|
-
return new Date(dateString).toLocaleDateString("en-US", {
|
|
1785
|
-
month: "short",
|
|
1786
|
-
day: "numeric",
|
|
1787
|
-
year: "numeric"
|
|
1788
|
-
});
|
|
1789
|
-
};
|
|
1790
|
-
const formatDateRange = (from, to) => `${formatDate$1(from)} – ${formatDate$1(to)}`;
|
|
1791
|
-
const formatNumber = (value) => value.toLocaleString("en-US");
|
|
1792
|
-
const getTierPricesFromItem = (item, currency, units) => {
|
|
1793
|
-
if (item.price?.type !== "tiered") return;
|
|
1794
|
-
const tiers = item.price.tiers;
|
|
1795
|
-
if (!tiers || tiers.length <= 1) return;
|
|
1796
|
-
const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
|
|
1797
|
-
return formatTieredPriceBreakdown({
|
|
1798
|
-
tiers: tiers.map((t) => ({
|
|
1799
|
-
upToAmount: t.upToAmount,
|
|
1800
|
-
unitPriceAmount: t.unitPrice?.amount,
|
|
1801
|
-
flatPriceAmount: t.flatPrice?.amount
|
|
1802
|
-
})),
|
|
1803
|
-
currency,
|
|
1804
|
-
unitLabel,
|
|
1805
|
-
includedLabel: "Included"
|
|
1806
|
-
});
|
|
1807
|
-
};
|
|
1808
|
-
const hasPricedFirstTier = (item) => {
|
|
1809
|
-
const firstTier = item.price?.tiers?.[0];
|
|
1810
|
-
if (!firstTier) return false;
|
|
1811
|
-
const flat = parseFloat(firstTier.flatPrice?.amount ?? "0");
|
|
1812
|
-
const unit = parseFloat(firstTier.unitPrice?.amount ?? "0");
|
|
1813
|
-
return flat > 0 || unit > 0;
|
|
1814
|
-
};
|
|
1815
|
-
const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
|
|
1816
|
-
const features = [];
|
|
1817
|
-
for (const item of items) {
|
|
1818
|
-
const entitlement = item.included?.entitlement;
|
|
1819
|
-
if (!entitlement) continue;
|
|
1820
|
-
if (entitlement.type === "metered" && entitlement.issueAfterReset != null) {
|
|
1821
|
-
const cadence = entitlement.usagePeriod?.intervalISO ?? item.billingCadence ?? fallbackBillingCadence;
|
|
1822
|
-
const tierPrices = getTierPricesFromItem(item, currency, units);
|
|
1823
|
-
const suppressLimit = hasPricedFirstTier(item) && !!tierPrices && tierPrices.length > 0;
|
|
1824
|
-
features.push({
|
|
1825
|
-
entitlementType: "metered",
|
|
1826
|
-
key: item.featureKey ?? item.key,
|
|
1827
|
-
name: item.name ?? item.featureKey ?? item.key,
|
|
1828
|
-
limit: suppressLimit ? void 0 : entitlement.issueAfterReset,
|
|
1829
|
-
period: suppressLimit ? void 0 : cadence ? formatDuration(cadence) : "month",
|
|
1830
|
-
tierPrices
|
|
1831
|
-
});
|
|
1832
|
-
continue;
|
|
1833
|
-
}
|
|
1834
|
-
if (entitlement.type === "boolean") {
|
|
1835
|
-
features.push({
|
|
1836
|
-
entitlementType: "boolean",
|
|
1837
|
-
key: item.featureKey ?? item.key,
|
|
1838
|
-
name: item.name ?? item.featureKey ?? item.key
|
|
1839
|
-
});
|
|
1840
|
-
continue;
|
|
1841
|
-
}
|
|
1842
|
-
if (entitlement.type === "static") {
|
|
1843
|
-
const base = {
|
|
1844
|
-
key: item.featureKey ?? item.key,
|
|
1845
|
-
name: item.name ?? item.featureKey ?? item.key
|
|
1846
|
-
};
|
|
1847
|
-
if (!entitlement.config) {
|
|
1848
|
-
features.push({
|
|
1849
|
-
entitlementType: "static",
|
|
1850
|
-
...base
|
|
1851
|
-
});
|
|
1852
|
-
continue;
|
|
1853
|
-
}
|
|
1854
|
-
features.push({
|
|
1855
|
-
entitlementType: "static",
|
|
1856
|
-
...base,
|
|
1857
|
-
value: formatStaticEntitlementConfig(entitlement.config)
|
|
1858
|
-
});
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
return { features };
|
|
1862
|
-
};
|
|
1863
|
-
const getPhaseRows = (opts) => {
|
|
1864
|
-
const { subscription, currency, units } = opts;
|
|
1865
|
-
const phases = [...subscription.phases].sort((a, b) => new Date(a.activeFrom).getTime() - new Date(b.activeFrom).getTime());
|
|
1866
|
-
const phaseGroups = [];
|
|
1867
|
-
for (const phase of phases) {
|
|
1868
|
-
const { features } = getEntitlementsFromItems(phase.items ?? [], currency, units, subscription.billingCadence);
|
|
1869
|
-
const rows = [];
|
|
1870
|
-
for (const f of features) rows.push({
|
|
1871
|
-
key: f.key,
|
|
1872
|
-
name: f.name,
|
|
1873
|
-
entitlementType: f.entitlementType,
|
|
1874
|
-
limit: f.entitlementType === "metered" ? f.limit : void 0,
|
|
1875
|
-
period: f.entitlementType === "metered" ? f.period : void 0,
|
|
1876
|
-
tierPrices: f.entitlementType === "metered" ? f.tierPrices : void 0,
|
|
1877
|
-
value: f.entitlementType === "static" ? f.value : void 0,
|
|
1878
|
-
phaseId: phase.id,
|
|
1879
|
-
activeFrom: phase.activeFrom,
|
|
1880
|
-
activeTo: phase.activeTo
|
|
1881
|
-
});
|
|
1882
|
-
if (rows.length > 0) phaseGroups.push({
|
|
1883
|
-
id: phase.id,
|
|
1884
|
-
name: phase.name,
|
|
1885
|
-
activeFrom: phase.activeFrom,
|
|
1886
|
-
activeTo: phase.activeTo,
|
|
1887
|
-
rows
|
|
1888
|
-
});
|
|
1889
|
-
}
|
|
1890
|
-
return { phaseGroups };
|
|
1891
|
-
};
|
|
1892
|
-
const formatActiveRange = (activeFrom, activeTo) => {
|
|
1893
|
-
if (!activeTo) return `Starts ${formatDate$1(activeFrom)}`;
|
|
1894
|
-
return `${formatDate$1(activeFrom)} – ${formatDate$1(activeTo)}`;
|
|
1895
|
-
};
|
|
1930
|
+
const formatDateTimeRange = (from, to) => `${formatDateTime(from)} – ${formatDateTime(to)}`;
|
|
1896
1931
|
const SubscriptionPlanDetails = ({ subscription }) => {
|
|
1897
1932
|
const { pricing } = useMonetizationConfig();
|
|
1898
1933
|
const plan = subscription.plan;
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
1934
|
+
const view = getSubscriptionPlanView(subscription, { units: pricing?.units });
|
|
1935
|
+
const { priceLabel } = view;
|
|
1901
1936
|
const taxLegendSentence = planHasDefaultTaxBehavior(plan) ? subscriptionTaxLegendSentence(plan.defaultTaxConfig?.behavior ?? "") : void 0;
|
|
1902
|
-
const
|
|
1903
|
-
className: "text-primary font-medium",
|
|
1904
|
-
children: "Free"
|
|
1905
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1906
|
-
className: "text-primary font-medium text-lg",
|
|
1907
|
-
children: formatPrice(priceInfo.monthly, currency)
|
|
1908
|
-
}), /* @__PURE__ */ jsxs("span", {
|
|
1909
|
-
className: "text-muted-foreground",
|
|
1910
|
-
children: [" / ", formatDuration(plan.billingCadence)]
|
|
1911
|
-
})] });
|
|
1912
|
-
const { phaseGroups } = getPhaseRows({
|
|
1913
|
-
subscription,
|
|
1914
|
-
currency,
|
|
1915
|
-
units: pricing?.units
|
|
1916
|
-
});
|
|
1937
|
+
const hasEntitlements = hasSubscriptionEntitlements(view);
|
|
1917
1938
|
return /* @__PURE__ */ jsxs("div", {
|
|
1918
1939
|
className: "space-y-4",
|
|
1919
1940
|
children: [/* @__PURE__ */ jsx(Heading, {
|
|
@@ -1939,14 +1960,19 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
1939
1960
|
children: "Active since"
|
|
1940
1961
|
}), /* @__PURE__ */ jsx("dd", {
|
|
1941
1962
|
className: "text-foreground",
|
|
1942
|
-
children:
|
|
1963
|
+
children: formatDateTime(subscription.activeFrom)
|
|
1943
1964
|
})] }),
|
|
1944
1965
|
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
1945
1966
|
className: detailLabelClassName,
|
|
1946
1967
|
children: "Price"
|
|
1947
1968
|
}), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("div", {
|
|
1948
1969
|
className: "flex flex-wrap items-baseline gap-1",
|
|
1949
|
-
children:
|
|
1970
|
+
children: /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1971
|
+
label: priceLabel,
|
|
1972
|
+
currency: view.currency,
|
|
1973
|
+
billingCadence: view.billingCadence,
|
|
1974
|
+
description: true
|
|
1975
|
+
})
|
|
1950
1976
|
}), taxLegendSentence ? /* @__PURE__ */ jsx("p", {
|
|
1951
1977
|
className: "text-xs text-muted-foreground mt-1",
|
|
1952
1978
|
children: taxLegendSentence
|
|
@@ -1956,53 +1982,20 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
1956
1982
|
children: "Current period"
|
|
1957
1983
|
}), /* @__PURE__ */ jsx("dd", {
|
|
1958
1984
|
className: "text-foreground",
|
|
1959
|
-
children: subscription.alignment?.currentAlignedBillingPeriod ?
|
|
1985
|
+
children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateTimeRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
|
|
1960
1986
|
})] })
|
|
1961
1987
|
]
|
|
1962
|
-
}),
|
|
1963
|
-
className: "space-y-
|
|
1964
|
-
children: /* @__PURE__ */
|
|
1965
|
-
className:
|
|
1966
|
-
children:
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
children: [phaseGroups.length > 1 ? /* @__PURE__ */ jsxs("div", {
|
|
1974
|
-
className: "text-sm font-medium text-card-foreground",
|
|
1975
|
-
children: [phase.name, /* @__PURE__ */ jsxs("span", {
|
|
1976
|
-
className: "text-muted-foreground font-normal",
|
|
1977
|
-
children: [
|
|
1978
|
-
" ",
|
|
1979
|
-
"—",
|
|
1980
|
-
" ",
|
|
1981
|
-
formatActiveRange(phase.activeFrom, phase.activeTo)
|
|
1982
|
-
]
|
|
1983
|
-
})]
|
|
1984
|
-
}) : null, /* @__PURE__ */ jsx("ul", {
|
|
1985
|
-
className: "space-y-3",
|
|
1986
|
-
children: phase.rows.map((row) => /* @__PURE__ */ jsx("li", {
|
|
1987
|
-
className: "text-sm",
|
|
1988
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1989
|
-
className: "flex flex-col gap-1",
|
|
1990
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1991
|
-
className: "text-foreground font-medium",
|
|
1992
|
-
children: [row.name, row.entitlementType === "static" && row.value !== void 0 ? `: ${row.value}` : ""]
|
|
1993
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1994
|
-
className: "text-muted-foreground",
|
|
1995
|
-
children: row.entitlementType === "metered" && (row.limit != null || row.tierPrices && row.tierPrices.length > 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [row.limit != null && (!row.tierPrices || row.tierPrices.length === 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [formatNumber(row.limit), row.period ? ` / ${row.period}` : ""] }) : null, row.tierPrices && row.tierPrices.length > 0 ? /* @__PURE__ */ jsx("ul", {
|
|
1996
|
-
className: "text-xs space-y-0.5",
|
|
1997
|
-
children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
1998
|
-
}) : null] }) : row.entitlementType === "static" && row.value !== void 0 ? null : "Included"
|
|
1999
|
-
})]
|
|
2000
|
-
})
|
|
2001
|
-
}, `${row.key}:${row.phaseId}`))
|
|
2002
|
-
})]
|
|
2003
|
-
}, phase.id))
|
|
2004
|
-
})]
|
|
2005
|
-
})
|
|
1988
|
+
}), hasEntitlements ? /* @__PURE__ */ jsxs("div", {
|
|
1989
|
+
className: "space-y-2 pt-2 border-t border-border",
|
|
1990
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1991
|
+
className: sectionLabelClassName,
|
|
1992
|
+
children: "What's included"
|
|
1993
|
+
}), /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
1994
|
+
view,
|
|
1995
|
+
currency: view.currency,
|
|
1996
|
+
billingCadence: view.billingCadence,
|
|
1997
|
+
units: pricing?.units
|
|
1998
|
+
})]
|
|
2006
1999
|
}) : null]
|
|
2007
2000
|
})] })]
|
|
2008
2001
|
});
|
|
@@ -2070,11 +2063,7 @@ const UsageItem = ({ meter, item, subscription, featureKey }) => {
|
|
|
2070
2063
|
}) })
|
|
2071
2064
|
]
|
|
2072
2065
|
}),
|
|
2073
|
-
/* @__PURE__ */
|
|
2074
|
-
item?.name ?? featureKey,
|
|
2075
|
-
" ",
|
|
2076
|
-
item?.price?.amount
|
|
2077
|
-
] })
|
|
2066
|
+
/* @__PURE__ */ jsx(CardTitle, { children: item?.name ?? featureKey })
|
|
2078
2067
|
] }), /* @__PURE__ */ jsxs(CardContent, {
|
|
2079
2068
|
className: "space-y-2",
|
|
2080
2069
|
children: [
|
|
@@ -2142,7 +2131,7 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
|
|
|
2142
2131
|
variant: "destructive",
|
|
2143
2132
|
size: "xs",
|
|
2144
2133
|
asChild: true,
|
|
2145
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
2134
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2146
2135
|
to: "/manage-payment",
|
|
2147
2136
|
target: "_blank",
|
|
2148
2137
|
children: "Manage billing"
|
|
@@ -2254,7 +2243,7 @@ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
|
|
|
2254
2243
|
};
|
|
2255
2244
|
const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
|
|
2256
2245
|
const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
|
|
2257
|
-
return /* @__PURE__ */ jsx(Link
|
|
2246
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
2258
2247
|
to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
|
|
2259
2248
|
children: /* @__PURE__ */ jsx(Item, {
|
|
2260
2249
|
size: "sm",
|