@zuplo/zudoku-plugin-monetization 0.0.31 → 0.0.33

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.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import "react";
2
- import * as zudoku from "zudoku";
1
+ import * as _$zudoku from "zudoku";
3
2
 
4
3
  //#region src/MonetizationContext.d.ts
5
4
  interface MonetizationConfig {
@@ -12,6 +11,6 @@ interface MonetizationConfig {
12
11
  }
13
12
  //#endregion
14
13
  //#region src/ZuploMonetizationPlugin.d.ts
15
- declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => zudoku.ZudokuPlugin;
14
+ declare const zuploMonetizationPlugin: (options?: MonetizationConfig | undefined) => _$zudoku.ZudokuPlugin;
16
15
  //#endregion
17
16
  export { zuploMonetizationPlugin };
package/dist/index.mjs CHANGED
@@ -6,10 +6,10 @@ import { useAuth, useZudoku } from "zudoku/hooks";
6
6
  import { QueryClient, QueryClientProvider, useMutation, useQuery, useQueryClient, useSuspenseQuery } from "zudoku/react-query";
7
7
  import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } from "zudoku/router";
8
8
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
9
- import { Card, CardContent, CardHeader, CardTitle } from "zudoku/ui/Card";
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "zudoku/ui/Card";
10
10
  import { Separator } from "zudoku/ui/Separator";
11
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
11
  import { parse } from "tinyduration";
12
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
13
  import { Button as Button$1 } from "zudoku/ui/Button";
14
14
  import { Skeleton } from "zudoku/ui/Skeleton";
15
15
  import { DismissibleAlert, DismissibleAlertAction } from "zudoku/ui/DismissibleAlert";
@@ -23,78 +23,6 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
23
23
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "zudoku/ui/Dialog";
24
24
  import { Input } from "zudoku/ui/Input";
25
25
  import { Progress } from "zudoku/ui/Progress";
26
-
27
- //#region src/components/FeatureItem.tsx
28
- const FeatureItem = ({ feature, className }) => {
29
- return /* @__PURE__ */ jsxs("div", {
30
- className: cn("flex items-start gap-2", className),
31
- children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
32
- className: "text-sm",
33
- children: feature.value ? /* @__PURE__ */ jsxs(Fragment, { children: [
34
- /* @__PURE__ */ jsxs("span", {
35
- className: "font-medium",
36
- children: [feature.name, ":"]
37
- }),
38
- " ",
39
- feature.value
40
- ] }) : feature.name
41
- })]
42
- });
43
- };
44
-
45
- //#endregion
46
- //#region src/components/QuotaItem.tsx
47
- const QuotaItem = ({ quota, className }) => {
48
- return /* @__PURE__ */ jsxs("div", {
49
- className: cn("flex items-start gap-2", className),
50
- children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
51
- className: "text-sm",
52
- children: [
53
- /* @__PURE__ */ jsxs("span", {
54
- className: "font-medium",
55
- children: [quota.name, ":"]
56
- }),
57
- " ",
58
- quota.limit.toLocaleString(),
59
- " / ",
60
- quota.period,
61
- quota.overagePrice && /* @__PURE__ */ jsxs("div", {
62
- className: "text-xs text-muted-foreground mt-0.5",
63
- children: [
64
- "+",
65
- quota.overagePrice,
66
- " after quota"
67
- ]
68
- })
69
- ]
70
- })]
71
- });
72
- };
73
-
74
- //#endregion
75
- //#region src/hooks/useDeploymentName.ts
76
- const useDeploymentName = () => {
77
- const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
78
- if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
79
- return deploymentName;
80
- };
81
-
82
- //#endregion
83
- //#region src/hooks/usePurchaseDetails.ts
84
- const usePurchaseDetails = (planId) => {
85
- const zudoku = useZudoku();
86
- return useSuspenseQuery({
87
- queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
88
- meta: { context: zudoku }
89
- });
90
- };
91
-
92
- //#endregion
93
- //#region src/MonetizationContext.tsx
94
- const MonetizationContext = createContext({});
95
- const useMonetizationConfig = () => use(MonetizationContext);
96
-
97
- //#endregion
98
26
  //#region src/utils/formatDuration.ts
99
27
  const formatDuration = (iso) => {
100
28
  try {
@@ -146,7 +74,6 @@ const formatDurationAdjective = (iso) => {
146
74
  return "billing period";
147
75
  }
148
76
  };
149
-
150
77
  //#endregion
151
78
  //#region src/utils/formatPrice.ts
152
79
  const formatPrice = (amount, currency) => new Intl.NumberFormat("en-US", {
@@ -171,7 +98,31 @@ const formatMinorCurrencyAmount = (amountInMinorUnits, currency) => {
171
98
  maximumFractionDigits: fractionDigits
172
99
  }).format(amountInMinorUnits / divisor);
173
100
  };
174
-
101
+ //#endregion
102
+ //#region src/utils/formatTieredPriceBreakdown.ts
103
+ const parseAmount = (value) => {
104
+ if (!value) return;
105
+ const parsed = Number.parseFloat(value);
106
+ return Number.isFinite(parsed) ? parsed : void 0;
107
+ };
108
+ const formatTieredPriceBreakdown = (opts) => {
109
+ const { tiers, currency, unitLabel, includedLabel, omitIncludedUpToAmount } = opts;
110
+ if (!tiers || tiers.length <= 1) return;
111
+ const lines = [];
112
+ let lastUpTo;
113
+ for (const tier of tiers) {
114
+ const upTo = parseAmount(tier.upToAmount);
115
+ const unit = parseAmount(tier.unitPriceAmount) ?? 0;
116
+ const flat = parseAmount(tier.flatPriceAmount) ?? 0;
117
+ const prefix = upTo != null ? `Up to ${upTo.toLocaleString("en-US")}` : lastUpTo != null ? `Over ${lastUpTo.toLocaleString("en-US")}` : `Per ${unitLabel}`;
118
+ const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : includedLabel;
119
+ const flatPart = flat > 0 ? ` + ${formatPrice(flat, currency)} base` : "";
120
+ const line = `${prefix}: ${unitPart}${flatPart}`;
121
+ if (omitIncludedUpToAmount != null && upTo != null && upTo === omitIncludedUpToAmount && unitPart === includedLabel && flatPart === "") {} else lines.push(line);
122
+ if (upTo != null) lastUpTo = upTo;
123
+ }
124
+ return lines.length > 0 ? lines : void 0;
125
+ };
175
126
  //#endregion
176
127
  //#region src/utils/categorizeRateCards.ts
177
128
  const categorizeRateCards = (rateCards, options) => {
@@ -183,20 +134,30 @@ const categorizeRateCards = (rateCards, options) => {
183
134
  if (!et) continue;
184
135
  if (et.type === "metered" && et.issueAfterReset != null) {
185
136
  let overagePrice;
186
- if (et.isSoftLimit !== false && rc.price?.type === "tiered" && rc.price.tiers) {
137
+ let tierPrices;
138
+ if (rc.price?.type === "tiered" && rc.price.tiers) {
139
+ const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
140
+ tierPrices = formatTieredPriceBreakdown({
141
+ tiers: rc.price.tiers.map((t) => ({
142
+ upToAmount: t.upToAmount,
143
+ unitPriceAmount: t.unitPrice?.amount,
144
+ flatPriceAmount: t.flatPrice?.amount
145
+ })),
146
+ currency,
147
+ unitLabel,
148
+ includedLabel: "Included",
149
+ omitIncludedUpToAmount: et.issueAfterReset
150
+ });
187
151
  const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
188
- if (overageTier?.unitPrice) {
189
- const amount = parseFloat(overageTier.unitPrice.amount);
190
- const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
191
- overagePrice = `${formatPrice(amount, currency)}/${unitLabel}`;
192
- }
152
+ if (et.isSoftLimit !== false && overageTier?.unitPrice) overagePrice = `${formatPrice(parseFloat(overageTier.unitPrice.amount), currency)}/${unitLabel}`;
193
153
  }
194
154
  quotas.push({
195
155
  key: rc.featureKey ?? rc.key,
196
156
  name: rc.name,
197
157
  limit: et.issueAfterReset,
198
158
  period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
199
- overagePrice
159
+ overagePrice,
160
+ tierPrices
200
161
  });
201
162
  } else if (et.type === "boolean") features.push({
202
163
  key: rc.featureKey ?? rc.key,
@@ -221,7 +182,123 @@ const categorizeRateCards = (rateCards, options) => {
221
182
  features
222
183
  };
223
184
  };
224
-
185
+ //#endregion
186
+ //#region src/components/FeatureItem.tsx
187
+ const FeatureItem = ({ feature, className }) => {
188
+ return /* @__PURE__ */ jsxs("div", {
189
+ className: cn("flex items-start gap-2", className),
190
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
191
+ className: "text-sm",
192
+ children: feature.value ? /* @__PURE__ */ jsxs(Fragment, { children: [
193
+ /* @__PURE__ */ jsxs("span", {
194
+ className: "font-medium",
195
+ children: [feature.name, ":"]
196
+ }),
197
+ " ",
198
+ feature.value
199
+ ] }) : feature.name
200
+ })]
201
+ });
202
+ };
203
+ //#endregion
204
+ //#region src/components/QuotaItem.tsx
205
+ const QuotaItem = ({ quota, className }) => {
206
+ return /* @__PURE__ */ jsxs("div", {
207
+ className: cn("flex items-start gap-2", className),
208
+ children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
209
+ className: "text-sm",
210
+ children: [
211
+ /* @__PURE__ */ jsxs("span", {
212
+ className: "font-medium",
213
+ children: [quota.name, ":"]
214
+ }),
215
+ " ",
216
+ quota.limit.toLocaleString(),
217
+ " / ",
218
+ quota.period,
219
+ quota.tierPrices && quota.tierPrices.length > 0 && /* @__PURE__ */ jsx("ul", {
220
+ className: "text-xs text-muted-foreground mt-1 space-y-0.5",
221
+ children: quota.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
222
+ }),
223
+ quota.overagePrice && /* @__PURE__ */ jsxs("div", {
224
+ className: "text-xs text-muted-foreground mt-0.5",
225
+ children: [
226
+ "+",
227
+ quota.overagePrice,
228
+ " after quota"
229
+ ]
230
+ })
231
+ ]
232
+ })]
233
+ });
234
+ };
235
+ //#endregion
236
+ //#region src/components/PlanEntitlements.tsx
237
+ const PhaseSection = ({ phase, currency, showName, billingCadence, units, itemClassName }) => {
238
+ const { quotas, features } = categorizeRateCards(phase.rateCards, {
239
+ currency,
240
+ units,
241
+ planBillingCadence: billingCadence
242
+ });
243
+ if (quotas.length === 0 && features.length === 0) return null;
244
+ return /* @__PURE__ */ jsxs("div", {
245
+ className: "space-y-2",
246
+ children: [
247
+ showName && /* @__PURE__ */ jsxs("div", {
248
+ className: "text-sm font-medium text-card-foreground",
249
+ children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
250
+ className: "text-muted-foreground font-normal",
251
+ children: [
252
+ " ",
253
+ "— ",
254
+ formatDuration(phase.duration)
255
+ ]
256
+ })]
257
+ }),
258
+ quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
259
+ quota,
260
+ className: itemClassName
261
+ }, quota.key)),
262
+ features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
263
+ feature,
264
+ className: itemClassName
265
+ }, feature.key))
266
+ ]
267
+ });
268
+ };
269
+ const PlanEntitlements = ({ phases, currency, billingCadence, units, itemClassName }) => {
270
+ return /* @__PURE__ */ jsx("div", {
271
+ className: "space-y-4",
272
+ children: phases.map((phase, idx) => /* @__PURE__ */ jsx(PhaseSection, {
273
+ phase,
274
+ currency,
275
+ showName: phases.length > 1,
276
+ billingCadence,
277
+ units,
278
+ itemClassName
279
+ }, phase.key ?? String(idx)))
280
+ });
281
+ };
282
+ //#endregion
283
+ //#region src/hooks/useDeploymentName.ts
284
+ const useDeploymentName = () => {
285
+ const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
286
+ if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
287
+ return deploymentName;
288
+ };
289
+ //#endregion
290
+ //#region src/hooks/usePurchaseDetails.ts
291
+ const usePurchaseDetails = (planId) => {
292
+ const zudoku = useZudoku();
293
+ return useSuspenseQuery({
294
+ queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
295
+ meta: { context: zudoku }
296
+ });
297
+ };
298
+ //#endregion
299
+ //#region src/MonetizationContext.tsx
300
+ const MonetizationContext = createContext({});
301
+ const useMonetizationConfig = () => use(MonetizationContext);
225
302
  //#endregion
226
303
  //#region src/utils/formatBillingCycle.ts
227
304
  const formatBillingCycle = (duration) => {
@@ -231,7 +308,6 @@ const formatBillingCycle = (duration) => {
231
308
  if (duration === "day") return "daily";
232
309
  return `every ${duration}`;
233
310
  };
234
-
235
311
  //#endregion
236
312
  //#region src/utils/getPriceFromPlan.ts
237
313
  const getPriceFromPlan = (plan) => {
@@ -240,7 +316,6 @@ const getPriceFromPlan = (plan) => {
240
316
  yearly: plan.yearlyPrice != null ? parseFloat(plan.yearlyPrice) : 0
241
317
  };
242
318
  };
243
-
244
319
  //#endregion
245
320
  //#region src/utils/purchaseDetails.ts
246
321
  const getPlanFromPurchaseDetails = (response) => {
@@ -258,7 +333,6 @@ const getTaxLabelFromPurchaseDetails = (response) => {
258
333
  const isTaxInclusiveFromPurchaseDetails = (response) => {
259
334
  return response.tax?.taxInclusive === true;
260
335
  };
261
-
262
336
  //#endregion
263
337
  //#region src/ZuploMonetizationWrapper.tsx
264
338
  const DEFAULT_GATEWAY_URL = "https://api.zuploedge.com";
@@ -330,7 +404,6 @@ const ZuploMonetizationWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(Query
330
404
  children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
331
405
  })
332
406
  });
333
-
334
407
  //#endregion
335
408
  //#region src/pages/CheckoutConfirmPage.tsx
336
409
  const CheckoutConfirmPage = () => {
@@ -346,12 +419,6 @@ const CheckoutConfirmPage = () => {
346
419
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
347
420
  const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
348
421
  const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
349
- const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
350
- const { quotas, features } = categorizeRateCards(rateCards ?? [], {
351
- currency: selectedPlan?.currency,
352
- units: pricing?.units,
353
- planBillingCadence: selectedPlan?.billingCadence
354
- });
355
422
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
356
423
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
357
424
  const createSubscriptionMutation = useMutation({
@@ -447,15 +514,12 @@ const CheckoutConfirmPage = () => {
447
514
  className: "text-sm font-medium mb-3 mt-3",
448
515
  children: "What's included:"
449
516
  }),
450
- /* @__PURE__ */ jsxs("div", {
451
- className: "grid grid-cols-2 gap-2 text-muted-foreground",
452
- children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
453
- quota,
454
- className: "text-muted-foreground"
455
- }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
456
- feature,
457
- className: "text-muted-foreground"
458
- }, feature.key))]
517
+ /* @__PURE__ */ jsx(PlanEntitlements, {
518
+ phases: selectedPlan.phases,
519
+ currency: selectedPlan.currency,
520
+ billingCadence: selectedPlan.billingCadence,
521
+ units: pricing?.units,
522
+ itemClassName: "text-muted-foreground"
459
523
  })
460
524
  ] })]
461
525
  }),
@@ -494,7 +558,6 @@ const CheckoutConfirmPage = () => {
494
558
  })
495
559
  });
496
560
  };
497
-
498
561
  //#endregion
499
562
  //#region src/components/RedirectPage.tsx
500
563
  const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
@@ -543,7 +606,6 @@ const RedirectPage = ({ icon: Icon, title, description, url, children }) => {
543
606
  })
544
607
  });
545
608
  };
546
-
547
609
  //#endregion
548
610
  //#region src/hooks/useUrlUtils.ts
549
611
  const useUrlUtils = () => {
@@ -553,7 +615,6 @@ const useUrlUtils = () => {
553
615
  return joinUrl(window.location.origin, basePath, path, searchParams ? `?${new URLSearchParams(searchParams)}` : void 0);
554
616
  } };
555
617
  };
556
-
557
618
  //#endregion
558
619
  //#region src/pages/CheckoutPage.tsx
559
620
  const CheckoutPage = () => {
@@ -604,7 +665,6 @@ const CheckoutPage = () => {
604
665
  })
605
666
  });
606
667
  };
607
-
608
668
  //#endregion
609
669
  //#region src/pages/ManagePaymentPage.tsx
610
670
  const ManagePaymentPage = () => {
@@ -647,7 +707,6 @@ const ManagePaymentPage = () => {
647
707
  })
648
708
  });
649
709
  };
650
-
651
710
  //#endregion
652
711
  //#region src/hooks/usePlans.ts
653
712
  const usePlans = () => {
@@ -658,43 +717,47 @@ const usePlans = () => {
658
717
  meta: { context: auth.isAuthenticated ? zudoku : void 0 }
659
718
  });
660
719
  };
661
-
662
720
  //#endregion
663
- //#region src/pages/pricing/PricingCard.tsx
664
- const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
665
- const { pricing } = useMonetizationConfig();
666
- const { quotas, features } = categorizeRateCards(phase.rateCards, {
667
- currency,
668
- units: pricing?.units,
669
- planBillingCadence: billingCadence
670
- });
671
- if (quotas.length === 0 && features.length === 0) return null;
672
- return /* @__PURE__ */ jsxs("div", {
673
- className: "space-y-2",
674
- children: [
675
- showName && /* @__PURE__ */ jsxs("div", {
676
- className: "text-sm font-medium text-card-foreground",
677
- children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
678
- className: "text-muted-foreground font-normal",
679
- children: [
680
- " ",
681
- "— ",
682
- formatDuration(phase.duration)
683
- ]
684
- })]
685
- }),
686
- quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
687
- features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
688
- ]
689
- });
721
+ //#region src/utils/pricingTaxLegend.ts
722
+ const normalizeTaxBehavior = (behavior) => {
723
+ switch (behavior.trim().toLowerCase()) {
724
+ case "exclusive":
725
+ case "tax_exclusive": return "exclusive";
726
+ case "inclusive":
727
+ case "tax_inclusive": return "inclusive";
728
+ default: return "unspecified";
729
+ }
730
+ };
731
+ const planHasDefaultTaxBehavior = (plan) => {
732
+ const behavior = plan.defaultTaxConfig?.behavior;
733
+ return typeof behavior === "string" && behavior.trim().length > 0;
690
734
  };
735
+ const collectDefaultTaxBehaviors = (plan) => {
736
+ const behavior = plan.defaultTaxConfig?.behavior;
737
+ return typeof behavior === "string" && behavior.trim().length > 0 ? normalizeTaxBehavior(behavior) : "unspecified";
738
+ };
739
+ const taxBehaviorLegendSentence = (behavior) => {
740
+ switch (normalizeTaxBehavior(behavior)) {
741
+ case "exclusive": return "Prices exclude tax; taxes may be added at checkout if applicable.";
742
+ case "inclusive": return "Prices include tax where applicable.";
743
+ default: return;
744
+ }
745
+ };
746
+ const subscriptionTaxLegendSentence = (behavior) => {
747
+ switch (normalizeTaxBehavior(behavior)) {
748
+ case "exclusive": return "Price excludes tax; taxes may be added on invoice if applicable.";
749
+ case "inclusive": return "Price includes tax where applicable.";
750
+ default: return;
751
+ }
752
+ };
753
+ //#endregion
754
+ //#region src/pages/pricing/PricingCard.tsx
691
755
  const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
692
756
  const { pricing } = useMonetizationConfig();
693
757
  if (plan.phases.length === 0) return null;
694
758
  const price = getPriceFromPlan(plan);
695
759
  const isFree = price.monthly === 0;
696
760
  const isCustom = plan.metadata?.isCustom === true;
697
- const hasMultiplePhases = plan.phases.length > 1;
698
761
  const billingInterval = formatDuration(plan.billingCadence);
699
762
  return /* @__PURE__ */ jsxs("div", {
700
763
  className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
@@ -740,12 +803,12 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
740
803
  }),
741
804
  /* @__PURE__ */ jsx("div", {
742
805
  className: "space-y-4 mb-6 grow",
743
- children: plan.phases.map((phase) => /* @__PURE__ */ jsx(PhaseSection, {
744
- phase,
806
+ children: /* @__PURE__ */ jsx(PlanEntitlements, {
807
+ phases: plan.phases,
745
808
  currency: plan.currency,
746
- showName: hasMultiplePhases,
747
- billingCadence: plan.billingCadence
748
- }, phase.key))
809
+ billingCadence: plan.billingCadence,
810
+ units: pricing?.units
811
+ })
749
812
  }),
750
813
  isSubscribed ? /* @__PURE__ */ jsx(Button, {
751
814
  variant: isPopular ? "default" : "secondary",
@@ -765,7 +828,6 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
765
828
  ]
766
829
  });
767
830
  };
768
-
769
831
  //#endregion
770
832
  //#region src/pages/PricingPage.tsx
771
833
  const PricingPage = () => {
@@ -774,6 +836,7 @@ const PricingPage = () => {
774
836
  const deploymentName = useDeploymentName();
775
837
  const auth = useAuth();
776
838
  const { data: pricingTable } = usePlans();
839
+ const taxLegendSentence = taxBehaviorLegendSentence(collectDefaultTaxBehaviors(pricingTable.items[0]));
777
840
  const { data: subscriptions = { items: [] } } = useQuery({
778
841
  meta: { context: zudoku },
779
842
  queryKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
@@ -804,19 +867,28 @@ const PricingPage = () => {
804
867
  className: "text-sm mt-2",
805
868
  children: "Make sure your plans are set up and published."
806
869
  })]
807
- }) : /* @__PURE__ */ jsx("div", {
870
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
808
871
  className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
809
872
  children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
810
873
  plan,
811
874
  isPopular: plan.metadata?.zuplo_most_popular === "true",
812
875
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
813
876
  }, plan.id))
814
- }),
877
+ }), taxLegendSentence && /* @__PURE__ */ jsxs("div", {
878
+ role: "note",
879
+ className: "mt-10 pt-6 border-t border-border max-w-2xl mx-auto text-center space-y-2",
880
+ children: [/* @__PURE__ */ jsx("p", {
881
+ className: "text-xs font-medium text-muted-foreground",
882
+ children: "Tax & Pricing"
883
+ }), /* @__PURE__ */ jsx("p", {
884
+ className: "text-xs text-muted-foreground",
885
+ children: taxLegendSentence
886
+ })]
887
+ })] }),
815
888
  /* @__PURE__ */ jsx(Slot.Target, { name: "pricing-page-after" })
816
889
  ]
817
890
  });
818
891
  };
819
-
820
892
  //#endregion
821
893
  //#region src/pages/PricingPageSkeleton.tsx
822
894
  const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
@@ -847,7 +919,6 @@ const PricingPageSkeleton = () => /* @__PURE__ */ jsxs("div", {
847
919
  }, i))
848
920
  })]
849
921
  });
850
-
851
922
  //#endregion
852
923
  //#region src/pages/SubscriptionChangeConfirmPage.tsx
853
924
  const SubscriptionChangeConfirmPage = () => {
@@ -866,12 +937,6 @@ const SubscriptionChangeConfirmPage = () => {
866
937
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
867
938
  const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
868
939
  const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
869
- const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
870
- const { quotas, features } = categorizeRateCards(rateCards ?? [], {
871
- currency: selectedPlan?.currency,
872
- units: pricing?.units,
873
- planBillingCadence: selectedPlan?.billingCadence
874
- });
875
940
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
876
941
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
877
942
  const effectiveChangeMessage = mode === "downgrade" ? "This change will take effect at the start of your next billing cycle." : "This change will take effect immediately.";
@@ -973,15 +1038,11 @@ const SubscriptionChangeConfirmPage = () => {
973
1038
  className: "text-sm font-medium mb-3 mt-3",
974
1039
  children: "What's included:"
975
1040
  }),
976
- /* @__PURE__ */ jsxs("div", {
977
- className: "grid grid-cols-2 gap-2 text-muted-foreground",
978
- children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
979
- quota,
980
- className: "text-muted-foreground"
981
- }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
982
- feature,
983
- className: "text-muted-foreground"
984
- }, feature.key))]
1041
+ /* @__PURE__ */ jsx(PlanEntitlements, {
1042
+ phases: selectedPlan.phases,
1043
+ currency: selectedPlan.currency,
1044
+ billingCadence: selectedPlan.billingCadence,
1045
+ units: pricing?.units
985
1046
  })
986
1047
  ] })]
987
1048
  }),
@@ -1015,7 +1076,6 @@ const SubscriptionChangeConfirmPage = () => {
1015
1076
  })
1016
1077
  });
1017
1078
  };
1018
-
1019
1079
  //#endregion
1020
1080
  //#region src/hooks/useSubscriptions.ts
1021
1081
  const useSubscriptions = (environmentName) => {
@@ -1033,7 +1093,6 @@ const useSubscriptions = (environmentName) => {
1033
1093
  })
1034
1094
  });
1035
1095
  };
1036
-
1037
1096
  //#endregion
1038
1097
  //#region src/pages/subscriptions/ConfirmDeleteKeyAlert.tsx
1039
1098
  const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
@@ -1045,10 +1104,9 @@ const ConfirmDeleteKeyAlert = ({ children, onDelete }) => {
1045
1104
  children: "Continue"
1046
1105
  })] })] })] });
1047
1106
  };
1048
-
1049
1107
  //#endregion
1050
1108
  //#region src/pages/subscriptions/ApiKey.tsx
1051
- const formatDate$1 = (dateString) => {
1109
+ const formatDate$2 = (dateString) => {
1052
1110
  if (!dateString) return "";
1053
1111
  return new Date(dateString).toLocaleDateString("en-US", {
1054
1112
  month: "short",
@@ -1059,8 +1117,7 @@ const formatDate$1 = (dateString) => {
1059
1117
  const getTimeAgo = (dateString) => {
1060
1118
  if (!dateString) return "Never";
1061
1119
  const date = new Date(dateString);
1062
- const now = /* @__PURE__ */ new Date();
1063
- const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1e3 * 60));
1120
+ const diffInMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - date.getTime()) / (1e3 * 60));
1064
1121
  if (diffInMinutes < 1) return "Just now";
1065
1122
  if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
1066
1123
  const diffInHours = Math.floor(diffInMinutes / 60);
@@ -1068,7 +1125,7 @@ const getTimeAgo = (dateString) => {
1068
1125
  const diffInDays = Math.floor(diffInHours / 24);
1069
1126
  if (diffInDays === 1) return "1 day ago";
1070
1127
  if (diffInDays < 30) return `${diffInDays} days ago`;
1071
- return formatDate$1(dateString);
1128
+ return formatDate$2(dateString);
1072
1129
  };
1073
1130
  const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label, onDelete }) => {
1074
1131
  const isExpiring = expiresOn && new Date(expiresOn) < new Date(Date.now() + 720 * 60 * 60 * 1e3);
@@ -1125,7 +1182,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1125
1182
  children: [
1126
1183
  /* @__PURE__ */ jsxs("div", {
1127
1184
  className: "flex items-center gap-1.5",
1128
- children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$1(createdAt)] })]
1185
+ children: [/* @__PURE__ */ jsx(ClockIcon, { className: "size-3" }), /* @__PURE__ */ jsxs("span", { children: ["Created ", formatDate$2(createdAt)] })]
1129
1186
  }),
1130
1187
  /* @__PURE__ */ jsx("span", {
1131
1188
  className: "text-muted-foreground/40",
@@ -1140,7 +1197,7 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1140
1197
  children: [
1141
1198
  isExpired ? "Expired" : "Expires",
1142
1199
  " on ",
1143
- formatDate$1(expiresOn)
1200
+ formatDate$2(expiresOn)
1144
1201
  ]
1145
1202
  })] })
1146
1203
  ]
@@ -1148,7 +1205,6 @@ const ApiKey = ({ apiKey, createdAt, lastUsed, expiresOn, isActive = true, label
1148
1205
  })]
1149
1206
  });
1150
1207
  };
1151
-
1152
1208
  //#endregion
1153
1209
  //#region src/pages/subscriptions/ApiKeyInfo.tsx
1154
1210
  const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
@@ -1168,7 +1224,6 @@ const ApiKeyInfo = () => /* @__PURE__ */ jsxs(DismissibleAlert, {
1168
1224
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1169
1225
  ]
1170
1226
  });
1171
-
1172
1227
  //#endregion
1173
1228
  //#region src/pages/subscriptions/ConfirmRollKeyAlert.tsx
1174
1229
  const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
@@ -1178,7 +1233,6 @@ const ConfirmRollKeyAlert = (props) => /* @__PURE__ */ jsxs(AlertDialog, { child
1178
1233
  onClick: props.onRollKey,
1179
1234
  children: "Continue"
1180
1235
  })] })] })] });
1181
-
1182
1236
  //#endregion
1183
1237
  //#region src/pages/subscriptions/ApiKeysList.tsx
1184
1238
  const PendingFirstPaymentAlert = ({ children }) => /* @__PURE__ */ jsxs("div", {
@@ -1258,7 +1312,7 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1258
1312
  /* @__PURE__ */ jsx(AlertTitle, { children: "API key was deleted" }),
1259
1313
  /* @__PURE__ */ jsx(AlertDescription, { children: (() => {
1260
1314
  const deletedKey = apiKeys.find((k) => k.id === deleteKeyMutation.variables?.keyId);
1261
- return deletedKey ? `API key created ${formatDate$1(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
1315
+ return deletedKey ? `API key created ${formatDate$2(deletedKey.createdOn)} has been removed.` : "The API key has been deleted.";
1262
1316
  })() }),
1263
1317
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
1264
1318
  ]
@@ -1330,7 +1384,6 @@ const ApiKeysList = ({ isPendingFirstPayment, apiKeys, deploymentName, consumerI
1330
1384
  ]
1331
1385
  });
1332
1386
  };
1333
-
1334
1387
  //#endregion
1335
1388
  //#region src/pages/subscriptions/CancelSubscriptionDialog.tsx
1336
1389
  const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1370,7 +1423,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1370
1423
  /* @__PURE__ */ jsx(AlertTitle, { children: "Your plan will be canceled at the end of your billing cycle." }),
1371
1424
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1372
1425
  "You'll retain access until ",
1373
- formatDate$1(billingPeriodEnd),
1426
+ formatDate$2(billingPeriodEnd),
1374
1427
  ". After your billing period ends, this plan will not renew and you would need to subscribe again to continue."
1375
1428
  ] })
1376
1429
  ]
@@ -1383,7 +1436,7 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1383
1436
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1384
1437
  "If you change your mind you have until",
1385
1438
  " ",
1386
- formatDate$1(billingPeriodEnd),
1439
+ formatDate$2(billingPeriodEnd),
1387
1440
  " to remove this cancellation from Manage subscription."
1388
1441
  ] })
1389
1442
  ]
@@ -1437,7 +1490,6 @@ const CancelSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId
1437
1490
  })
1438
1491
  });
1439
1492
  };
1440
-
1441
1493
  //#endregion
1442
1494
  //#region src/pages/subscriptions/RestoreSubscriptionDialog.tsx
1443
1495
  const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionId, billingPeriodEnd }) => {
@@ -1487,7 +1539,7 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1487
1539
  className: "space-y-2",
1488
1540
  children: [/* @__PURE__ */ jsxs("p", { children: [
1489
1541
  "Your access stays in place until ",
1490
- formatDate$1(billingPeriodEnd),
1542
+ formatDate$2(billingPeriodEnd),
1491
1543
  " ",
1492
1544
  "either way."
1493
1545
  ] }), /* @__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." })]
@@ -1519,7 +1571,6 @@ const RestoreSubscriptionDialog = ({ open, onOpenChange, planName, subscriptionI
1519
1571
  })
1520
1572
  });
1521
1573
  };
1522
-
1523
1574
  //#endregion
1524
1575
  //#region src/pages/subscriptions/SwitchPlanModal.tsx
1525
1576
  const getAllKeysAcrossPhases = (plan, units) => {
@@ -1669,6 +1720,7 @@ const ChangeIndicator = ({ change }) => {
1669
1720
  if (change === "decrease" || change === "removed" || change === "downgraded") return /* @__PURE__ */ jsx(ArrowDownIcon, { className: "w-4 h-4 text-amber-600 shrink-0" });
1670
1721
  return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
1671
1722
  };
1723
+ const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1672
1724
  const modeLabelMap = {
1673
1725
  upgrade: "Upgrade",
1674
1726
  downgrade: "Downgrade",
@@ -1859,12 +1911,29 @@ const SwitchPlanModal = ({ subscription, children }) => {
1859
1911
  });
1860
1912
  const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1861
1913
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1862
- if (!plansData?.items || !currentPlan) return {
1914
+ if (!plansData?.items) return {
1863
1915
  upgrades: [],
1864
1916
  downgrades: [],
1865
1917
  privatePlans: []
1866
1918
  };
1867
- const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1919
+ if (!currentPlan) {
1920
+ const currentIndex = -1;
1921
+ return {
1922
+ upgrades: plansData.items.map((plan, targetIndex) => comparePlans(void 0, plan, currentIndex, targetIndex, pricing?.units)).filter((c) => !isPrivatePlan(c.plan)),
1923
+ downgrades: [],
1924
+ privatePlans: []
1925
+ };
1926
+ }
1927
+ if (isPrivatePlan(currentPlan)) {
1928
+ const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1929
+ return {
1930
+ upgrades: plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1931
+ return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1932
+ }).filter((c) => !isPrivatePlan(c.plan)),
1933
+ downgrades: [],
1934
+ privatePlans: []
1935
+ };
1936
+ }
1868
1937
  const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1869
1938
  const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1870
1939
  return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
@@ -1914,6 +1983,13 @@ const SwitchPlanModal = ({ subscription, children }) => {
1914
1983
  children: currentPlan.name
1915
1984
  })] })
1916
1985
  }),
1986
+ !currentPlan && /* @__PURE__ */ jsx(Item, {
1987
+ variant: "outline",
1988
+ children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
1989
+ className: "text-lg font-bold",
1990
+ children: subscription.plan.name
1991
+ })] })
1992
+ }),
1917
1993
  upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
1918
1994
  className: "flex items-center justify-between mb-3",
1919
1995
  children: [/* @__PURE__ */ jsxs("div", {
@@ -1985,7 +2061,6 @@ const SwitchPlanModal = ({ subscription, children }) => {
1985
2061
  }) })]
1986
2062
  });
1987
2063
  };
1988
-
1989
2064
  //#endregion
1990
2065
  //#region src/pages/subscriptions/ManageSubscription.tsx
1991
2066
  const ManageSubscription = ({ subscription, planName }) => {
@@ -2077,7 +2152,262 @@ const ManageSubscription = ({ subscription, planName }) => {
2077
2152
  })
2078
2153
  ] });
2079
2154
  };
2080
-
2155
+ //#endregion
2156
+ //#region src/pages/subscriptions/SubscriptionPlanDetails.tsx
2157
+ const detailLabelClassName = "text-sm font-semibold tracking-wide mb-1";
2158
+ const sectionLabelClassName = "text-base font-semibold tracking-wide mb-3 mt-2";
2159
+ const formatDate$1 = (dateString) => {
2160
+ return new Date(dateString).toLocaleDateString("en-US", {
2161
+ month: "short",
2162
+ day: "numeric",
2163
+ year: "numeric"
2164
+ });
2165
+ };
2166
+ const formatDateRange = (from, to) => `${formatDate$1(from)} – ${formatDate$1(to)}`;
2167
+ const formatNumber = (value) => value.toLocaleString("en-US");
2168
+ const getOveragePriceFromItem = (item, currency, units) => {
2169
+ const tiers = item.price?.tiers;
2170
+ if (!tiers || tiers.length === 0) return void 0;
2171
+ const amount = tiers.find((t) => {
2172
+ const amount = t.unitPrice?.amount;
2173
+ if (!amount) return false;
2174
+ const parsed = parseFloat(amount);
2175
+ return Number.isFinite(parsed) && parsed > 0;
2176
+ })?.unitPrice?.amount;
2177
+ if (!amount) return void 0;
2178
+ const parsed = parseFloat(amount);
2179
+ if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
2180
+ const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
2181
+ return `${formatPrice(parsed, currency)}/${unitLabel}`;
2182
+ };
2183
+ const getTierPricesFromItem = (item, currency, units) => {
2184
+ if (item.price?.type !== "tiered") return;
2185
+ const tiers = item.price.tiers;
2186
+ if (!tiers || tiers.length <= 1) return;
2187
+ const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
2188
+ return formatTieredPriceBreakdown({
2189
+ tiers: tiers.map((t) => ({
2190
+ upToAmount: t.upToAmount,
2191
+ unitPriceAmount: t.unitPrice?.amount,
2192
+ flatPriceAmount: t.flatPrice?.amount
2193
+ })),
2194
+ currency,
2195
+ unitLabel,
2196
+ includedLabel: "Included",
2197
+ omitIncludedUpToAmount: item.included?.entitlement?.issueAfterReset
2198
+ });
2199
+ };
2200
+ const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
2201
+ const features = [];
2202
+ for (const item of items) {
2203
+ const entitlement = item.included?.entitlement;
2204
+ if (!entitlement) continue;
2205
+ if (entitlement.type === "metered" && entitlement.issueAfterReset != null) {
2206
+ const cadence = item.billingCadence ?? fallbackBillingCadence;
2207
+ features.push({
2208
+ entitlementType: "metered",
2209
+ key: item.featureKey ?? item.key,
2210
+ name: item.name ?? item.featureKey ?? item.key,
2211
+ limit: entitlement.issueAfterReset,
2212
+ period: cadence ? formatDuration(cadence) : "month",
2213
+ overagePrice: entitlement.isSoftLimit !== false ? getOveragePriceFromItem(item, currency, units) : void 0,
2214
+ tierPrices: getTierPricesFromItem(item, currency, units)
2215
+ });
2216
+ continue;
2217
+ }
2218
+ if (entitlement.type === "boolean") {
2219
+ features.push({
2220
+ entitlementType: "boolean",
2221
+ key: item.featureKey ?? item.key,
2222
+ name: item.name ?? item.featureKey ?? item.key
2223
+ });
2224
+ continue;
2225
+ }
2226
+ if (entitlement.type === "static") {
2227
+ const base = {
2228
+ key: item.featureKey ?? item.key,
2229
+ name: item.name ?? item.featureKey ?? item.key
2230
+ };
2231
+ if (!entitlement.config) {
2232
+ features.push({
2233
+ entitlementType: "static",
2234
+ ...base
2235
+ });
2236
+ continue;
2237
+ }
2238
+ try {
2239
+ const parsed = JSON.parse(entitlement.config);
2240
+ features.push({
2241
+ entitlementType: "static",
2242
+ ...base,
2243
+ value: parsed?.value != null ? String(parsed.value) : void 0
2244
+ });
2245
+ } catch {
2246
+ features.push({
2247
+ entitlementType: "static",
2248
+ ...base
2249
+ });
2250
+ }
2251
+ }
2252
+ }
2253
+ return { features };
2254
+ };
2255
+ const getPhaseRows = (opts) => {
2256
+ const { subscription, currency, units } = opts;
2257
+ const phases = [...subscription.phases].sort((a, b) => new Date(a.activeFrom).getTime() - new Date(b.activeFrom).getTime());
2258
+ const phaseGroups = [];
2259
+ for (const phase of phases) {
2260
+ const { features } = getEntitlementsFromItems(phase.items ?? [], currency, units, subscription.billingCadence);
2261
+ const rows = [];
2262
+ for (const f of features) rows.push({
2263
+ key: f.key,
2264
+ name: f.name,
2265
+ entitlementType: f.entitlementType,
2266
+ limit: f.entitlementType === "metered" ? f.limit : void 0,
2267
+ period: f.entitlementType === "metered" ? f.period : void 0,
2268
+ overagePrice: f.entitlementType === "metered" ? f.overagePrice : void 0,
2269
+ tierPrices: f.entitlementType === "metered" ? f.tierPrices : void 0,
2270
+ value: f.entitlementType === "static" ? f.value : void 0,
2271
+ phaseId: phase.id,
2272
+ activeFrom: phase.activeFrom,
2273
+ activeTo: phase.activeTo
2274
+ });
2275
+ if (rows.length > 0) phaseGroups.push({
2276
+ id: phase.id,
2277
+ name: phase.name,
2278
+ activeFrom: phase.activeFrom,
2279
+ activeTo: phase.activeTo,
2280
+ rows
2281
+ });
2282
+ }
2283
+ return { phaseGroups };
2284
+ };
2285
+ const formatActiveRange = (activeFrom, activeTo) => {
2286
+ if (!activeTo) return `Starts ${formatDate$1(activeFrom)}`;
2287
+ return `${formatDate$1(activeFrom)} – ${formatDate$1(activeTo)}`;
2288
+ };
2289
+ const SubscriptionPlanDetails = ({ subscription }) => {
2290
+ const { pricing } = useMonetizationConfig();
2291
+ const plan = subscription.plan;
2292
+ const currency = subscription.currency ?? plan.currency;
2293
+ const priceInfo = getPriceFromPlan(plan);
2294
+ const taxLegendSentence = planHasDefaultTaxBehavior(plan) ? subscriptionTaxLegendSentence(plan.defaultTaxConfig?.behavior ?? "") : void 0;
2295
+ const primaryPrice = priceInfo.monthly === 0 && priceInfo.yearly === 0 ? /* @__PURE__ */ jsx("span", {
2296
+ className: "text-primary font-medium",
2297
+ children: "Free"
2298
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2299
+ className: "text-primary font-medium text-lg",
2300
+ children: formatPrice(priceInfo.monthly, currency)
2301
+ }), /* @__PURE__ */ jsxs("span", {
2302
+ className: "text-muted-foreground",
2303
+ children: [" / ", formatDuration(plan.billingCadence)]
2304
+ })] });
2305
+ const { phaseGroups } = getPhaseRows({
2306
+ subscription,
2307
+ currency,
2308
+ units: pricing?.units
2309
+ });
2310
+ return /* @__PURE__ */ jsxs("div", {
2311
+ className: "space-y-4",
2312
+ children: [/* @__PURE__ */ jsx(Heading, {
2313
+ level: 3,
2314
+ children: "Subscription Details"
2315
+ }), /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, { children: [/* @__PURE__ */ jsx(CardTitle, {
2316
+ className: "text-lg font-semibold leading-tight",
2317
+ children: plan.name
2318
+ }), plan.description ? /* @__PURE__ */ jsx(CardDescription, { children: plan.description }) : null] }), /* @__PURE__ */ jsxs(CardContent, {
2319
+ className: "space-y-6",
2320
+ children: [/* @__PURE__ */ jsxs("dl", {
2321
+ className: "grid gap-4 sm:grid-cols-2 text-sm",
2322
+ children: [
2323
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2324
+ className: detailLabelClassName,
2325
+ children: "Subscription ID"
2326
+ }), /* @__PURE__ */ jsx("dd", {
2327
+ className: "text-foreground font-mono text-xs break-all",
2328
+ children: subscription.id
2329
+ })] }),
2330
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2331
+ className: detailLabelClassName,
2332
+ children: "Active since"
2333
+ }), /* @__PURE__ */ jsx("dd", {
2334
+ className: "text-foreground",
2335
+ children: formatDate$1(subscription.activeFrom)
2336
+ })] }),
2337
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2338
+ className: detailLabelClassName,
2339
+ children: "Price"
2340
+ }), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("div", {
2341
+ className: "flex flex-wrap items-baseline gap-1",
2342
+ children: primaryPrice
2343
+ }), taxLegendSentence ? /* @__PURE__ */ jsx("p", {
2344
+ className: "text-xs text-muted-foreground mt-1",
2345
+ children: taxLegendSentence
2346
+ }) : null] })] }),
2347
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2348
+ className: detailLabelClassName,
2349
+ children: "Current period"
2350
+ }), /* @__PURE__ */ jsx("dd", {
2351
+ className: "text-foreground",
2352
+ children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
2353
+ })] })
2354
+ ]
2355
+ }), phaseGroups.length > 0 ? /* @__PURE__ */ jsx("div", {
2356
+ className: "space-y-5 pt-2 border-t border-border",
2357
+ children: /* @__PURE__ */ jsxs("div", {
2358
+ className: "space-y-2",
2359
+ children: [/* @__PURE__ */ jsx("p", {
2360
+ className: cn(sectionLabelClassName, "mb-5"),
2361
+ children: "Entitlements"
2362
+ }), /* @__PURE__ */ jsx("div", {
2363
+ className: "space-y-5",
2364
+ children: phaseGroups.map((phase) => /* @__PURE__ */ jsxs("div", {
2365
+ className: "space-y-3",
2366
+ children: [phaseGroups.length > 1 ? /* @__PURE__ */ jsxs("div", {
2367
+ className: "text-sm font-medium text-card-foreground",
2368
+ children: [phase.name, /* @__PURE__ */ jsxs("span", {
2369
+ className: "text-muted-foreground font-normal",
2370
+ children: [
2371
+ " ",
2372
+ "—",
2373
+ " ",
2374
+ formatActiveRange(phase.activeFrom, phase.activeTo)
2375
+ ]
2376
+ })]
2377
+ }) : null, /* @__PURE__ */ jsx("ul", {
2378
+ className: "space-y-3",
2379
+ children: phase.rows.map((row) => /* @__PURE__ */ jsx("li", {
2380
+ className: "text-sm",
2381
+ children: /* @__PURE__ */ jsxs("div", {
2382
+ className: "flex flex-col gap-1",
2383
+ children: [/* @__PURE__ */ jsxs("div", {
2384
+ className: "text-foreground font-medium",
2385
+ children: [row.name, row.entitlementType === "static" && row.value ? `: ${row.value}` : ""]
2386
+ }), /* @__PURE__ */ jsx("div", {
2387
+ className: "text-muted-foreground",
2388
+ children: row.entitlementType === "metered" && row.limit != null ? /* @__PURE__ */ jsxs(Fragment, { children: [
2389
+ formatNumber(row.limit),
2390
+ row.period ? ` / ${row.period}` : "",
2391
+ row.tierPrices && row.tierPrices.length > 0 ? /* @__PURE__ */ jsx("ul", {
2392
+ className: "text-xs mt-1 space-y-0.5",
2393
+ children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
2394
+ }) : null,
2395
+ row.overagePrice ? /* @__PURE__ */ jsxs("div", {
2396
+ className: "text-xs mt-0.5",
2397
+ children: ["Overage: ", row.overagePrice]
2398
+ }) : null
2399
+ ] }) : row.entitlementType === "static" && row.value ? null : "Included"
2400
+ })]
2401
+ })
2402
+ }, `${row.key}:${row.phaseId}`))
2403
+ })]
2404
+ }, phase.id))
2405
+ })]
2406
+ })
2407
+ }) : null]
2408
+ })] })]
2409
+ });
2410
+ };
2081
2411
  //#endregion
2082
2412
  //#region src/pages/subscriptions/Usage.tsx
2083
2413
  const isMeteredEntitlement = (entitlement) => {
@@ -2237,7 +2567,6 @@ const Usage = ({ usage, isFetching, currentItems, subscription, isPendingFirstPa
2237
2567
  ]
2238
2568
  });
2239
2569
  };
2240
-
2241
2570
  //#endregion
2242
2571
  //#region src/pages/subscriptions/ActiveSubscription.tsx
2243
2572
  const ActiveSubscription = ({ subscription, deploymentName }) => {
@@ -2261,6 +2590,7 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2261
2590
  /* @__PURE__ */ jsx(DismissibleAlertAction, {})
2262
2591
  ]
2263
2592
  }),
2593
+ /* @__PURE__ */ jsx(SubscriptionPlanDetails, { subscription }),
2264
2594
  /* @__PURE__ */ jsx(Usage, {
2265
2595
  currentItems: activePhase?.items,
2266
2596
  usage: usageQuery.data,
@@ -2280,7 +2610,6 @@ const ActiveSubscription = ({ subscription, deploymentName }) => {
2280
2610
  })
2281
2611
  ] });
2282
2612
  };
2283
-
2284
2613
  //#endregion
2285
2614
  //#region src/pages/subscriptions/SubscriptionsList.tsx
2286
2615
  const formatDate = (dateString) => {
@@ -2354,7 +2683,6 @@ const SubscriptionItem = ({ subscription, isSelected, isExpired }) => {
2354
2683
  }, subscription.id)
2355
2684
  });
2356
2685
  };
2357
-
2358
2686
  //#endregion
2359
2687
  //#region src/pages/SubscriptionsPage.tsx
2360
2688
  const SubscriptionsPage = () => {
@@ -2399,7 +2727,6 @@ const SubscriptionsPage = () => {
2399
2727
  })]
2400
2728
  });
2401
2729
  };
2402
-
2403
2730
  //#endregion
2404
2731
  //#region src/pages/SubscriptionsPageSkeleton.tsx
2405
2732
  const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
@@ -2433,7 +2760,6 @@ const SubscriptionsPageSkeleton = () => /* @__PURE__ */ jsx("div", {
2433
2760
  ]
2434
2761
  })
2435
2762
  });
2436
-
2437
2763
  //#endregion
2438
2764
  //#region src/ZuploMonetizationPlugin.tsx
2439
2765
  const PRICING_PATH = "/pricing";
@@ -2520,6 +2846,5 @@ const zuploMonetizationPlugin = createPlugin((options = {}) => ({
2520
2846
  ];
2521
2847
  }
2522
2848
  }));
2523
-
2524
2849
  //#endregion
2525
- export { zuploMonetizationPlugin };
2850
+ export { zuploMonetizationPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -27,11 +27,11 @@
27
27
  "@testing-library/react": "16.3.2",
28
28
  "@types/react": "19.2.14",
29
29
  "@types/react-dom": "19.2.3",
30
- "happy-dom": "20.8.9",
31
- "react": "19.2.4",
32
- "react-dom": "19.2.4",
33
- "tsdown": "0.20.3",
34
- "zudoku": "0.75.0"
30
+ "happy-dom": "20.9.0",
31
+ "react": "19.2.5",
32
+ "react-dom": "19.2.5",
33
+ "tsdown": "0.21.9",
34
+ "zudoku": "0.76.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=19.2.0",