@zuplo/zudoku-plugin-monetization 0.0.41 → 0.0.42
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-BlcXx4-5.mjs} +149 -130
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +903 -824
- package/dist/pricing-ui.d.mts +61 -32
- 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 { S as formatDurationInterval, a as planHasDefaultTaxBehavior, b as formatDuration, c as formatPlanPrice, d as PlanEntitlements, f as EntitlementList, h as categorizeRateCards, o as subscriptionTaxLegendSentence, r as isCustomPlan, t as PricingTable, u as PlanPriceTag, v as formatMinorCurrencyAmount, x as formatDurationAdjective, y as formatPrice } from "./PricingTable-BlcXx4-5.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,132 @@ 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.
|
|
260
|
+
*
|
|
261
|
+
* The price is derived from the plan's rate cards via {@link formatPlanPrice}
|
|
262
|
+
* and rendered in the plan's own billing cadence, so it stays correct for any
|
|
263
|
+
* cadence (e.g. `$2.99/hour`).
|
|
264
|
+
*/
|
|
265
|
+
const PlanSummaryCard = ({ plan, descriptionFallback, taxAmount, taxLabel, taxInclusive, units, entitlementsItemClassName }) => {
|
|
266
|
+
const priceLabel = formatPlanPrice(plan);
|
|
267
|
+
const billingCycle = plan.billingCadence ? formatDuration(plan.billingCadence) : null;
|
|
268
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
269
|
+
className: "bg-muted/50",
|
|
270
|
+
children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
|
|
271
|
+
className: "flex justify-between items-start",
|
|
272
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
273
|
+
className: "flex items-center gap-3",
|
|
274
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
275
|
+
className: "flex flex-col text-2xl font-bold bg-primary text-primary-foreground items-center justify-center rounded size-12",
|
|
276
|
+
children: plan.name.at(0)?.toUpperCase()
|
|
277
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
278
|
+
className: "flex flex-col",
|
|
279
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
280
|
+
className: "text-lg font-bold",
|
|
281
|
+
children: plan.name
|
|
282
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
283
|
+
className: "text-sm font-normal text-muted-foreground",
|
|
284
|
+
children: plan.description || descriptionFallback
|
|
285
|
+
})]
|
|
286
|
+
})]
|
|
287
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
288
|
+
className: "text-right",
|
|
289
|
+
children: [/* @__PURE__ */ jsx(PlanPriceTag, {
|
|
290
|
+
label: priceLabel,
|
|
291
|
+
currency: plan.currency,
|
|
292
|
+
size: "lg",
|
|
293
|
+
description: true
|
|
294
|
+
}), priceLabel.type === "priced" && /* @__PURE__ */ jsxs(Fragment$1, { children: [taxAmount != null && /* @__PURE__ */ jsx("div", {
|
|
295
|
+
className: "text-sm font-normal mt-1",
|
|
296
|
+
children: taxInclusive ? `${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel} included` : `+ ${formatMinorCurrencyAmount(taxAmount, plan.currency)} ${taxLabel}`
|
|
297
|
+
}), billingCycle && /* @__PURE__ */ jsxs("div", {
|
|
298
|
+
className: "text-sm text-muted-foreground font-normal",
|
|
299
|
+
children: ["Billed ", formatBillingCycle(billingCycle)]
|
|
300
|
+
})] })]
|
|
301
|
+
})]
|
|
302
|
+
}) }), /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
303
|
+
/* @__PURE__ */ jsx(Separator, {}),
|
|
304
|
+
/* @__PURE__ */ jsx("div", {
|
|
305
|
+
className: "text-sm font-medium mb-3 mt-3",
|
|
306
|
+
children: "What's included:"
|
|
307
|
+
}),
|
|
308
|
+
/* @__PURE__ */ jsx(PlanEntitlements, {
|
|
309
|
+
phases: plan.phases,
|
|
310
|
+
currency: plan.currency,
|
|
311
|
+
billingCadence: plan.billingCadence,
|
|
312
|
+
units,
|
|
313
|
+
itemClassName: entitlementsItemClassName
|
|
314
|
+
})
|
|
315
|
+
] })]
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/pages/CheckoutConfirmPage.tsx
|
|
320
|
+
const CheckoutConfirmPage = () => {
|
|
321
|
+
const [search] = useSearchParams();
|
|
322
|
+
const planId = search.get("planId");
|
|
323
|
+
const { pricing } = useMonetizationConfig();
|
|
324
|
+
if (!planId) throw new Error("Parameter `planId` missing");
|
|
325
|
+
const { selectedPlan, taxAmount, taxLabel, taxInclusive } = usePurchaseSummary(planId);
|
|
326
|
+
const createSubscriptionMutation = useSubscriptionConfirmMutation({
|
|
327
|
+
endpoint: "subscriptions",
|
|
328
|
+
planId
|
|
329
|
+
});
|
|
330
|
+
return /* @__PURE__ */ jsx(ConfirmationScreen, {
|
|
331
|
+
title: "Review your subscription",
|
|
332
|
+
message: /* @__PURE__ */ jsx("p", {
|
|
333
|
+
className: "text-muted-foreground text-base",
|
|
334
|
+
children: "Please confirm the details below before completing your purchase."
|
|
335
|
+
}),
|
|
336
|
+
errorMessage: createSubscriptionMutation.isError ? createSubscriptionMutation.error.message : void 0,
|
|
337
|
+
confirmLabel: "Confirm & Subscribe",
|
|
338
|
+
pendingLabel: "Processing Payment...",
|
|
339
|
+
onConfirm: () => createSubscriptionMutation.mutate(),
|
|
340
|
+
isPending: createSubscriptionMutation.isPending,
|
|
341
|
+
confirmDisabled: !selectedPlan,
|
|
342
|
+
cancelTo: "/pricing",
|
|
343
|
+
termsNote: "By confirming, you agree to our Terms of Service and Privacy Policy. You can cancel anytime.",
|
|
344
|
+
footer: /* @__PURE__ */ jsxs("div", {
|
|
345
|
+
className: "flex items-center gap-2 text-muted-foreground text-xs item-center justify-center pt-4",
|
|
346
|
+
children: [/* @__PURE__ */ jsx(LockIcon, { className: "size-3" }), "Your payment is secured by Stripe"]
|
|
347
|
+
}),
|
|
348
|
+
children: selectedPlan && /* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
349
|
+
plan: selectedPlan,
|
|
350
|
+
descriptionFallback: "Selected plan",
|
|
351
|
+
taxAmount,
|
|
352
|
+
taxLabel,
|
|
353
|
+
taxInclusive,
|
|
354
|
+
units: pricing?.units,
|
|
355
|
+
entitlementsItemClassName: "text-muted-foreground"
|
|
356
|
+
})
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
//#endregion
|
|
296
360
|
//#region src/components/RedirectPage.tsx
|
|
297
361
|
const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
|
|
298
362
|
useEffect(() => {
|
|
@@ -391,7 +455,7 @@ const CheckoutPage = () => {
|
|
|
391
455
|
variant: "outline",
|
|
392
456
|
size: "xs",
|
|
393
457
|
asChild: true,
|
|
394
|
-
children: /* @__PURE__ */ jsx(Link
|
|
458
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
395
459
|
to: "/subscriptions",
|
|
396
460
|
children: "Back"
|
|
397
461
|
})
|
|
@@ -433,7 +497,7 @@ const ManagePaymentPage = () => {
|
|
|
433
497
|
variant: "outline",
|
|
434
498
|
size: "xs",
|
|
435
499
|
asChild: true,
|
|
436
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
500
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
437
501
|
to: "/subscriptions",
|
|
438
502
|
children: "Back"
|
|
439
503
|
})
|
|
@@ -494,19 +558,18 @@ const PricingPage = () => {
|
|
|
494
558
|
}),
|
|
495
559
|
/* @__PURE__ */ jsx(PricingTable, {
|
|
496
560
|
plans: pricingTable.items,
|
|
497
|
-
showYearlyPrice: pricing?.showYearlyPrice !== false,
|
|
498
561
|
units: pricing?.units,
|
|
499
562
|
renderAction: (plan, isPopular) => isSubscribed ? /* @__PURE__ */ jsx(Button, {
|
|
500
563
|
variant: isPopular ? "default" : "outline",
|
|
501
564
|
asChild: true,
|
|
502
|
-
children: /* @__PURE__ */ jsx(Link
|
|
565
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
503
566
|
to: `/subscriptions#manage`,
|
|
504
567
|
children: "Manage Subscriptions"
|
|
505
568
|
})
|
|
506
569
|
}) : /* @__PURE__ */ jsx(Button, {
|
|
507
570
|
variant: isPopular ? "default" : "outline",
|
|
508
571
|
asChild: true,
|
|
509
|
-
children: /* @__PURE__ */ jsx(Link
|
|
572
|
+
children: /* @__PURE__ */ jsx(Link, {
|
|
510
573
|
to: `/checkout?planId=${encodeURIComponent(plan.id)}`,
|
|
511
574
|
children: "Subscribe"
|
|
512
575
|
})
|
|
@@ -517,6 +580,284 @@ const PricingPage = () => {
|
|
|
517
580
|
});
|
|
518
581
|
};
|
|
519
582
|
//#endregion
|
|
583
|
+
//#region src/hooks/useChangeCreditEstimate.ts
|
|
584
|
+
/**
|
|
585
|
+
* Preview the proration credit for changing a subscription's plan, via the
|
|
586
|
+
* Zuplo metering `.../change/estimate-credit` endpoint. Failures are swallowed
|
|
587
|
+
* (`retry: false`, `throwOnError: false`) so the confirmation page never breaks
|
|
588
|
+
* when the preview is unavailable.
|
|
589
|
+
*/
|
|
590
|
+
const useChangeCreditEstimate = (subscriptionId, timing) => {
|
|
591
|
+
const zudoku = useZudoku();
|
|
592
|
+
return useQuery({
|
|
593
|
+
queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/subscriptions/${subscriptionId}/change/estimate-credit`, timing],
|
|
594
|
+
meta: {
|
|
595
|
+
context: zudoku,
|
|
596
|
+
request: {
|
|
597
|
+
method: "POST",
|
|
598
|
+
body: JSON.stringify({ timing })
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
retry: false,
|
|
602
|
+
throwOnError: false
|
|
603
|
+
});
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* Extract a positive credit amount from the estimate, or `undefined` when there
|
|
607
|
+
* isn't a usable one (so the UI only shows a credit when there actually is one).
|
|
608
|
+
*/
|
|
609
|
+
const getEstimatedCreditAmount = (estimate) => {
|
|
610
|
+
if (!estimate) return void 0;
|
|
611
|
+
const amount = Number.parseFloat(estimate.creditAmount ?? "");
|
|
612
|
+
if (!Number.isFinite(amount) || amount <= 0) return void 0;
|
|
613
|
+
return {
|
|
614
|
+
amount,
|
|
615
|
+
currency: estimate.currency
|
|
616
|
+
};
|
|
617
|
+
};
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/utils/formatDateTime.ts
|
|
620
|
+
/**
|
|
621
|
+
* Format an ISO timestamp with the time of day included, e.g.
|
|
622
|
+
* "Jun 3, 2026, 2:00 PM". Subscriptions can bill on sub-day cadences, so the
|
|
623
|
+
* time is what disambiguates a period's boundaries and the next renewal.
|
|
624
|
+
*/
|
|
625
|
+
const formatDateTime = (dateString) => new Date(dateString).toLocaleString("en-US", {
|
|
626
|
+
month: "short",
|
|
627
|
+
day: "numeric",
|
|
628
|
+
year: "numeric",
|
|
629
|
+
hour: "numeric",
|
|
630
|
+
minute: "2-digit"
|
|
631
|
+
});
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/utils/billables.ts
|
|
634
|
+
const getActivePhase = (sub) => {
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
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];
|
|
637
|
+
};
|
|
638
|
+
const activePhaseHasBillables = (sub) => getActivePhase(sub)?.items.some((i) => i.price != null) ?? false;
|
|
639
|
+
const hasFutureBillables = (sub) => {
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
return sub.phases.filter((p) => new Date(p.activeFrom).getTime() > now).some((p) => p.items.some((i) => i.price != null));
|
|
642
|
+
};
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/utils/subscriptionEntitlements.ts
|
|
645
|
+
const toRateCardPrice = (price) => {
|
|
646
|
+
if (!price) return null;
|
|
647
|
+
if (price.type === "tiered" && price.tiers) return {
|
|
648
|
+
type: "tiered",
|
|
649
|
+
mode: price.mode === "volume" ? "volume" : "graduated",
|
|
650
|
+
tiers: price.tiers.map((t) => ({
|
|
651
|
+
upToAmount: t.upToAmount,
|
|
652
|
+
flatPrice: t.flatPrice ? { amount: t.flatPrice.amount } : void 0,
|
|
653
|
+
unitPrice: t.unitPrice ? { amount: t.unitPrice.amount } : void 0
|
|
654
|
+
}))
|
|
655
|
+
};
|
|
656
|
+
if (price.type === "unit" && price.amount != null) return {
|
|
657
|
+
type: "unit",
|
|
658
|
+
amount: price.amount
|
|
659
|
+
};
|
|
660
|
+
if (price.type === "flat" && price.amount != null) return {
|
|
661
|
+
type: "flat",
|
|
662
|
+
amount: price.amount,
|
|
663
|
+
paymentTerm: price.paymentTerm === "in_advance" || price.paymentTerm === "in_arrears" ? price.paymentTerm : void 0
|
|
664
|
+
};
|
|
665
|
+
return null;
|
|
666
|
+
};
|
|
667
|
+
const flatOrNull = (price) => price?.type === "flat" ? price : null;
|
|
668
|
+
/**
|
|
669
|
+
* Convert an actual provisioned subscription line item into the `RateCard`
|
|
670
|
+
* shape so it can run through the same {@link categorizeRateCards} logic the
|
|
671
|
+
* pricing card uses. This is the key to showing a subscription's *real*
|
|
672
|
+
* included quota (e.g. `10 / hour`) — the catalog plan reports `0` for a
|
|
673
|
+
* priced first tier, but the subscription item carries the true
|
|
674
|
+
* `issueAfterReset`.
|
|
675
|
+
*/
|
|
676
|
+
const itemToRateCard = (item) => {
|
|
677
|
+
const ent = item.included?.entitlement;
|
|
678
|
+
const name = item.name ?? item.included?.feature?.name ?? item.featureKey ?? item.key;
|
|
679
|
+
const base = {
|
|
680
|
+
key: item.key,
|
|
681
|
+
name,
|
|
682
|
+
featureKey: item.featureKey
|
|
683
|
+
};
|
|
684
|
+
const price = toRateCardPrice(item.price);
|
|
685
|
+
const billingCadence = item.billingCadence ?? ent?.usagePeriod?.intervalISO ?? null;
|
|
686
|
+
if (ent?.type === "metered") {
|
|
687
|
+
const entitlementTemplate = {
|
|
688
|
+
type: "metered",
|
|
689
|
+
isSoftLimit: ent.isSoftLimit,
|
|
690
|
+
issueAfterReset: ent.issueAfterReset,
|
|
691
|
+
usagePeriod: ent.usagePeriod?.intervalISO
|
|
692
|
+
};
|
|
693
|
+
if (price?.type === "flat") return {
|
|
694
|
+
...base,
|
|
695
|
+
type: "flat_fee",
|
|
696
|
+
billingCadence,
|
|
697
|
+
price,
|
|
698
|
+
entitlementTemplate
|
|
699
|
+
};
|
|
700
|
+
return {
|
|
701
|
+
...base,
|
|
702
|
+
type: "usage_based",
|
|
703
|
+
billingCadence: billingCadence ?? "P1M",
|
|
704
|
+
price,
|
|
705
|
+
entitlementTemplate
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (ent?.type === "boolean") return {
|
|
709
|
+
...base,
|
|
710
|
+
type: "flat_fee",
|
|
711
|
+
billingCadence,
|
|
712
|
+
price: flatOrNull(price),
|
|
713
|
+
entitlementTemplate: { type: "boolean" }
|
|
714
|
+
};
|
|
715
|
+
if (ent?.type === "static") return {
|
|
716
|
+
...base,
|
|
717
|
+
type: "flat_fee",
|
|
718
|
+
billingCadence,
|
|
719
|
+
price: flatOrNull(price),
|
|
720
|
+
entitlementTemplate: {
|
|
721
|
+
type: "static",
|
|
722
|
+
config: ent.config ?? ""
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
...base,
|
|
727
|
+
type: "flat_fee",
|
|
728
|
+
billingCadence,
|
|
729
|
+
price: flatOrNull(price)
|
|
730
|
+
};
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* Categorize a subscription phase's actual items into `{ quotas, features }`,
|
|
734
|
+
* reusing {@link categorizeRateCards} so the subscription view stays
|
|
735
|
+
* consistent with the pricing card.
|
|
736
|
+
*/
|
|
737
|
+
const categorizeSubscriptionItems = (items, options) => categorizeRateCards(items.map(itemToRateCard), options);
|
|
738
|
+
/** Map a subscription phase's items to rate cards (for price/entitlement derivation). */
|
|
739
|
+
const subscriptionItemsToRateCards = (items) => items.map(itemToRateCard);
|
|
740
|
+
/**
|
|
741
|
+
* Resolve a subscription's headline price and entitlements, preferring the
|
|
742
|
+
* active phase's ACTUAL provisioned items (the authoritative source — real
|
|
743
|
+
* included quotas and recurring fees) and falling back to the catalog plan's
|
|
744
|
+
* rate cards only when items aren't populated. Keeping both the price and the
|
|
745
|
+
* entitlements on the same source avoids showing "Free" for a paid plan whose
|
|
746
|
+
* embedded snapshot is incomplete.
|
|
747
|
+
*/
|
|
748
|
+
const getSubscriptionPlanView = (subscription, options) => {
|
|
749
|
+
const plan = subscription.plan;
|
|
750
|
+
const currency = subscription.currency ?? plan.currency;
|
|
751
|
+
const billingCadence = subscription.billingCadence ?? plan.billingCadence;
|
|
752
|
+
const items = getActivePhase(subscription)?.items ?? [];
|
|
753
|
+
if (items.length > 0) {
|
|
754
|
+
const rateCards = subscriptionItemsToRateCards(items);
|
|
755
|
+
return {
|
|
756
|
+
priceLabel: formatPlanPrice({
|
|
757
|
+
...plan,
|
|
758
|
+
billingCadence,
|
|
759
|
+
phases: [{
|
|
760
|
+
key: "active",
|
|
761
|
+
name: "Active",
|
|
762
|
+
rateCards
|
|
763
|
+
}]
|
|
764
|
+
}),
|
|
765
|
+
entitlements: categorizeRateCards(rateCards, {
|
|
766
|
+
currency,
|
|
767
|
+
units: options?.units,
|
|
768
|
+
planBillingCadence: billingCadence
|
|
769
|
+
}),
|
|
770
|
+
fallbackPhases: [],
|
|
771
|
+
usingItems: true,
|
|
772
|
+
billingCadence,
|
|
773
|
+
currency
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
priceLabel: formatPlanPrice(plan),
|
|
778
|
+
entitlements: {
|
|
779
|
+
quotas: [],
|
|
780
|
+
features: []
|
|
781
|
+
},
|
|
782
|
+
fallbackPhases: plan.phases ?? [],
|
|
783
|
+
usingItems: false,
|
|
784
|
+
billingCadence,
|
|
785
|
+
currency
|
|
786
|
+
};
|
|
787
|
+
};
|
|
788
|
+
/** Whether a resolved view has any entitlements to render. */
|
|
789
|
+
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));
|
|
790
|
+
//#endregion
|
|
791
|
+
//#region src/pages/components/SubscriptionEntitlements.tsx
|
|
792
|
+
/**
|
|
793
|
+
* Render a subscription's entitlements from a resolved
|
|
794
|
+
* {@link SubscriptionPlanView}: the active phase's *provisioned* items when
|
|
795
|
+
* present (the real included quotas + per-unit prices), otherwise the catalog
|
|
796
|
+
* plan's phases. Centralizes the "items, else fall back to plan phases" branch
|
|
797
|
+
* shared by the subscription details page and the Switch Plan baseline.
|
|
798
|
+
*
|
|
799
|
+
* Renders `null` when there's nothing to show, so callers can wrap it with
|
|
800
|
+
* {@link hasSubscriptionEntitlements} (for a section header / border) without
|
|
801
|
+
* leaving an empty container behind.
|
|
802
|
+
*/
|
|
803
|
+
const SubscriptionEntitlements = ({ view, currency, billingCadence, units, itemClassName }) => {
|
|
804
|
+
if (view.usingItems) {
|
|
805
|
+
const { quotas, features } = view.entitlements;
|
|
806
|
+
return /* @__PURE__ */ jsx(EntitlementList, {
|
|
807
|
+
quotas,
|
|
808
|
+
features,
|
|
809
|
+
itemClassName
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
if (view.fallbackPhases.length === 0) return null;
|
|
813
|
+
return /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
814
|
+
phases: view.fallbackPhases,
|
|
815
|
+
currency,
|
|
816
|
+
billingCadence,
|
|
817
|
+
units,
|
|
818
|
+
itemClassName
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/pages/components/CurrentPlanBaseline.tsx
|
|
823
|
+
/**
|
|
824
|
+
* Baseline shown at the top of the Switch Plan modal: the current plan's price
|
|
825
|
+
* and what it actually includes, so users have a concrete reference to compare
|
|
826
|
+
* targets against. Both the price and the entitlements come from the
|
|
827
|
+
* subscription's *provisioned* items (real included quotas + recurring fees),
|
|
828
|
+
* falling back to the plan's rate cards only when items aren't present — see
|
|
829
|
+
* {@link getSubscriptionPlanView}.
|
|
830
|
+
*/
|
|
831
|
+
const CurrentPlanBaseline = ({ subscription, units }) => {
|
|
832
|
+
const plan = subscription.plan;
|
|
833
|
+
const view = getSubscriptionPlanView(subscription, { units });
|
|
834
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
835
|
+
className: "border rounded-lg p-4",
|
|
836
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
837
|
+
className: "flex items-start justify-between gap-3 flex-wrap",
|
|
838
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
839
|
+
className: "text-xs font-medium text-muted-foreground uppercase tracking-wide",
|
|
840
|
+
children: "Current Plan"
|
|
841
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
842
|
+
className: "text-lg font-bold text-foreground",
|
|
843
|
+
children: plan.name
|
|
844
|
+
})] }), /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
845
|
+
label: view.priceLabel,
|
|
846
|
+
currency: view.currency,
|
|
847
|
+
billingCadence: view.billingCadence
|
|
848
|
+
})]
|
|
849
|
+
}), hasSubscriptionEntitlements(view) && /* @__PURE__ */ jsx("div", {
|
|
850
|
+
className: "mt-3 pt-3 border-t",
|
|
851
|
+
children: /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
852
|
+
view,
|
|
853
|
+
currency: view.currency,
|
|
854
|
+
billingCadence: view.billingCadence,
|
|
855
|
+
units
|
|
856
|
+
})
|
|
857
|
+
})]
|
|
858
|
+
});
|
|
859
|
+
};
|
|
860
|
+
//#endregion
|
|
520
861
|
//#region src/pages/SubscriptionChangeConfirmPage.tsx
|
|
521
862
|
const SubscriptionChangeConfirmPage = () => {
|
|
522
863
|
const [search] = useSearchParams();
|
|
@@ -524,152 +865,68 @@ const SubscriptionChangeConfirmPage = () => {
|
|
|
524
865
|
const subscriptionId = search.get("subscriptionId");
|
|
525
866
|
const mode = search.get("mode");
|
|
526
867
|
const zudoku = useZudoku();
|
|
527
|
-
const deploymentName = useDeploymentName();
|
|
528
|
-
const navigate = useNavigate();
|
|
529
868
|
const { pricing } = useMonetizationConfig();
|
|
530
869
|
if (!planId) throw new Error("Parameter `planId` missing");
|
|
531
870
|
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
|
-
}
|
|
871
|
+
const { selectedPlan, taxAmount, taxLabel, taxInclusive } = usePurchaseSummary(planId);
|
|
872
|
+
const { data: subscriptionsData } = useQuery(subscriptionsQuery(zudoku));
|
|
873
|
+
const currentSubscription = subscriptionsData?.items.find((s) => s.id === subscriptionId);
|
|
874
|
+
const isDowngrade = mode === "downgrade";
|
|
875
|
+
const credit = getEstimatedCreditAmount(useChangeCreditEstimate(subscriptionId, isDowngrade ? "next_billing_cycle" : "immediate").data);
|
|
876
|
+
const nextCycleEnd = currentSubscription?.alignment?.currentAlignedBillingPeriod?.to;
|
|
877
|
+
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";
|
|
878
|
+
const changeMutation = useSubscriptionConfirmMutation({
|
|
879
|
+
endpoint: `subscriptions/${subscriptionId}/change`,
|
|
880
|
+
planId,
|
|
881
|
+
navigateState: { planSwitched: { newPlanName: selectedPlan?.name } }
|
|
553
882
|
});
|
|
554
|
-
return /* @__PURE__ */ jsx(
|
|
555
|
-
|
|
883
|
+
return /* @__PURE__ */ jsx(ConfirmationScreen, {
|
|
884
|
+
title: "Confirm plan change",
|
|
885
|
+
message: /* @__PURE__ */ jsx("p", {
|
|
886
|
+
className: "text-muted-foreground text-base",
|
|
887
|
+
children: "Review the change below before confirming."
|
|
888
|
+
}),
|
|
889
|
+
errorMessage: changeMutation.isError ? changeMutation.error.message : void 0,
|
|
890
|
+
confirmLabel: "Confirm & Change Plan",
|
|
891
|
+
pendingLabel: "Changing plan...",
|
|
892
|
+
onConfirm: () => changeMutation.mutate(),
|
|
893
|
+
isPending: changeMutation.isPending,
|
|
894
|
+
cancelTo: `/subscriptions?${new URLSearchParams({ subscriptionId })}`,
|
|
895
|
+
termsNote: "By confirming, you agree to our Terms of Service and Privacy Policy.",
|
|
556
896
|
children: /* @__PURE__ */ jsxs("div", {
|
|
557
|
-
className: "
|
|
558
|
-
children: [
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
897
|
+
className: "space-y-3",
|
|
898
|
+
children: [
|
|
899
|
+
currentSubscription && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CurrentPlanBaseline, {
|
|
900
|
+
subscription: currentSubscription,
|
|
901
|
+
units: pricing?.units
|
|
902
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
903
|
+
className: "flex items-center justify-center gap-1.5 text-sm text-muted-foreground",
|
|
904
|
+
children: [/* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4" }), " Changing to"]
|
|
905
|
+
})] }),
|
|
906
|
+
selectedPlan && /* @__PURE__ */ jsx(PlanSummaryCard, {
|
|
907
|
+
plan: selectedPlan,
|
|
908
|
+
descriptionFallback: "New plan",
|
|
909
|
+
taxAmount,
|
|
910
|
+
taxLabel,
|
|
911
|
+
taxInclusive,
|
|
912
|
+
units: pricing?.units
|
|
913
|
+
}),
|
|
914
|
+
/* @__PURE__ */ jsxs("div", {
|
|
915
|
+
className: "rounded-lg bg-muted/50 p-3 text-sm space-y-1",
|
|
916
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
917
|
+
className: "font-medium text-card-foreground",
|
|
918
|
+
children: effectiveText
|
|
919
|
+
}), credit && /* @__PURE__ */ jsxs("div", {
|
|
920
|
+
className: "text-muted-foreground",
|
|
574
921
|
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
|
-
})
|
|
922
|
+
"You'll be credited ",
|
|
923
|
+
formatPrice(credit.amount, credit.currency),
|
|
924
|
+
" ",
|
|
925
|
+
"for unused time on your current plan."
|
|
587
926
|
]
|
|
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
|
-
})]
|
|
927
|
+
})]
|
|
928
|
+
})
|
|
929
|
+
]
|
|
673
930
|
})
|
|
674
931
|
});
|
|
675
932
|
};
|
|
@@ -689,17 +946,6 @@ const useSubscriptions = () => {
|
|
|
689
946
|
});
|
|
690
947
|
};
|
|
691
948
|
//#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
949
|
//#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
|
|
704
950
|
const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
|
|
705
951
|
return /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
|
|
@@ -712,7 +958,7 @@ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
|
|
|
712
958
|
};
|
|
713
959
|
//#endregion
|
|
714
960
|
//#region src/pages/subscriptions/ApiKey.tsx
|
|
715
|
-
const formatDate$
|
|
961
|
+
const formatDate$1 = (dateString) => {
|
|
716
962
|
if (!dateString) return "";
|
|
717
963
|
return new Date(dateString).toLocaleDateString("en-US", {
|
|
718
964
|
month: "short",
|
|
@@ -731,7 +977,7 @@ const getTimeAgo = (dateString) => {
|
|
|
731
977
|
const diffInDays = Math.floor(diffInHours / 24);
|
|
732
978
|
if (diffInDays === 1) return "1 day ago";
|
|
733
979
|
if (diffInDays < 30) return `${diffInDays} days ago`;
|
|
734
|
-
return formatDate$
|
|
980
|
+
return formatDate$1(dateString);
|
|
735
981
|
};
|
|
736
982
|
const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
|
|
737
983
|
const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
|
|
@@ -788,7 +1034,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
|
|
|
788
1034
|
children: [
|
|
789
1035
|
/* @__PURE__ */ jsxs("div", {
|
|
790
1036
|
className: "flex items-center gap-1.5",
|
|
791
|
-
children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$
|
|
1037
|
+
children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(createdAt)] })]
|
|
792
1038
|
}),
|
|
793
1039
|
/* @__PURE__ */ jsx("span", {
|
|
794
1040
|
className: "text-muted-foreground/40",
|
|
@@ -803,7 +1049,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
|
|
|
803
1049
|
children: [
|
|
804
1050
|
isExpired ? "Expired" : "Expires",
|
|
805
1051
|
" on ",
|
|
806
|
-
formatDate$
|
|
1052
|
+
formatDate$1(expiresOn)
|
|
807
1053
|
]
|
|
808
1054
|
})] })
|
|
809
1055
|
]
|
|
@@ -918,7 +1164,7 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
|
|
|
918
1164
|
/* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
|
|
919
1165
|
/* @__PURE__ */ jsx(AlertDescription, { children: (() => {
|
|
920
1166
|
const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
|
|
921
|
-
return deletedKey ? `API key created ${formatDate$
|
|
1167
|
+
return deletedKey ? `API key created ${formatDate$1(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
|
|
922
1168
|
})() }),
|
|
923
1169
|
/* @__PURE__ */ jsx(DismissibleAlertAction, {})
|
|
924
1170
|
]
|
|
@@ -1041,7 +1287,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
|
|
|
1041
1287
|
" subscription?"
|
|
1042
1288
|
] }), /* @__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
1289
|
"You'll retain access until ",
|
|
1044
|
-
formatDate$
|
|
1290
|
+
formatDate$1(billingPeriodEnd),
|
|
1045
1291
|
". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
|
|
1046
1292
|
] })] })]
|
|
1047
1293
|
}),
|
|
@@ -1050,7 +1296,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
|
|
|
1050
1296
|
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
1297
|
"If you change your mind you have until",
|
|
1052
1298
|
" ",
|
|
1053
|
-
formatDate$
|
|
1299
|
+
formatDate$1(billingPeriodEnd),
|
|
1054
1300
|
" to remove this cancellation from Manage subscription."
|
|
1055
1301
|
] })] })]
|
|
1056
1302
|
}),
|
|
@@ -1152,7 +1398,7 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1152
1398
|
className: "space-y-2",
|
|
1153
1399
|
children: [/* @__PURE__ */ jsxs("p", { children: [
|
|
1154
1400
|
"Your access stays in place until ",
|
|
1155
|
-
formatDate$
|
|
1401
|
+
formatDate$1(billingPeriodEnd),
|
|
1156
1402
|
" ",
|
|
1157
1403
|
"either way."
|
|
1158
1404
|
] }), /* @__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 +1431,315 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
|
|
|
1185
1431
|
});
|
|
1186
1432
|
};
|
|
1187
1433
|
//#endregion
|
|
1188
|
-
//#region src/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
};
|
|
1434
|
+
//#region src/utils/comparePlanEntitlements.ts
|
|
1435
|
+
/** Compact, human-readable value for a quota row. */
|
|
1436
|
+
const quotaValueLabel = (q) => {
|
|
1437
|
+
if (q.unitPrice) return q.unitPrice;
|
|
1438
|
+
if (q.tierPrices && q.tierPrices.length > 0) return "Tiered pricing";
|
|
1439
|
+
if (q.isPayg) return "Usage-based";
|
|
1440
|
+
return `${q.limit.toLocaleString("en-US")} / ${q.period}`;
|
|
1205
1441
|
};
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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) {
|
|
1442
|
+
const featureValueLabel = (f) => f.value ?? "Included";
|
|
1443
|
+
const isPlainNumericQuota = (q) => !q.isPayg && !q.unitPrice && (!q.tierPrices || q.tierPrices.length === 0);
|
|
1444
|
+
const sameTierSchedule = (a, b) => (a ?? []).join("\n") === (b ?? []).join("\n");
|
|
1445
|
+
/**
|
|
1446
|
+
* Compare two plans' entitlements, matching strictly by feature key (never by
|
|
1447
|
+
* display name). Each key yields exactly one change row, so a key that exists
|
|
1448
|
+
* on one side and a differently-keyed feature that merely shares a display
|
|
1449
|
+
* name can never read as a contradictory "added" + "removed" of the same
|
|
1450
|
+
* thing. Labels are disambiguated afterwards when they would collide.
|
|
1451
|
+
*/
|
|
1452
|
+
const comparePlanEntitlements = (current, target) => {
|
|
1453
|
+
const changes = [];
|
|
1454
|
+
const curQuota = new Map(current.quotas.map((q) => [q.key, q]));
|
|
1455
|
+
const tgtQuota = new Map(target.quotas.map((q) => [q.key, q]));
|
|
1456
|
+
const curFeat = new Map(current.features.map((f) => [f.key, f]));
|
|
1457
|
+
const tgtFeat = new Map(target.features.map((f) => [f.key, f]));
|
|
1458
|
+
for (const key of new Set([...curQuota.keys(), ...tgtQuota.keys()])) {
|
|
1459
|
+
const c = curQuota.get(key);
|
|
1460
|
+
const t = tgtQuota.get(key);
|
|
1461
|
+
if (c && t) {
|
|
1462
|
+
const currentValue = quotaValueLabel(c);
|
|
1463
|
+
const targetValue = quotaValueLabel(t);
|
|
1237
1464
|
let change = "same";
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
change
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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"
|
|
1465
|
+
if (isPlainNumericQuota(c) && isPlainNumericQuota(t) && c.period === t.period) {
|
|
1466
|
+
if (t.limit > c.limit) change = "increase";
|
|
1467
|
+
else if (t.limit < c.limit) change = "decrease";
|
|
1468
|
+
} else if (currentValue !== targetValue || !sameTierSchedule(c.tierPrices, t.tierPrices)) change = "changed";
|
|
1469
|
+
changes.push({
|
|
1470
|
+
key,
|
|
1471
|
+
label: t.name,
|
|
1472
|
+
kind: "quota",
|
|
1473
|
+
change,
|
|
1474
|
+
currentValue,
|
|
1475
|
+
targetValue,
|
|
1476
|
+
tierPrices: t.tierPrices,
|
|
1477
|
+
period: t.period
|
|
1277
1478
|
});
|
|
1278
|
-
}
|
|
1479
|
+
} else if (t) changes.push({
|
|
1480
|
+
key,
|
|
1481
|
+
label: t.name,
|
|
1482
|
+
kind: "quota",
|
|
1483
|
+
change: "added",
|
|
1484
|
+
targetValue: quotaValueLabel(t),
|
|
1485
|
+
tierPrices: t.tierPrices,
|
|
1486
|
+
period: t.period
|
|
1487
|
+
});
|
|
1488
|
+
else if (c) changes.push({
|
|
1489
|
+
key,
|
|
1490
|
+
label: c.name,
|
|
1491
|
+
kind: "quota",
|
|
1492
|
+
change: "removed",
|
|
1493
|
+
currentValue: quotaValueLabel(c),
|
|
1494
|
+
period: c.period
|
|
1495
|
+
});
|
|
1279
1496
|
}
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
currentValue
|
|
1292
|
-
|
|
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"
|
|
1497
|
+
for (const key of new Set([...curFeat.keys(), ...tgtFeat.keys()])) {
|
|
1498
|
+
const c = curFeat.get(key);
|
|
1499
|
+
const t = tgtFeat.get(key);
|
|
1500
|
+
if (c && t) {
|
|
1501
|
+
const currentValue = featureValueLabel(c);
|
|
1502
|
+
const targetValue = featureValueLabel(t);
|
|
1503
|
+
changes.push({
|
|
1504
|
+
key,
|
|
1505
|
+
label: t.name,
|
|
1506
|
+
kind: "feature",
|
|
1507
|
+
change: currentValue === targetValue ? "same" : "changed",
|
|
1508
|
+
currentValue,
|
|
1509
|
+
targetValue
|
|
1321
1510
|
});
|
|
1322
|
-
}
|
|
1511
|
+
} else if (t) changes.push({
|
|
1512
|
+
key,
|
|
1513
|
+
label: t.name,
|
|
1514
|
+
kind: "feature",
|
|
1515
|
+
change: "added",
|
|
1516
|
+
targetValue: featureValueLabel(t)
|
|
1517
|
+
});
|
|
1518
|
+
else if (c) changes.push({
|
|
1519
|
+
key,
|
|
1520
|
+
label: c.name,
|
|
1521
|
+
kind: "feature",
|
|
1522
|
+
change: "removed",
|
|
1523
|
+
currentValue: featureValueLabel(c)
|
|
1524
|
+
});
|
|
1323
1525
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1526
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
1527
|
+
for (const ch of changes) labelCounts.set(ch.label, (labelCounts.get(ch.label) ?? 0) + 1);
|
|
1528
|
+
return changes.map(({ period, ...ch }) => {
|
|
1529
|
+
if ((labelCounts.get(ch.label) ?? 0) > 1 && ch.kind === "quota" && period) return {
|
|
1530
|
+
...ch,
|
|
1531
|
+
label: `${ch.label} (${period})`
|
|
1532
|
+
};
|
|
1533
|
+
return ch;
|
|
1534
|
+
});
|
|
1331
1535
|
};
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1536
|
+
//#endregion
|
|
1537
|
+
//#region src/utils/formatPhaseRampSummary.ts
|
|
1538
|
+
const durationWithCount = (iso) => {
|
|
1539
|
+
const text = formatDuration(iso);
|
|
1540
|
+
return /^\d/.test(text) ? text : `1 ${text}`;
|
|
1336
1541
|
};
|
|
1337
|
-
const
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
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;
|
|
1542
|
+
const steadyStateLabel = (plan) => {
|
|
1543
|
+
const label = formatPlanPrice(plan);
|
|
1544
|
+
if (label.type === "priced") return `${formatPrice(label.amount, plan.currency)} / ${formatDuration(plan.billingCadence)}`;
|
|
1545
|
+
return label.type === "payg" ? "Pay as you go" : "Free";
|
|
1345
1546
|
};
|
|
1346
|
-
|
|
1547
|
+
/**
|
|
1548
|
+
* One-line summary of a multi-phase plan's progression for compact UI, e.g.
|
|
1549
|
+
* `"Free Trial (1 week), then $2.99 / month"`. Returns `undefined` for
|
|
1550
|
+
* single-phase plans (nothing to summarize).
|
|
1551
|
+
*/
|
|
1552
|
+
const formatPhaseRampSummary = (plan) => {
|
|
1553
|
+
if (!plan.phases || plan.phases.length <= 1) return void 0;
|
|
1554
|
+
const first = plan.phases[0];
|
|
1555
|
+
return `${first.duration ? `${first.name} (${durationWithCount(first.duration)})` : first.name}, then ${steadyStateLabel(plan)}`;
|
|
1556
|
+
};
|
|
1557
|
+
//#endregion
|
|
1558
|
+
//#region src/pages/components/PlanChangeCard.tsx
|
|
1559
|
+
const MODE_LABEL = {
|
|
1347
1560
|
upgrade: "Upgrade",
|
|
1348
1561
|
downgrade: "Downgrade",
|
|
1349
1562
|
private: "Switch"
|
|
1350
1563
|
};
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1564
|
+
const ChangeRow = ({ change }) => {
|
|
1565
|
+
const arrow = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1566
|
+
className: "text-muted-foreground",
|
|
1567
|
+
children: change.currentValue
|
|
1568
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1569
|
+
className: "text-muted-foreground",
|
|
1570
|
+
children: "→"
|
|
1571
|
+
})] });
|
|
1572
|
+
let icon;
|
|
1573
|
+
let body;
|
|
1574
|
+
switch (change.change) {
|
|
1575
|
+
case "added":
|
|
1576
|
+
icon = /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 text-green-600 shrink-0 mt-0.5" });
|
|
1577
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1578
|
+
/* @__PURE__ */ jsx("span", {
|
|
1579
|
+
className: "font-medium",
|
|
1580
|
+
children: change.label
|
|
1581
|
+
}),
|
|
1582
|
+
change.targetValue && change.targetValue !== "Included" ? /* @__PURE__ */ jsxs("span", {
|
|
1583
|
+
className: "text-muted-foreground",
|
|
1584
|
+
children: [": ", change.targetValue]
|
|
1585
|
+
}) : null,
|
|
1586
|
+
/* @__PURE__ */ jsx("span", {
|
|
1587
|
+
className: "text-green-600",
|
|
1588
|
+
children: " — now included"
|
|
1589
|
+
})
|
|
1590
|
+
] });
|
|
1591
|
+
break;
|
|
1592
|
+
case "removed":
|
|
1593
|
+
icon = /* @__PURE__ */ jsx(XIcon, { className: "size-4 text-destructive shrink-0 mt-0.5" });
|
|
1594
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1595
|
+
className: "font-medium",
|
|
1596
|
+
children: change.label
|
|
1597
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1598
|
+
className: "text-destructive",
|
|
1599
|
+
children: " — no longer included"
|
|
1600
|
+
})] });
|
|
1601
|
+
break;
|
|
1602
|
+
case "increase":
|
|
1603
|
+
icon = /* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-4 text-primary shrink-0 mt-0.5" });
|
|
1604
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1605
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1606
|
+
className: "font-medium",
|
|
1607
|
+
children: [change.label, ":"]
|
|
1608
|
+
}),
|
|
1609
|
+
" ",
|
|
1610
|
+
arrow,
|
|
1611
|
+
/* @__PURE__ */ jsx("span", {
|
|
1612
|
+
className: "font-medium text-primary",
|
|
1613
|
+
children: change.targetValue
|
|
1614
|
+
})
|
|
1615
|
+
] });
|
|
1616
|
+
break;
|
|
1617
|
+
case "decrease":
|
|
1618
|
+
icon = /* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-4 text-amber-600 shrink-0 mt-0.5" });
|
|
1619
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1620
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1621
|
+
className: "font-medium",
|
|
1622
|
+
children: [change.label, ":"]
|
|
1623
|
+
}),
|
|
1624
|
+
" ",
|
|
1625
|
+
arrow,
|
|
1626
|
+
/* @__PURE__ */ jsx("span", {
|
|
1627
|
+
className: "font-medium text-amber-600",
|
|
1628
|
+
children: change.targetValue
|
|
1629
|
+
})
|
|
1630
|
+
] });
|
|
1631
|
+
break;
|
|
1632
|
+
case "same":
|
|
1633
|
+
icon = /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 text-muted-foreground shrink-0 mt-0.5" });
|
|
1634
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { children: change.label }), change.targetValue && change.targetValue !== "Included" && /* @__PURE__ */ jsxs("span", {
|
|
1635
|
+
className: "text-muted-foreground",
|
|
1636
|
+
children: [": ", change.targetValue]
|
|
1637
|
+
})] });
|
|
1638
|
+
break;
|
|
1639
|
+
default:
|
|
1640
|
+
icon = /* @__PURE__ */ jsx(ArrowLeftRightIcon, { className: "size-4 text-muted-foreground shrink-0 mt-0.5" });
|
|
1641
|
+
body = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1642
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1643
|
+
className: "font-medium",
|
|
1644
|
+
children: [change.label, ":"]
|
|
1645
|
+
}),
|
|
1646
|
+
" ",
|
|
1647
|
+
change.currentValue !== change.targetValue && arrow,
|
|
1648
|
+
/* @__PURE__ */ jsx("span", {
|
|
1649
|
+
className: "font-medium",
|
|
1650
|
+
children: change.targetValue
|
|
1651
|
+
})
|
|
1652
|
+
] });
|
|
1653
|
+
}
|
|
1654
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1655
|
+
className: "flex items-start gap-2 text-sm",
|
|
1656
|
+
children: [icon, /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
1657
|
+
className: "flex flex-wrap items-baseline gap-1",
|
|
1658
|
+
children: body
|
|
1659
|
+
}), change.tierPrices && change.tierPrices.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1660
|
+
className: "text-xs text-muted-foreground mt-1 space-y-0.5",
|
|
1661
|
+
children: change.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
|
|
1662
|
+
})] })]
|
|
1663
|
+
});
|
|
1355
1664
|
};
|
|
1356
|
-
const
|
|
1357
|
-
const
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1665
|
+
const PlanChangeCard = ({ plan, mode, currentEntitlements, isNewerVersion, isSwitching, units, onSwitch }) => {
|
|
1666
|
+
const isCustom = isCustomPlan(plan);
|
|
1667
|
+
const priceLabel = formatPlanPrice(plan);
|
|
1668
|
+
const ramp = formatPhaseRampSummary(plan);
|
|
1669
|
+
const entitlementChanges = useMemo(() => {
|
|
1670
|
+
const steadyPhase = plan.phases.at(-1);
|
|
1671
|
+
const changes = comparePlanEntitlements(currentEntitlements, steadyPhase ? categorizeRateCards(steadyPhase.rateCards, {
|
|
1672
|
+
currency: plan.currency,
|
|
1673
|
+
units,
|
|
1674
|
+
planBillingCadence: plan.billingCadence
|
|
1675
|
+
}) : {
|
|
1676
|
+
quotas: [],
|
|
1677
|
+
features: []
|
|
1678
|
+
});
|
|
1679
|
+
return [...changes.filter((c) => c.change !== "removed"), ...changes.filter((c) => c.change === "removed")];
|
|
1680
|
+
}, [
|
|
1681
|
+
plan,
|
|
1682
|
+
currentEntitlements,
|
|
1683
|
+
units
|
|
1684
|
+
]);
|
|
1361
1685
|
return /* @__PURE__ */ jsxs("div", {
|
|
1362
1686
|
className: "border rounded-lg p-4",
|
|
1363
|
-
children: [
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
className: "flex items-baseline gap-2 flex-wrap",
|
|
1687
|
+
children: [
|
|
1688
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1689
|
+
className: "flex items-center justify-between gap-3 mb-2",
|
|
1367
1690
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1368
|
-
className: "flex items-
|
|
1369
|
-
children: [/* @__PURE__ */
|
|
1370
|
-
className: "
|
|
1371
|
-
children:
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1691
|
+
className: "flex items-baseline gap-2 flex-wrap",
|
|
1692
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1693
|
+
className: "flex items-center gap-2",
|
|
1694
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
1695
|
+
className: "font-semibold text-foreground",
|
|
1696
|
+
children: plan.name
|
|
1697
|
+
}), isNewerVersion && /* @__PURE__ */ jsx(Badge, {
|
|
1698
|
+
variant: "outline",
|
|
1699
|
+
className: "rounded-full border-primary/30 bg-primary/10 text-primary font-medium",
|
|
1700
|
+
children: "New version"
|
|
1701
|
+
})]
|
|
1702
|
+
}), isCustom ? /* @__PURE__ */ jsx("span", {
|
|
1703
|
+
className: "text-primary font-medium",
|
|
1704
|
+
children: "Custom"
|
|
1705
|
+
}) : /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
1706
|
+
label: priceLabel,
|
|
1707
|
+
currency: plan.currency,
|
|
1708
|
+
billingCadence: plan.billingCadence
|
|
1376
1709
|
})]
|
|
1377
|
-
}), isCustom ? /* @__PURE__ */ jsx(
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
"/",
|
|
1388
|
-
formatDuration(comparison.plan.billingCadence)
|
|
1389
|
-
]
|
|
1710
|
+
}), isCustom ? /* @__PURE__ */ jsx(Button$1, {
|
|
1711
|
+
variant: "default",
|
|
1712
|
+
size: "sm",
|
|
1713
|
+
children: "Contact Sales"
|
|
1714
|
+
}) : /* @__PURE__ */ jsx(Button$1, {
|
|
1715
|
+
variant: mode === "upgrade" ? "default" : "outline",
|
|
1716
|
+
onClick: onSwitch,
|
|
1717
|
+
size: "sm",
|
|
1718
|
+
disabled: isSwitching,
|
|
1719
|
+
children: MODE_LABEL[mode]
|
|
1390
1720
|
})]
|
|
1391
|
-
}),
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
children:
|
|
1395
|
-
})
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
}),
|
|
1402
|
-
size: "sm",
|
|
1403
|
-
disabled: isSwitching,
|
|
1404
|
-
children: modeLabelMap[mode]
|
|
1405
|
-
})]
|
|
1406
|
-
}), hasChanges && /* @__PURE__ */ jsxs("div", {
|
|
1407
|
-
className: "space-y-1.5",
|
|
1408
|
-
children: [comparison.quotaChanges.map((quota) => /* @__PURE__ */ jsxs("div", {
|
|
1409
|
-
className: "flex items-center gap-2 text-sm",
|
|
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
|
-
})]
|
|
1721
|
+
}),
|
|
1722
|
+
ramp && /* @__PURE__ */ jsx("p", {
|
|
1723
|
+
className: "text-sm text-muted-foreground mb-2",
|
|
1724
|
+
children: ramp
|
|
1725
|
+
}),
|
|
1726
|
+
entitlementChanges.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1727
|
+
className: "space-y-1.5",
|
|
1728
|
+
children: entitlementChanges.map((change) => /* @__PURE__ */ jsx(ChangeRow, { change }, `${change.kind}:${change.key}`))
|
|
1729
|
+
})
|
|
1730
|
+
]
|
|
1506
1731
|
});
|
|
1507
1732
|
};
|
|
1733
|
+
//#endregion
|
|
1734
|
+
//#region src/pages/subscriptions/SwitchPlanModal.tsx
|
|
1735
|
+
const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
|
|
1736
|
+
const planVersion = (plan) => plan.version ?? 1;
|
|
1737
|
+
const isNewerPlanVersion = (subscribedPlan, target) => target.key === subscribedPlan.key && planVersion(target) > planVersion(subscribedPlan);
|
|
1738
|
+
const resolveIsUpgrade = ({ target, targetIndex, subscribedPlan, currentIndex }) => {
|
|
1739
|
+
if (target.key === subscribedPlan.key) return planVersion(target) > planVersion(subscribedPlan);
|
|
1740
|
+
return targetIndex >= currentIndex;
|
|
1741
|
+
};
|
|
1742
|
+
const isSwitchPlanTarget = (value) => typeof value === "object" && value !== null && "subscriptionId" in value && "plan" in value && "mode" in value;
|
|
1508
1743
|
const SwitchPlanModal = ({ subscription, children }) => {
|
|
1509
1744
|
const [open, setOpen] = useState(false);
|
|
1510
1745
|
const { data: plansData } = usePlans();
|
|
@@ -1539,6 +1774,27 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1539
1774
|
}
|
|
1540
1775
|
});
|
|
1541
1776
|
const subscribedPlan = subscription.plan;
|
|
1777
|
+
const currentEntitlements = useMemo(() => {
|
|
1778
|
+
const currency = subscription.currency ?? subscribedPlan.currency;
|
|
1779
|
+
const activePhase = getActivePhase(subscription);
|
|
1780
|
+
if (activePhase && (activePhase.items?.length ?? 0) > 0) return categorizeSubscriptionItems(activePhase.items, {
|
|
1781
|
+
currency,
|
|
1782
|
+
units: pricing?.units
|
|
1783
|
+
});
|
|
1784
|
+
const lastPhase = subscribedPlan.phases?.at(-1);
|
|
1785
|
+
return lastPhase ? categorizeRateCards(lastPhase.rateCards, {
|
|
1786
|
+
currency,
|
|
1787
|
+
units: pricing?.units,
|
|
1788
|
+
planBillingCadence: subscribedPlan.billingCadence
|
|
1789
|
+
}) : {
|
|
1790
|
+
quotas: [],
|
|
1791
|
+
features: []
|
|
1792
|
+
};
|
|
1793
|
+
}, [
|
|
1794
|
+
subscription,
|
|
1795
|
+
subscribedPlan,
|
|
1796
|
+
pricing?.units
|
|
1797
|
+
]);
|
|
1542
1798
|
const { upgrades, downgrades, privatePlans } = useMemo(() => {
|
|
1543
1799
|
const catalogItems = plansData?.items;
|
|
1544
1800
|
if (!catalogItems?.length) return {
|
|
@@ -1546,13 +1802,12 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1546
1802
|
downgrades: [],
|
|
1547
1803
|
privatePlans: []
|
|
1548
1804
|
};
|
|
1549
|
-
const planForComparison = resolvePlanForComparison(subscribedPlan, catalogItems);
|
|
1550
1805
|
const currentIndex = catalogItems.some((p) => p.id === subscribedPlan.id) ? catalogItems.findIndex((p) => p.id === subscribedPlan.id) : -1;
|
|
1551
1806
|
const subscribedIsPrivate = isPrivatePlan(subscribedPlan);
|
|
1552
|
-
const
|
|
1807
|
+
const entries = catalogItems.flatMap((plan, targetIndex) => {
|
|
1553
1808
|
if (plan.id === subscribedPlan.id) return [];
|
|
1554
1809
|
return [{
|
|
1555
|
-
|
|
1810
|
+
plan,
|
|
1556
1811
|
isUpgrade: resolveIsUpgrade({
|
|
1557
1812
|
target: plan,
|
|
1558
1813
|
targetIndex,
|
|
@@ -1563,20 +1818,32 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1563
1818
|
}];
|
|
1564
1819
|
});
|
|
1565
1820
|
if (subscribedIsPrivate) return {
|
|
1566
|
-
upgrades:
|
|
1821
|
+
upgrades: entries.filter((c) => !isPrivatePlan(c.plan)),
|
|
1567
1822
|
downgrades: [],
|
|
1568
|
-
privatePlans:
|
|
1823
|
+
privatePlans: entries.filter((c) => isPrivatePlan(c.plan))
|
|
1569
1824
|
};
|
|
1570
1825
|
return {
|
|
1571
|
-
upgrades:
|
|
1572
|
-
downgrades:
|
|
1573
|
-
privatePlans:
|
|
1826
|
+
upgrades: entries.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1827
|
+
downgrades: entries.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
|
|
1828
|
+
privatePlans: entries.filter((c) => isPrivatePlan(c.plan))
|
|
1574
1829
|
};
|
|
1575
|
-
}, [
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1830
|
+
}, [plansData?.items, subscribedPlan]);
|
|
1831
|
+
const renderCards = (entries, mode) => /* @__PURE__ */ jsx("div", {
|
|
1832
|
+
className: "space-y-3",
|
|
1833
|
+
children: entries.map(({ plan, isNewerVersion }) => /* @__PURE__ */ jsx(PlanChangeCard, {
|
|
1834
|
+
plan,
|
|
1835
|
+
mode,
|
|
1836
|
+
currentEntitlements,
|
|
1837
|
+
isNewerVersion,
|
|
1838
|
+
isSwitching: switchPlanMutation.isPending,
|
|
1839
|
+
units: pricing?.units,
|
|
1840
|
+
onSwitch: () => switchPlanMutation.mutate({
|
|
1841
|
+
subscriptionId: subscription.id,
|
|
1842
|
+
plan,
|
|
1843
|
+
mode
|
|
1844
|
+
})
|
|
1845
|
+
}, plan.id))
|
|
1846
|
+
});
|
|
1580
1847
|
return /* @__PURE__ */ jsxs(Dialog, {
|
|
1581
1848
|
open,
|
|
1582
1849
|
onOpenChange: setOpen,
|
|
@@ -1605,12 +1872,9 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1605
1872
|
children: switchPlanMutation.error.message
|
|
1606
1873
|
})
|
|
1607
1874
|
}),
|
|
1608
|
-
/* @__PURE__ */ jsx(
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
className: "text-lg font-bold",
|
|
1612
|
-
children: subscribedPlan.name
|
|
1613
|
-
})] })
|
|
1875
|
+
/* @__PURE__ */ jsx(CurrentPlanBaseline, {
|
|
1876
|
+
subscription,
|
|
1877
|
+
units: pricing?.units
|
|
1614
1878
|
}),
|
|
1615
1879
|
upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1616
1880
|
className: "flex items-center justify-between mb-3",
|
|
@@ -1624,16 +1888,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1624
1888
|
className: "text-sm text-muted-foreground",
|
|
1625
1889
|
children: "Takes effect immediately"
|
|
1626
1890
|
})]
|
|
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
|
-
})] }),
|
|
1891
|
+
}), renderCards(upgrades, "upgrade")] }),
|
|
1637
1892
|
downgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1638
1893
|
className: "flex items-center justify-between mb-3",
|
|
1639
1894
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1644,18 +1899,9 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1644
1899
|
})]
|
|
1645
1900
|
}), /* @__PURE__ */ jsx("span", {
|
|
1646
1901
|
className: "text-sm text-muted-foreground",
|
|
1647
|
-
children: "Takes effect next billing cycle"
|
|
1902
|
+
children: "Takes effect at your next billing cycle"
|
|
1648
1903
|
})]
|
|
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
|
-
})] }),
|
|
1904
|
+
}), renderCards(downgrades, "downgrade")] }),
|
|
1659
1905
|
privatePlans.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
1660
1906
|
className: "flex items-center justify-between mb-3",
|
|
1661
1907
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
@@ -1668,16 +1914,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
|
|
|
1668
1914
|
className: "text-sm text-muted-foreground",
|
|
1669
1915
|
children: "Takes effect immediately"
|
|
1670
1916
|
})]
|
|
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
|
-
})] })
|
|
1917
|
+
}), renderCards(privatePlans, "private")] })
|
|
1681
1918
|
]
|
|
1682
1919
|
})]
|
|
1683
1920
|
}) })]
|
|
@@ -1733,7 +1970,7 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1733
1970
|
variant: "outline",
|
|
1734
1971
|
size: "sm",
|
|
1735
1972
|
asChild: true,
|
|
1736
|
-
children: /* @__PURE__ */ jsxs(Link, {
|
|
1973
|
+
children: /* @__PURE__ */ jsxs(Link$1, {
|
|
1737
1974
|
to: "/pricing",
|
|
1738
1975
|
children: [/* @__PURE__ */ jsx(RefreshCcw, { className: "w-4 h-4 mr-2" }), "New subscription"]
|
|
1739
1976
|
})
|
|
@@ -1755,7 +1992,7 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1755
1992
|
asChild: true,
|
|
1756
1993
|
size: "sm",
|
|
1757
1994
|
variant: "secondary",
|
|
1758
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
1995
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
1759
1996
|
to: "/manage-payment",
|
|
1760
1997
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1761
1998
|
className: "flex items-center gap-2",
|
|
@@ -1780,140 +2017,14 @@ const ManageSubscription = ({ subscription, planName }) => {
|
|
|
1780
2017
|
//#region src/pages/subscriptions/SubscriptionPlanDetails.tsx
|
|
1781
2018
|
const detailLabelClassName = "text-sm font-semibold tracking-wide mb-1";
|
|
1782
2019
|
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
|
-
};
|
|
2020
|
+
const formatDateTimeRange = (from, to) => `${formatDateTime(from)} – ${formatDateTime(to)}`;
|
|
1896
2021
|
const SubscriptionPlanDetails = ({ subscription }) => {
|
|
1897
2022
|
const { pricing } = useMonetizationConfig();
|
|
1898
2023
|
const plan = subscription.plan;
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
2024
|
+
const view = getSubscriptionPlanView(subscription, { units: pricing?.units });
|
|
2025
|
+
const { priceLabel } = view;
|
|
1901
2026
|
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
|
-
});
|
|
2027
|
+
const hasEntitlements = hasSubscriptionEntitlements(view);
|
|
1917
2028
|
return /* @__PURE__ */ jsxs("div", {
|
|
1918
2029
|
className: "space-y-4",
|
|
1919
2030
|
children: [/* @__PURE__ */ jsx(Heading, {
|
|
@@ -1939,14 +2050,19 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
1939
2050
|
children: "Active since"
|
|
1940
2051
|
}), /* @__PURE__ */ jsx("dd", {
|
|
1941
2052
|
className: "text-foreground",
|
|
1942
|
-
children:
|
|
2053
|
+
children: formatDateTime(subscription.activeFrom)
|
|
1943
2054
|
})] }),
|
|
1944
2055
|
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
1945
2056
|
className: detailLabelClassName,
|
|
1946
2057
|
children: "Price"
|
|
1947
2058
|
}), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("div", {
|
|
1948
2059
|
className: "flex flex-wrap items-baseline gap-1",
|
|
1949
|
-
children:
|
|
2060
|
+
children: /* @__PURE__ */ jsx(PlanPriceTag, {
|
|
2061
|
+
label: priceLabel,
|
|
2062
|
+
currency: view.currency,
|
|
2063
|
+
billingCadence: view.billingCadence,
|
|
2064
|
+
description: true
|
|
2065
|
+
})
|
|
1950
2066
|
}), taxLegendSentence ? /* @__PURE__ */ jsx("p", {
|
|
1951
2067
|
className: "text-xs text-muted-foreground mt-1",
|
|
1952
2068
|
children: taxLegendSentence
|
|
@@ -1956,53 +2072,20 @@ const SubscriptionPlanDetails = ({ subscription }) => {
|
|
|
1956
2072
|
children: "Current period"
|
|
1957
2073
|
}), /* @__PURE__ */ jsx("dd", {
|
|
1958
2074
|
className: "text-foreground",
|
|
1959
|
-
children: subscription.alignment?.currentAlignedBillingPeriod ?
|
|
2075
|
+
children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateTimeRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
|
|
1960
2076
|
})] })
|
|
1961
2077
|
]
|
|
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
|
-
})
|
|
2078
|
+
}), hasEntitlements ? /* @__PURE__ */ jsxs("div", {
|
|
2079
|
+
className: "space-y-2 pt-2 border-t border-border",
|
|
2080
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2081
|
+
className: sectionLabelClassName,
|
|
2082
|
+
children: "What's included"
|
|
2083
|
+
}), /* @__PURE__ */ jsx(SubscriptionEntitlements, {
|
|
2084
|
+
view,
|
|
2085
|
+
currency: view.currency,
|
|
2086
|
+
billingCadence: view.billingCadence,
|
|
2087
|
+
units: pricing?.units
|
|
2088
|
+
})]
|
|
2006
2089
|
}) : null]
|
|
2007
2090
|
})] })]
|
|
2008
2091
|
});
|
|
@@ -2070,11 +2153,7 @@ const UsageItem = ({ meter, item, subscription, featureKey }) => {
|
|
|
2070
2153
|
}) })
|
|
2071
2154
|
]
|
|
2072
2155
|
}),
|
|
2073
|
-
/* @__PURE__ */
|
|
2074
|
-
item?.name ?? featureKey,
|
|
2075
|
-
" ",
|
|
2076
|
-
item?.price?.amount
|
|
2077
|
-
] })
|
|
2156
|
+
/* @__PURE__ */ jsx(CardTitle, { children: item?.name ?? featureKey })
|
|
2078
2157
|
] }), /* @__PURE__ */ jsxs(CardContent, {
|
|
2079
2158
|
className: "space-y-2",
|
|
2080
2159
|
children: [
|
|
@@ -2142,7 +2221,7 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
|
|
|
2142
2221
|
variant: "destructive",
|
|
2143
2222
|
size: "xs",
|
|
2144
2223
|
asChild: true,
|
|
2145
|
-
children: /* @__PURE__ */ jsx(Link, {
|
|
2224
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
2146
2225
|
to: "/manage-payment",
|
|
2147
2226
|
target: "_blank",
|
|
2148
2227
|
children: "Manage billing"
|
|
@@ -2254,7 +2333,7 @@ const SubscriptionsList = ({ subscriptions, activeSubscriptionId }) => {
|
|
|
2254
2333
|
};
|
|
2255
2334
|
const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
|
|
2256
2335
|
const willExpire = subscription.activeTo && new Date(subscription.activeTo) > /* @__PURE__ */ new Date();
|
|
2257
|
-
return /* @__PURE__ */ jsx(Link
|
|
2336
|
+
return /* @__PURE__ */ jsx(Link, {
|
|
2258
2337
|
to: `/subscriptions?subscriptionId=${encodeURIComponent(subscription.id)}`,
|
|
2259
2338
|
children: /* @__PURE__ */ jsx(Item, {
|
|
2260
2339
|
size: "sm",
|