@zuplo/zudoku-plugin-monetization 0.0.41 → 0.0.43

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