@zuplo/zudoku-plugin-monetization 0.0.32 → 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.
Files changed (2) hide show
  1. package/dist/index.mjs +304 -176
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -8,8 +8,8 @@ import { Link as Link$1, Outlet, useLocation, useNavigate, useSearchParams } fro
8
8
  import { Alert, AlertAction, AlertDescription, AlertTitle } from "zudoku/ui/Alert";
9
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,72 +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
- //#region src/components/FeatureItem.tsx
27
- const FeatureItem = ({ feature, className }) => {
28
- return /* @__PURE__ */ jsxs("div", {
29
- className: cn("flex items-start gap-2", className),
30
- children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsx("div", {
31
- className: "text-sm",
32
- children: feature.value ? /* @__PURE__ */ jsxs(Fragment, { children: [
33
- /* @__PURE__ */ jsxs("span", {
34
- className: "font-medium",
35
- children: [feature.name, ":"]
36
- }),
37
- " ",
38
- feature.value
39
- ] }) : feature.name
40
- })]
41
- });
42
- };
43
- //#endregion
44
- //#region src/components/QuotaItem.tsx
45
- const QuotaItem = ({ quota, className }) => {
46
- return /* @__PURE__ */ jsxs("div", {
47
- className: cn("flex items-start gap-2", className),
48
- children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
49
- className: "text-sm",
50
- children: [
51
- /* @__PURE__ */ jsxs("span", {
52
- className: "font-medium",
53
- children: [quota.name, ":"]
54
- }),
55
- " ",
56
- quota.limit.toLocaleString(),
57
- " / ",
58
- quota.period,
59
- quota.overagePrice && /* @__PURE__ */ jsxs("div", {
60
- className: "text-xs text-muted-foreground mt-0.5",
61
- children: [
62
- "+",
63
- quota.overagePrice,
64
- " after quota"
65
- ]
66
- })
67
- ]
68
- })]
69
- });
70
- };
71
- //#endregion
72
- //#region src/hooks/useDeploymentName.ts
73
- const useDeploymentName = () => {
74
- const deploymentName = useZudoku().env.ZUPLO_PUBLIC_DEPLOYMENT_NAME;
75
- if (!deploymentName) throw new Error("ZUPLO_PUBLIC_DEPLOYMENT_NAME is not set");
76
- return deploymentName;
77
- };
78
- //#endregion
79
- //#region src/hooks/usePurchaseDetails.ts
80
- const usePurchaseDetails = (planId) => {
81
- const zudoku = useZudoku();
82
- return useSuspenseQuery({
83
- queryKey: [`/v3/zudoku-metering/${useDeploymentName()}/plans/${planId}/purchase-details`],
84
- meta: { context: zudoku }
85
- });
86
- };
87
- //#endregion
88
- //#region src/MonetizationContext.tsx
89
- const MonetizationContext = createContext({});
90
- const useMonetizationConfig = () => use(MonetizationContext);
91
- //#endregion
92
26
  //#region src/utils/formatDuration.ts
93
27
  const formatDuration = (iso) => {
94
28
  try {
@@ -165,6 +99,31 @@ const formatMinorCurrencyAmount = (amountInMinorUnits, currency) => {
165
99
  }).format(amountInMinorUnits / divisor);
166
100
  };
167
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
+ };
126
+ //#endregion
168
127
  //#region src/utils/categorizeRateCards.ts
169
128
  const categorizeRateCards = (rateCards, options) => {
170
129
  const { currency, units, planBillingCadence } = options ?? {};
@@ -175,20 +134,30 @@ const categorizeRateCards = (rateCards, options) => {
175
134
  if (!et) continue;
176
135
  if (et.type === "metered" && et.issueAfterReset != null) {
177
136
  let overagePrice;
178
- 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
+ });
179
151
  const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
180
- if (overageTier?.unitPrice) {
181
- const amount = parseFloat(overageTier.unitPrice.amount);
182
- const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
183
- overagePrice = `${formatPrice(amount, currency)}/${unitLabel}`;
184
- }
152
+ if (et.isSoftLimit !== false && overageTier?.unitPrice) overagePrice = `${formatPrice(parseFloat(overageTier.unitPrice.amount), currency)}/${unitLabel}`;
185
153
  }
186
154
  quotas.push({
187
155
  key: rc.featureKey ?? rc.key,
188
156
  name: rc.name,
189
157
  limit: et.issueAfterReset,
190
158
  period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
191
- overagePrice
159
+ overagePrice,
160
+ tierPrices
192
161
  });
193
162
  } else if (et.type === "boolean") features.push({
194
163
  key: rc.featureKey ?? rc.key,
@@ -214,6 +183,123 @@ const categorizeRateCards = (rateCards, options) => {
214
183
  };
215
184
  };
216
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);
302
+ //#endregion
217
303
  //#region src/utils/formatBillingCycle.ts
218
304
  const formatBillingCycle = (duration) => {
219
305
  if (duration === "month") return "monthly";
@@ -333,12 +419,6 @@ const CheckoutConfirmPage = () => {
333
419
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
334
420
  const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
335
421
  const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
336
- const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
337
- const { quotas, features } = categorizeRateCards(rateCards ?? [], {
338
- currency: selectedPlan?.currency,
339
- units: pricing?.units,
340
- planBillingCadence: selectedPlan?.billingCadence
341
- });
342
422
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
343
423
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
344
424
  const createSubscriptionMutation = useMutation({
@@ -434,15 +514,12 @@ const CheckoutConfirmPage = () => {
434
514
  className: "text-sm font-medium mb-3 mt-3",
435
515
  children: "What's included:"
436
516
  }),
437
- /* @__PURE__ */ jsxs("div", {
438
- className: "grid grid-cols-2 gap-2 text-muted-foreground",
439
- children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
440
- quota,
441
- className: "text-muted-foreground"
442
- }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
443
- feature,
444
- className: "text-muted-foreground"
445
- }, 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"
446
523
  })
447
524
  ] })]
448
525
  }),
@@ -641,41 +718,46 @@ const usePlans = () => {
641
718
  });
642
719
  };
643
720
  //#endregion
644
- //#region src/pages/pricing/PricingCard.tsx
645
- const PhaseSection = ({ phase, currency, showName, billingCadence }) => {
646
- const { pricing } = useMonetizationConfig();
647
- const { quotas, features } = categorizeRateCards(phase.rateCards, {
648
- currency,
649
- units: pricing?.units,
650
- planBillingCadence: billingCadence
651
- });
652
- if (quotas.length === 0 && features.length === 0) return null;
653
- return /* @__PURE__ */ jsxs("div", {
654
- className: "space-y-2",
655
- children: [
656
- showName && /* @__PURE__ */ jsxs("div", {
657
- className: "text-sm font-medium text-card-foreground",
658
- children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
659
- className: "text-muted-foreground font-normal",
660
- children: [
661
- " ",
662
- "— ",
663
- formatDuration(phase.duration)
664
- ]
665
- })]
666
- }),
667
- quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, { quota }, quota.key)),
668
- features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, { feature }, feature.key))
669
- ]
670
- });
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;
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
+ }
671
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
672
755
  const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
673
756
  const { pricing } = useMonetizationConfig();
674
757
  if (plan.phases.length === 0) return null;
675
758
  const price = getPriceFromPlan(plan);
676
759
  const isFree = price.monthly === 0;
677
760
  const isCustom = plan.metadata?.isCustom === true;
678
- const hasMultiplePhases = plan.phases.length > 1;
679
761
  const billingInterval = formatDuration(plan.billingCadence);
680
762
  return /* @__PURE__ */ jsxs("div", {
681
763
  className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2"),
@@ -721,12 +803,12 @@ const PricingCard = ({ plan, isPopular = false, isSubscribed = false }) => {
721
803
  }),
722
804
  /* @__PURE__ */ jsx("div", {
723
805
  className: "space-y-4 mb-6 grow",
724
- children: plan.phases.map((phase) => /* @__PURE__ */ jsx(PhaseSection, {
725
- phase,
806
+ children: /* @__PURE__ */ jsx(PlanEntitlements, {
807
+ phases: plan.phases,
726
808
  currency: plan.currency,
727
- showName: hasMultiplePhases,
728
- billingCadence: plan.billingCadence
729
- }, phase.key))
809
+ billingCadence: plan.billingCadence,
810
+ units: pricing?.units
811
+ })
730
812
  }),
731
813
  isSubscribed ? /* @__PURE__ */ jsx(Button, {
732
814
  variant: isPopular ? "default" : "secondary",
@@ -754,6 +836,7 @@ const PricingPage = () => {
754
836
  const deploymentName = useDeploymentName();
755
837
  const auth = useAuth();
756
838
  const { data: pricingTable } = usePlans();
839
+ const taxLegendSentence = taxBehaviorLegendSentence(collectDefaultTaxBehaviors(pricingTable.items[0]));
757
840
  const { data: subscriptions = { items: [] } } = useQuery({
758
841
  meta: { context: zudoku },
759
842
  queryKey: [`/v3/zudoku-metering/${deploymentName}/subscriptions`],
@@ -784,14 +867,24 @@ const PricingPage = () => {
784
867
  className: "text-sm mt-2",
785
868
  children: "Make sure your plans are set up and published."
786
869
  })]
787
- }) : /* @__PURE__ */ jsx("div", {
870
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
788
871
  className: "w-full grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(300px,max-content))] justify-center gap-6",
789
872
  children: pricingTable.items.map((plan) => /* @__PURE__ */ jsx(PricingCard, {
790
873
  plan,
791
874
  isPopular: plan.metadata?.zuplo_most_popular === "true",
792
875
  isSubscribed: subscriptions.items.some((subscription) => ["active", "canceled"].includes(subscription.status))
793
876
  }, plan.id))
794
- }),
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
+ })] }),
795
888
  /* @__PURE__ */ jsx(Slot.Target, { name: "pricing-page-after" })
796
889
  ]
797
890
  });
@@ -844,12 +937,6 @@ const SubscriptionChangeConfirmPage = () => {
844
937
  const taxAmount = getTaxAmountFromPurchaseDetails(purchaseDetails.data);
845
938
  const taxLabel = getTaxLabelFromPurchaseDetails(purchaseDetails.data);
846
939
  const taxInclusive = isTaxInclusiveFromPurchaseDetails(purchaseDetails.data);
847
- const rateCards = selectedPlan?.phases.at(-1)?.rateCards;
848
- const { quotas, features } = categorizeRateCards(rateCards ?? [], {
849
- currency: selectedPlan?.currency,
850
- units: pricing?.units,
851
- planBillingCadence: selectedPlan?.billingCadence
852
- });
853
940
  const price = selectedPlan ? getPriceFromPlan(selectedPlan) : null;
854
941
  const billingCycle = selectedPlan?.billingCadence ? formatDuration(selectedPlan.billingCadence) : null;
855
942
  const effectiveChangeMessage = mode === "downgrade" ? "This change will take effect at the start of your next billing cycle." : "This change will take effect immediately.";
@@ -951,15 +1038,11 @@ const SubscriptionChangeConfirmPage = () => {
951
1038
  className: "text-sm font-medium mb-3 mt-3",
952
1039
  children: "What's included:"
953
1040
  }),
954
- /* @__PURE__ */ jsxs("div", {
955
- className: "grid grid-cols-2 gap-2 text-muted-foreground",
956
- children: [quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
957
- quota,
958
- className: "text-muted-foreground"
959
- }, quota.key)), features.map((feature) => /* @__PURE__ */ jsx(FeatureItem, {
960
- feature,
961
- className: "text-muted-foreground"
962
- }, feature.key))]
1041
+ /* @__PURE__ */ jsx(PlanEntitlements, {
1042
+ phases: selectedPlan.phases,
1043
+ currency: selectedPlan.currency,
1044
+ billingCadence: selectedPlan.billingCadence,
1045
+ units: pricing?.units
963
1046
  })
964
1047
  ] })]
965
1048
  }),
@@ -2097,6 +2180,23 @@ const getOveragePriceFromItem = (item, currency, units) => {
2097
2180
  const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
2098
2181
  return `${formatPrice(parsed, currency)}/${unitLabel}`;
2099
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
+ };
2100
2200
  const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
2101
2201
  const features = [];
2102
2202
  for (const item of items) {
@@ -2110,7 +2210,8 @@ const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence
2110
2210
  name: item.name ?? item.featureKey ?? item.key,
2111
2211
  limit: entitlement.issueAfterReset,
2112
2212
  period: cadence ? formatDuration(cadence) : "month",
2113
- overagePrice: entitlement.isSoftLimit !== false ? getOveragePriceFromItem(item, currency, units) : void 0
2213
+ overagePrice: entitlement.isSoftLimit !== false ? getOveragePriceFromItem(item, currency, units) : void 0,
2214
+ tierPrices: getTierPricesFromItem(item, currency, units)
2114
2215
  });
2115
2216
  continue;
2116
2217
  }
@@ -2154,23 +2255,32 @@ const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence
2154
2255
  const getPhaseRows = (opts) => {
2155
2256
  const { subscription, currency, units } = opts;
2156
2257
  const phases = [...subscription.phases].sort((a, b) => new Date(a.activeFrom).getTime() - new Date(b.activeFrom).getTime());
2157
- const featureRows = [];
2258
+ const phaseGroups = [];
2158
2259
  for (const phase of phases) {
2159
2260
  const { features } = getEntitlementsFromItems(phase.items ?? [], currency, units, subscription.billingCadence);
2160
- for (const f of features) featureRows.push({
2261
+ const rows = [];
2262
+ for (const f of features) rows.push({
2161
2263
  key: f.key,
2162
2264
  name: f.name,
2163
2265
  entitlementType: f.entitlementType,
2164
2266
  limit: f.entitlementType === "metered" ? f.limit : void 0,
2165
2267
  period: f.entitlementType === "metered" ? f.period : void 0,
2166
2268
  overagePrice: f.entitlementType === "metered" ? f.overagePrice : void 0,
2269
+ tierPrices: f.entitlementType === "metered" ? f.tierPrices : void 0,
2167
2270
  value: f.entitlementType === "static" ? f.value : void 0,
2168
2271
  phaseId: phase.id,
2169
2272
  activeFrom: phase.activeFrom,
2170
2273
  activeTo: phase.activeTo
2171
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
+ });
2172
2282
  }
2173
- return { featureRows };
2283
+ return { phaseGroups };
2174
2284
  };
2175
2285
  const formatActiveRange = (activeFrom, activeTo) => {
2176
2286
  if (!activeTo) return `Starts ${formatDate$1(activeFrom)}`;
@@ -2181,6 +2291,7 @@ const SubscriptionPlanDetails = ({ subscription }) => {
2181
2291
  const plan = subscription.plan;
2182
2292
  const currency = subscription.currency ?? plan.currency;
2183
2293
  const priceInfo = getPriceFromPlan(plan);
2294
+ const taxLegendSentence = planHasDefaultTaxBehavior(plan) ? subscriptionTaxLegendSentence(plan.defaultTaxConfig?.behavior ?? "") : void 0;
2184
2295
  const primaryPrice = priceInfo.monthly === 0 && priceInfo.yearly === 0 ? /* @__PURE__ */ jsx("span", {
2185
2296
  className: "text-primary font-medium",
2186
2297
  children: "Free"
@@ -2191,7 +2302,7 @@ const SubscriptionPlanDetails = ({ subscription }) => {
2191
2302
  className: "text-muted-foreground",
2192
2303
  children: [" / ", formatDuration(plan.billingCadence)]
2193
2304
  })] });
2194
- const { featureRows } = getPhaseRows({
2305
+ const { phaseGroups } = getPhaseRows({
2195
2306
  subscription,
2196
2307
  currency,
2197
2308
  units: pricing?.units
@@ -2226,10 +2337,13 @@ const SubscriptionPlanDetails = ({ subscription }) => {
2226
2337
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2227
2338
  className: detailLabelClassName,
2228
2339
  children: "Price"
2229
- }), /* @__PURE__ */ jsx("dd", {
2340
+ }), /* @__PURE__ */ jsxs("dd", { children: [/* @__PURE__ */ jsx("div", {
2230
2341
  className: "flex flex-wrap items-baseline gap-1",
2231
2342
  children: primaryPrice
2232
- })] }),
2343
+ }), taxLegendSentence ? /* @__PURE__ */ jsx("p", {
2344
+ className: "text-xs text-muted-foreground mt-1",
2345
+ children: taxLegendSentence
2346
+ }) : null] })] }),
2233
2347
  /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2234
2348
  className: detailLabelClassName,
2235
2349
  children: "Current period"
@@ -2238,42 +2352,56 @@ const SubscriptionPlanDetails = ({ subscription }) => {
2238
2352
  children: subscription.alignment?.currentAlignedBillingPeriod ? formatDateRange(subscription.alignment.currentAlignedBillingPeriod.from, subscription.alignment.currentAlignedBillingPeriod.to) : "—"
2239
2353
  })] })
2240
2354
  ]
2241
- }), featureRows.length > 0 ? /* @__PURE__ */ jsx("div", {
2355
+ }), phaseGroups.length > 0 ? /* @__PURE__ */ jsx("div", {
2242
2356
  className: "space-y-5 pt-2 border-t border-border",
2243
2357
  children: /* @__PURE__ */ jsxs("div", {
2244
2358
  className: "space-y-2",
2245
2359
  children: [/* @__PURE__ */ jsx("p", {
2246
2360
  className: cn(sectionLabelClassName, "mb-5"),
2247
2361
  children: "Entitlements"
2248
- }), /* @__PURE__ */ jsx("ul", {
2249
- className: "space-y-3",
2250
- children: featureRows.map((row) => /* @__PURE__ */ jsxs("li", {
2251
- className: "grid gap-1 text-sm sm:grid-cols-4 sm:items-center sm:gap-4",
2252
- children: [
2253
- /* @__PURE__ */ jsx("div", {
2254
- className: "flex items-start gap-2 text-muted-foreground sm:col-span-2",
2255
- children: /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs("span", {
2256
- className: "text-foreground font-medium",
2257
- children: [row.name, " "]
2258
- }), row.entitlementType === "static" && row.value ? `: ${row.value}` : ""] })
2259
- }),
2260
- /* @__PURE__ */ jsx("div", {
2261
- className: "text-muted-foreground sm:text-right",
2262
- children: row.entitlementType === "metered" && row.limit != null ? /* @__PURE__ */ jsxs(Fragment, { children: [
2263
- formatNumber(row.limit),
2264
- row.period ? ` / ${row.period}` : "",
2265
- row.overagePrice ? /* @__PURE__ */ jsxs("div", {
2266
- className: "text-xs mt-0.5",
2267
- children: ["Overage: ", row.overagePrice]
2268
- }) : null
2269
- ] }) : row.entitlementType === "static" && row.value ? row.value : "Included"
2270
- }),
2271
- /* @__PURE__ */ jsx("div", {
2272
- className: "text-xs text-muted-foreground sm:text-right",
2273
- children: formatActiveRange(row.activeFrom, row.activeTo)
2274
- })
2275
- ]
2276
- }, `${row.key}:${row.phaseId}`))
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))
2277
2405
  })]
2278
2406
  })
2279
2407
  }) : null]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",