@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.
- package/dist/index.mjs +304 -176
- 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
|
-
|
|
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__ */
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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/
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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:
|
|
725
|
-
|
|
806
|
+
children: /* @__PURE__ */ jsx(PlanEntitlements, {
|
|
807
|
+
phases: plan.phases,
|
|
726
808
|
currency: plan.currency,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
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__ */
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
|
2258
|
+
const phaseGroups = [];
|
|
2158
2259
|
for (const phase of phases) {
|
|
2159
2260
|
const { features } = getEntitlementsFromItems(phase.items ?? [], currency, units, subscription.billingCadence);
|
|
2160
|
-
|
|
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 {
|
|
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 {
|
|
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__ */
|
|
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
|
-
}),
|
|
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("
|
|
2249
|
-
className: "space-y-
|
|
2250
|
-
children:
|
|
2251
|
-
className: "
|
|
2252
|
-
children: [
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
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]
|