@zuplo/zudoku-plugin-monetization 0.0.38 → 0.0.40

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.
@@ -15,6 +15,12 @@ const formatDuration = (iso) => {
15
15
  if (d.weeks && d.weeks > 1) return `${d.weeks} weeks`;
16
16
  if (d.days === 1) return "day";
17
17
  if (d.days && d.days > 1) return `${d.days} days`;
18
+ if (d.hours === 1) return "hour";
19
+ if (d.hours && d.hours > 1) return `${d.hours} hours`;
20
+ if (d.minutes === 1) return "minute";
21
+ if (d.minutes && d.minutes > 1) return `${d.minutes} minutes`;
22
+ if (d.seconds === 1) return "second";
23
+ if (d.seconds && d.seconds > 1) return `${d.seconds} seconds`;
18
24
  return iso;
19
25
  } catch {
20
26
  return iso;
@@ -31,6 +37,12 @@ const formatDurationInterval = (iso) => {
31
37
  if (d.weeks && d.weeks > 1) return `every ${d.weeks} weeks`;
32
38
  if (d.days === 1) return "daily";
33
39
  if (d.days && d.days > 1) return `every ${d.days} days`;
40
+ if (d.hours === 1) return "hourly";
41
+ if (d.hours && d.hours > 1) return `every ${d.hours} hours`;
42
+ if (d.minutes === 1) return "every minute";
43
+ if (d.minutes && d.minutes > 1) return `every ${d.minutes} minutes`;
44
+ if (d.seconds === 1) return "every second";
45
+ if (d.seconds && d.seconds > 1) return `every ${d.seconds} seconds`;
34
46
  return iso;
35
47
  } catch {
36
48
  return iso;
@@ -40,7 +52,8 @@ const formatDurationInterval = (iso) => {
40
52
  * Returns an adjective form suitable for possessive context
41
53
  * e.g. "your monthly quota", "your weekly limit".
42
54
  * Falls back to "billing period" for multi-unit cadences
43
- * where "every 3 months" would be grammatically awkward.
55
+ * or sub-hour units where the adjective form is grammatically awkward
56
+ * (e.g. "every 3 months", "every 5 minutes").
44
57
  */
45
58
  const formatDurationAdjective = (iso) => {
46
59
  try {
@@ -49,6 +62,7 @@ const formatDurationAdjective = (iso) => {
49
62
  if (d.months === 1) return "monthly";
50
63
  if (d.weeks === 1) return "weekly";
51
64
  if (d.days === 1) return "daily";
65
+ if (d.hours === 1) return "hourly";
52
66
  return "billing period";
53
67
  } catch {
54
68
  return "billing period";
@@ -104,7 +118,7 @@ const parseAmount = (value) => {
104
118
  return Number.isFinite(parsed) ? parsed : void 0;
105
119
  };
106
120
  const formatTieredPriceBreakdown = (opts) => {
107
- const { tiers, currency, unitLabel, includedLabel, omitIncludedUpToAmount } = opts;
121
+ const { tiers, currency, unitLabel, includedLabel } = opts;
108
122
  if (!tiers || tiers.length <= 1) return;
109
123
  const lines = [];
110
124
  let lastUpTo;
@@ -113,10 +127,10 @@ const formatTieredPriceBreakdown = (opts) => {
113
127
  const unit = parseAmount(tier.unitPriceAmount) ?? 0;
114
128
  const flat = parseAmount(tier.flatPriceAmount) ?? 0;
115
129
  const prefix = upTo != null ? `Up to ${upTo.toLocaleString("en-US")}` : lastUpTo != null ? `Over ${lastUpTo.toLocaleString("en-US")}` : `Per ${unitLabel}`;
116
- const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : includedLabel;
117
- const flatPart = flat > 0 ? ` + ${formatPrice(flat, currency)} base` : "";
118
- const line = `${prefix}: ${unitPart}${flatPart}`;
119
- if (omitIncludedUpToAmount != null && upTo != null && upTo === omitIncludedUpToAmount && unitPart === includedLabel && flatPart === "") {} else lines.push(line);
130
+ const flatPart = flat > 0 ? formatPrice(flat, currency) : "";
131
+ const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : "";
132
+ const pricePart = flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart || includedLabel;
133
+ lines.push(`${prefix}: ${pricePart}`);
120
134
  if (upTo != null) lastUpTo = upTo;
121
135
  }
122
136
  return lines.length > 0 ? lines : void 0;
@@ -130,32 +144,99 @@ const categorizeRateCards = (rateCards, options) => {
130
144
  for (const rc of rateCards) {
131
145
  const et = rc.entitlementTemplate;
132
146
  if (!et) continue;
133
- if (et.type === "metered" && et.issueAfterReset != null) {
134
- let overagePrice;
147
+ const unitLabelFor = (rcArg) => units?.[rcArg.key] ?? units?.[rcArg.featureKey ?? ""] ?? "unit";
148
+ const periodFor = (rcArg) => {
149
+ if (et.type === "metered" && et.usagePeriod) return formatDuration(et.usagePeriod);
150
+ if (rcArg.billingCadence) return formatDuration(rcArg.billingCadence);
151
+ if (planBillingCadence) return formatDuration(planBillingCadence);
152
+ return "month";
153
+ };
154
+ const firstTier = rc.price?.type === "tiered" && rc.price.tiers.length > 0 ? rc.price.tiers[0] : void 0;
155
+ const firstTierIsPriced = !!firstTier && (parseFloat(firstTier.flatPrice?.amount ?? "0") > 0 || parseFloat(firstTier.unitPrice?.amount ?? "0") > 0);
156
+ if (et.type === "metered" && et.issueAfterReset != null && !firstTierIsPriced) {
135
157
  let tierPrices;
136
- if (rc.price?.type === "tiered" && rc.price.tiers) {
137
- const unitLabel = units?.[rc.key] ?? units?.[rc.featureKey ?? ""] ?? "unit";
138
- tierPrices = formatTieredPriceBreakdown({
139
- tiers: rc.price.tiers.map((t) => ({
158
+ if (rc.price?.type === "tiered" && rc.price.tiers) tierPrices = formatTieredPriceBreakdown({
159
+ tiers: rc.price.tiers.map((t) => ({
160
+ upToAmount: t.upToAmount,
161
+ unitPriceAmount: t.unitPrice?.amount,
162
+ flatPriceAmount: t.flatPrice?.amount
163
+ })),
164
+ currency,
165
+ unitLabel: unitLabelFor(rc),
166
+ includedLabel: "Included"
167
+ });
168
+ quotas.push({
169
+ key: rc.featureKey ?? rc.key,
170
+ name: rc.name,
171
+ limit: et.issueAfterReset,
172
+ period: periodFor(rc),
173
+ tierPrices
174
+ });
175
+ } else if (et.type === "metered" && rc.type === "usage_based" && rc.price) {
176
+ const unitLabel = unitLabelFor(rc);
177
+ if (rc.price.type === "tiered" && rc.price.tiers.length > 0) {
178
+ const tiers = rc.price.tiers;
179
+ if (!tiers.some((t) => parseFloat(t.flatPrice?.amount ?? "0") > 0 || parseFloat(t.unitPrice?.amount ?? "0") > 0)) {
180
+ features.push({
181
+ key: rc.featureKey ?? rc.key,
182
+ name: rc.name
183
+ });
184
+ continue;
185
+ }
186
+ if (tiers.length === 1) {
187
+ const unit = parseFloat(tiers[0].unitPrice?.amount ?? "0");
188
+ const flat = parseFloat(tiers[0].flatPrice?.amount ?? "0");
189
+ const flatPart = flat > 0 ? formatPrice(flat, currency) : "";
190
+ const unitPart = unit > 0 ? `${formatPrice(unit, currency)}/${unitLabel}` : "";
191
+ const pricePart = flatPart && unitPart ? `${flatPart} + ${unitPart}` : flatPart || unitPart;
192
+ if (pricePart) {
193
+ quotas.push({
194
+ key: rc.featureKey ?? rc.key,
195
+ name: rc.name,
196
+ limit: 0,
197
+ period: periodFor(rc),
198
+ unitPrice: pricePart,
199
+ isPayg: true
200
+ });
201
+ continue;
202
+ }
203
+ features.push({
204
+ key: rc.featureKey ?? rc.key,
205
+ name: rc.name
206
+ });
207
+ continue;
208
+ }
209
+ const tierPrices = formatTieredPriceBreakdown({
210
+ tiers: tiers.map((t) => ({
140
211
  upToAmount: t.upToAmount,
141
212
  unitPriceAmount: t.unitPrice?.amount,
142
213
  flatPriceAmount: t.flatPrice?.amount
143
214
  })),
144
215
  currency,
145
216
  unitLabel,
146
- includedLabel: "Included",
147
- omitIncludedUpToAmount: et.issueAfterReset
217
+ includedLabel: "Included"
148
218
  });
149
- const overageTier = rc.price.tiers.find((t) => t.unitPrice?.amount && parseFloat(t.unitPrice.amount) > 0);
150
- if (et.isSoftLimit !== false && overageTier?.unitPrice) overagePrice = `${formatPrice(parseFloat(overageTier.unitPrice.amount), currency)}/${unitLabel}`;
151
- }
152
- quotas.push({
219
+ quotas.push({
220
+ key: rc.featureKey ?? rc.key,
221
+ name: rc.name,
222
+ limit: 0,
223
+ period: periodFor(rc),
224
+ tierPrices,
225
+ isPayg: true
226
+ });
227
+ } else if (rc.price.type === "unit" && parseFloat(rc.price.amount) > 0) {
228
+ const amount = parseFloat(rc.price.amount);
229
+ quotas.push({
230
+ key: rc.featureKey ?? rc.key,
231
+ name: rc.name,
232
+ limit: 0,
233
+ period: periodFor(rc),
234
+ unitPrice: `${formatPrice(amount, currency)}/${unitLabel}`,
235
+ isPayg: true
236
+ });
237
+ } else features.push({
153
238
  key: rc.featureKey ?? rc.key,
154
- name: rc.name,
155
- limit: et.issueAfterReset,
156
- period: rc.billingCadence ? formatDuration(rc.billingCadence) : planBillingCadence ? formatDuration(planBillingCadence) : "month",
157
- overagePrice,
158
- tierPrices
239
+ name: rc.name
159
240
  });
160
241
  } else if (et.type === "boolean") features.push({
161
242
  key: rc.featureKey ?? rc.key,
@@ -217,30 +298,33 @@ const FeatureItem = ({ feature, className }) => {
217
298
  //#endregion
218
299
  //#region src/pricing-ui/QuotaItem.tsx
219
300
  const QuotaItem = ({ quota, className }) => {
301
+ const hasTierBreakdown = !!quota.tierPrices && quota.tierPrices.length > 0;
302
+ const showQuotaLine = !quota.isPayg && !hasTierBreakdown;
220
303
  return /* @__PURE__ */ jsxs("div", {
221
304
  className: cn("flex items-start gap-2", className),
222
305
  children: [/* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-primary shrink-0 mt-0.5" }), /* @__PURE__ */ jsxs("div", {
223
306
  className: "text-sm",
224
307
  children: [
225
- /* @__PURE__ */ jsxs("span", {
308
+ showQuotaLine ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
309
+ /* @__PURE__ */ jsxs("span", {
310
+ className: "font-medium",
311
+ children: [quota.name, ":"]
312
+ }),
313
+ " ",
314
+ quota.limit.toLocaleString(),
315
+ " / ",
316
+ quota.period
317
+ ] }) : /* @__PURE__ */ jsx("span", {
226
318
  className: "font-medium",
227
- children: [quota.name, ":"]
319
+ children: quota.name
228
320
  }),
229
- " ",
230
- quota.limit.toLocaleString(),
231
- " / ",
232
- quota.period,
233
- quota.tierPrices && quota.tierPrices.length > 0 && /* @__PURE__ */ jsx("ul", {
234
- className: "text-xs text-muted-foreground mt-1 space-y-0.5",
235
- children: quota.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
321
+ quota.unitPrice && /* @__PURE__ */ jsxs("span", {
322
+ className: "text-muted-foreground",
323
+ children: [" ", quota.unitPrice]
236
324
  }),
237
- quota.overagePrice && /* @__PURE__ */ jsxs("div", {
238
- className: "text-xs text-muted-foreground mt-0.5",
239
- children: [
240
- "+",
241
- quota.overagePrice,
242
- " after quota"
243
- ]
325
+ hasTierBreakdown && /* @__PURE__ */ jsx("ul", {
326
+ className: "text-xs text-muted-foreground mt-1 space-y-0.5",
327
+ children: quota.tierPrices?.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
244
328
  })
245
329
  ]
246
330
  })]
@@ -410,11 +494,40 @@ const subscriptionTaxLegendSentence = (behavior) => {
410
494
  }
411
495
  };
412
496
  //#endregion
497
+ //#region src/utils/formatPlanPrice.ts
498
+ const isPricedUsageRateCard = (rc) => {
499
+ if (rc.type !== "usage_based" || !rc.price) return false;
500
+ const p = rc.price;
501
+ if (p.type === "unit") return parseFloat(p.amount) > 0;
502
+ if (p.type === "tiered") return p.tiers.some((t) => parseFloat(t.flatPrice?.amount ?? "0") > 0 || parseFloat(t.unitPrice?.amount ?? "0") > 0);
503
+ return true;
504
+ };
505
+ const hasPricedUsageRateCard = (plan) => plan.phases.some((phase) => phase.rateCards.some(isPricedUsageRateCard));
506
+ /**
507
+ * Headline pricing for plan cards. Centralizes the "Pay as you go" detection:
508
+ * plans whose flat-fee total is zero but that bill on usage shouldn't render
509
+ * as "Free" - they're charged per-unit.
510
+ */
511
+ const formatPlanPrice = (plan) => {
512
+ if (plan.phases.length === 0) return { type: "free" };
513
+ const { monthly, yearly } = getPriceFromPlan(plan);
514
+ if (monthly > 0) return {
515
+ type: "priced",
516
+ monthly,
517
+ yearly
518
+ };
519
+ if (hasPricedUsageRateCard(plan)) return {
520
+ type: "payg",
521
+ main: "Pay as you go",
522
+ sub: "Usage-based pricing"
523
+ };
524
+ return { type: "free" };
525
+ };
526
+ //#endregion
413
527
  //#region src/pricing-ui/PricingCard.tsx
414
528
  const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, action, className }) => {
415
529
  if (plan.phases.length === 0) return null;
416
- const price = getPriceFromPlan(plan);
417
- const isFree = price.monthly === 0;
530
+ const priceLabel = formatPlanPrice(plan);
418
531
  const isCustom = plan.metadata?.isCustom === true;
419
532
  const billingInterval = formatDuration(plan.billingCadence);
420
533
  return /* @__PURE__ */ jsxs("div", {
@@ -442,16 +555,29 @@ const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, a
442
555
  }), /* @__PURE__ */ jsx("div", {
443
556
  className: "text-sm text-muted-foreground mt-1",
444
557
  children: "Contact Sales"
445
- })] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
558
+ })] }) : priceLabel.type === "payg" ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
559
+ className: "text-2xl font-bold text-card-foreground text-balance",
560
+ children: priceLabel.main
561
+ }), /* @__PURE__ */ jsx("div", {
562
+ className: "text-sm text-muted-foreground mt-1",
563
+ children: priceLabel.sub
564
+ })] }) : priceLabel.type === "free" ? /* @__PURE__ */ jsx("span", {
446
565
  className: "text-3xl font-bold text-card-foreground",
447
- children: isFree ? "Free" : formatPrice(price.monthly, plan.currency)
448
- }), !isFree && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
449
- className: "text-muted-foreground text-sm",
450
- children: ["/", billingInterval]
451
- }), showYearlyPrice && price.yearly > 0 && /* @__PURE__ */ jsxs("div", {
452
- className: "w-full text-sm text-muted-foreground mt-1",
453
- children: [formatPrice(price.yearly, plan.currency), "/year"]
454
- })] })] })
566
+ children: "Free"
567
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
568
+ /* @__PURE__ */ jsx("span", {
569
+ className: "text-3xl font-bold text-card-foreground",
570
+ children: formatPrice(priceLabel.monthly, plan.currency)
571
+ }),
572
+ /* @__PURE__ */ jsxs("span", {
573
+ className: "text-muted-foreground text-sm",
574
+ children: ["/", billingInterval]
575
+ }),
576
+ showYearlyPrice && priceLabel.yearly > 0 && /* @__PURE__ */ jsxs("div", {
577
+ className: "w-full text-sm text-muted-foreground mt-1",
578
+ children: [formatPrice(priceLabel.yearly, plan.currency), "/year"]
579
+ })
580
+ ] })
455
581
  }),
456
582
  plan.paymentRequired === false && /* @__PURE__ */ jsx("div", {
457
583
  className: "text-sm text-muted-foreground mt-1",
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, p as formatTieredPriceBreakdown, t as PricingTable, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-Dsbd3E2Q.mjs";
1
+ import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, p as formatTieredPriceBreakdown, t as PricingTable, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-DNop2iX9.mjs";
2
2
  import { cn, createPlugin, joinUrl, throwIfProblemJson } from "zudoku";
3
3
  import { AlertTriangleIcon, ArrowDownIcon, ArrowLeftRightIcon, ArrowUpIcon, CalendarIcon, CheckCheckIcon, CheckIcon, CircleSlashIcon, ClockIcon, CreditCardIcon, Grid2x2XIcon, InfoIcon, Loader2Icon, LockIcon, MoreVerticalIcon, RefreshCcw, RefreshCwIcon, Settings, ShieldIcon, StarsIcon, Trash2Icon, XIcon } from "zudoku/icons";
4
4
  import { Button, ClientOnly, Head, Heading, Link, Slot } from "zudoku/components";
@@ -1324,6 +1324,7 @@ const comparePlans = (currentPlan, targetPlan, currentIndex, targetIndex, units)
1324
1324
  return {
1325
1325
  plan: targetPlan,
1326
1326
  isUpgrade,
1327
+ isNewerVersion: false,
1327
1328
  quotaChanges,
1328
1329
  featureChanges
1329
1330
  };
@@ -1334,6 +1335,14 @@ const ChangeIndicator = ({ change }) => {
1334
1335
  return /* @__PURE__ */ jsx(CheckIcon, { className: "w-4 h-4 text-green-600 shrink-0" });
1335
1336
  };
1336
1337
  const isPrivatePlan = (plan) => plan.metadata?.zuplo_private_plan === "true";
1338
+ const planVersion = (plan) => plan.version ?? 1;
1339
+ const isNewerPlanVersion = (subscribedPlan, target) => target.key === subscribedPlan.key && planVersion(target) > planVersion(subscribedPlan);
1340
+ /** Baseline for comparisons: catalog entry when present, else subscription plan. */
1341
+ const resolvePlanForComparison = (subscribedPlan, catalogItems) => catalogItems?.find((p) => p.id === subscribedPlan.id) ?? subscribedPlan;
1342
+ const resolveIsUpgrade = ({ target, targetIndex, subscribedPlan, currentIndex }) => {
1343
+ if (target.key === subscribedPlan.key) return planVersion(target) > planVersion(subscribedPlan);
1344
+ return targetIndex > currentIndex;
1345
+ };
1337
1346
  const modeLabelMap = {
1338
1347
  upgrade: "Upgrade",
1339
1348
  downgrade: "Downgrade",
@@ -1354,10 +1363,17 @@ const PlanComparisonItem = ({ comparison, subscriptionId, mode, onRequestChange,
1354
1363
  children: [/* @__PURE__ */ jsxs("div", {
1355
1364
  className: "flex items-center justify-between mb-3",
1356
1365
  children: [/* @__PURE__ */ jsxs("div", {
1357
- className: "flex items-baseline gap-2",
1358
- children: [/* @__PURE__ */ jsx("h4", {
1359
- className: "font-semibold text-foreground",
1360
- children: comparison.plan.name
1366
+ className: "flex items-baseline gap-2 flex-wrap",
1367
+ children: [/* @__PURE__ */ jsxs("div", {
1368
+ className: "flex items-center gap-2",
1369
+ children: [/* @__PURE__ */ jsx("h4", {
1370
+ className: "font-semibold text-foreground",
1371
+ children: comparison.plan.name
1372
+ }), comparison.isNewerVersion && /* @__PURE__ */ jsx(Badge, {
1373
+ variant: "outline",
1374
+ className: "rounded-full border-primary/30 bg-primary/10 text-primary font-medium",
1375
+ children: "New version"
1376
+ })]
1361
1377
  }), isCustom ? /* @__PURE__ */ jsx("span", {
1362
1378
  className: "text-primary font-medium",
1363
1379
  children: "Custom"
@@ -1522,35 +1538,35 @@ const SwitchPlanModal = ({ subscription, children }) => {
1522
1538
  window.location.href = data.url;
1523
1539
  }
1524
1540
  });
1525
- const currentPlan = plansData?.items.find((p) => p.key === subscription.plan.key);
1541
+ const subscribedPlan = subscription.plan;
1526
1542
  const { upgrades, downgrades, privatePlans } = useMemo(() => {
1527
- if (!plansData?.items) return {
1543
+ const catalogItems = plansData?.items;
1544
+ if (!catalogItems?.length) return {
1528
1545
  upgrades: [],
1529
1546
  downgrades: [],
1530
1547
  privatePlans: []
1531
1548
  };
1532
- if (!currentPlan) {
1533
- const currentIndex = -1;
1534
- return {
1535
- upgrades: plansData.items.map((plan, targetIndex) => comparePlans(void 0, plan, currentIndex, targetIndex, pricing?.units)).filter((c) => !isPrivatePlan(c.plan)),
1536
- downgrades: [],
1537
- privatePlans: []
1538
- };
1539
- }
1540
- if (isPrivatePlan(currentPlan)) {
1541
- const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1542
- return {
1543
- upgrades: plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1544
- return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1545
- }).filter((c) => !isPrivatePlan(c.plan)),
1546
- downgrades: [],
1547
- privatePlans: []
1548
- };
1549
- }
1550
- const currentIndex = plansData.items.findIndex((p) => p.id === currentPlan.id);
1551
- const allComparisons = plansData.items.filter((p) => p.id !== currentPlan.id).map((plan) => {
1552
- return comparePlans(currentPlan, plan, currentIndex, plansData.items.indexOf(plan), pricing?.units);
1549
+ const planForComparison = resolvePlanForComparison(subscribedPlan, catalogItems);
1550
+ const currentIndex = catalogItems.some((p) => p.id === subscribedPlan.id) ? catalogItems.findIndex((p) => p.id === subscribedPlan.id) : -1;
1551
+ const subscribedIsPrivate = isPrivatePlan(subscribedPlan);
1552
+ const allComparisons = catalogItems.flatMap((plan, targetIndex) => {
1553
+ if (plan.id === subscribedPlan.id) return [];
1554
+ return [{
1555
+ ...comparePlans(planForComparison, plan, currentIndex, targetIndex, pricing?.units),
1556
+ isUpgrade: resolveIsUpgrade({
1557
+ target: plan,
1558
+ targetIndex,
1559
+ subscribedPlan,
1560
+ currentIndex
1561
+ }),
1562
+ isNewerVersion: isNewerPlanVersion(subscribedPlan, plan)
1563
+ }];
1553
1564
  });
1565
+ if (subscribedIsPrivate) return {
1566
+ upgrades: allComparisons.filter((c) => !isPrivatePlan(c.plan)),
1567
+ downgrades: [],
1568
+ privatePlans: allComparisons.filter((c) => isPrivatePlan(c.plan))
1569
+ };
1554
1570
  return {
1555
1571
  upgrades: allComparisons.filter((c) => c.isUpgrade && !isPrivatePlan(c.plan)),
1556
1572
  downgrades: allComparisons.filter((c) => !c.isUpgrade && !isPrivatePlan(c.plan)),
@@ -1558,7 +1574,7 @@ const SwitchPlanModal = ({ subscription, children }) => {
1558
1574
  };
1559
1575
  }, [
1560
1576
  plansData?.items,
1561
- currentPlan,
1577
+ subscribedPlan,
1562
1578
  pricing?.units
1563
1579
  ]);
1564
1580
  return /* @__PURE__ */ jsxs(Dialog, {
@@ -1589,18 +1605,11 @@ const SwitchPlanModal = ({ subscription, children }) => {
1589
1605
  children: switchPlanMutation.error.message
1590
1606
  })
1591
1607
  }),
1592
- currentPlan && /* @__PURE__ */ jsx(Item, {
1593
- variant: "outline",
1594
- children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
1595
- className: "text-lg font-bold",
1596
- children: currentPlan.name
1597
- })] })
1598
- }),
1599
- !currentPlan && /* @__PURE__ */ jsx(Item, {
1608
+ /* @__PURE__ */ jsx(Item, {
1600
1609
  variant: "outline",
1601
1610
  children: /* @__PURE__ */ jsxs(ItemContent, { children: [/* @__PURE__ */ jsx(ItemTitle, { children: "Current Plan" }), /* @__PURE__ */ jsx(ItemDescription, {
1602
1611
  className: "text-lg font-bold",
1603
- children: subscription.plan.name
1612
+ children: subscribedPlan.name
1604
1613
  })] })
1605
1614
  }),
1606
1615
  upgrades.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
@@ -1780,21 +1789,6 @@ const formatDate$1 = (dateString) => {
1780
1789
  };
1781
1790
  const formatDateRange = (from, to) => `${formatDate$1(from)} – ${formatDate$1(to)}`;
1782
1791
  const formatNumber = (value) => value.toLocaleString("en-US");
1783
- const getOveragePriceFromItem = (item, currency, units) => {
1784
- const tiers = item.price?.tiers;
1785
- if (!tiers || tiers.length === 0) return void 0;
1786
- const amount = tiers.find((t) => {
1787
- const amount = t.unitPrice?.amount;
1788
- if (!amount) return false;
1789
- const parsed = parseFloat(amount);
1790
- return Number.isFinite(parsed) && parsed > 0;
1791
- })?.unitPrice?.amount;
1792
- if (!amount) return void 0;
1793
- const parsed = parseFloat(amount);
1794
- if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
1795
- const unitLabel = units?.[item.key] ?? units?.[item.featureKey] ?? "unit";
1796
- return `${formatPrice(parsed, currency)}/${unitLabel}`;
1797
- };
1798
1792
  const getTierPricesFromItem = (item, currency, units) => {
1799
1793
  if (item.price?.type !== "tiered") return;
1800
1794
  const tiers = item.price.tiers;
@@ -1808,25 +1802,32 @@ const getTierPricesFromItem = (item, currency, units) => {
1808
1802
  })),
1809
1803
  currency,
1810
1804
  unitLabel,
1811
- includedLabel: "Included",
1812
- omitIncludedUpToAmount: item.included?.entitlement?.issueAfterReset
1805
+ includedLabel: "Included"
1813
1806
  });
1814
1807
  };
1808
+ const hasPricedFirstTier = (item) => {
1809
+ const firstTier = item.price?.tiers?.[0];
1810
+ if (!firstTier) return false;
1811
+ const flat = parseFloat(firstTier.flatPrice?.amount ?? "0");
1812
+ const unit = parseFloat(firstTier.unitPrice?.amount ?? "0");
1813
+ return flat > 0 || unit > 0;
1814
+ };
1815
1815
  const getEntitlementsFromItems = (items, currency, units, fallbackBillingCadence) => {
1816
1816
  const features = [];
1817
1817
  for (const item of items) {
1818
1818
  const entitlement = item.included?.entitlement;
1819
1819
  if (!entitlement) continue;
1820
1820
  if (entitlement.type === "metered" && entitlement.issueAfterReset != null) {
1821
- const cadence = item.billingCadence ?? fallbackBillingCadence;
1821
+ const cadence = entitlement.usagePeriod?.intervalISO ?? item.billingCadence ?? fallbackBillingCadence;
1822
+ const tierPrices = getTierPricesFromItem(item, currency, units);
1823
+ const suppressLimit = hasPricedFirstTier(item) && !!tierPrices && tierPrices.length > 0;
1822
1824
  features.push({
1823
1825
  entitlementType: "metered",
1824
1826
  key: item.featureKey ?? item.key,
1825
1827
  name: item.name ?? item.featureKey ?? item.key,
1826
- limit: entitlement.issueAfterReset,
1827
- period: cadence ? formatDuration(cadence) : "month",
1828
- overagePrice: entitlement.isSoftLimit !== false ? getOveragePriceFromItem(item, currency, units) : void 0,
1829
- tierPrices: getTierPricesFromItem(item, currency, units)
1828
+ limit: suppressLimit ? void 0 : entitlement.issueAfterReset,
1829
+ period: suppressLimit ? void 0 : cadence ? formatDuration(cadence) : "month",
1830
+ tierPrices
1830
1831
  });
1831
1832
  continue;
1832
1833
  }
@@ -1872,7 +1873,6 @@ const getPhaseRows = (opts) => {
1872
1873
  entitlementType: f.entitlementType,
1873
1874
  limit: f.entitlementType === "metered" ? f.limit : void 0,
1874
1875
  period: f.entitlementType === "metered" ? f.period : void 0,
1875
- overagePrice: f.entitlementType === "metered" ? f.overagePrice : void 0,
1876
1876
  tierPrices: f.entitlementType === "metered" ? f.tierPrices : void 0,
1877
1877
  value: f.entitlementType === "static" ? f.value : void 0,
1878
1878
  phaseId: phase.id,
@@ -1992,18 +1992,10 @@ const SubscriptionPlanDetails = ({ subscription }) => {
1992
1992
  children: [row.name, row.entitlementType === "static" && row.value !== void 0 ? `: ${row.value}` : ""]
1993
1993
  }), /* @__PURE__ */ jsx("div", {
1994
1994
  className: "text-muted-foreground",
1995
- children: row.entitlementType === "metered" && row.limit != null ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
1996
- formatNumber(row.limit),
1997
- row.period ? ` / ${row.period}` : "",
1998
- row.tierPrices && row.tierPrices.length > 0 ? /* @__PURE__ */ jsx("ul", {
1999
- className: "text-xs mt-1 space-y-0.5",
2000
- children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
2001
- }) : null,
2002
- row.overagePrice ? /* @__PURE__ */ jsxs("div", {
2003
- className: "text-xs mt-0.5",
2004
- children: ["Overage: ", row.overagePrice]
2005
- }) : null
2006
- ] }) : row.entitlementType === "static" && row.value !== void 0 ? null : "Included"
1995
+ children: row.entitlementType === "metered" && (row.limit != null || row.tierPrices && row.tierPrices.length > 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [row.limit != null && (!row.tierPrices || row.tierPrices.length === 0) ? /* @__PURE__ */ jsxs(Fragment$1, { children: [formatNumber(row.limit), row.period ? ` / ${row.period}` : ""] }) : null, row.tierPrices && row.tierPrices.length > 0 ? /* @__PURE__ */ jsx("ul", {
1996
+ className: "text-xs space-y-0.5",
1997
+ children: row.tierPrices.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line))
1998
+ }) : null] }) : row.entitlementType === "static" && row.value !== void 0 ? null : "Included"
2007
1999
  })]
2008
2000
  })
2009
2001
  }, `${row.key}:${row.phaseId}`))
@@ -84,8 +84,9 @@ interface Quota {
84
84
  name: string;
85
85
  limit: number;
86
86
  period: string;
87
- overagePrice?: string;
88
87
  tierPrices?: string[];
88
+ isPayg?: boolean;
89
+ unitPrice?: string;
89
90
  }
90
91
  interface Feature {
91
92
  key: string;
@@ -253,7 +254,8 @@ declare const formatDurationInterval: (iso: string) => string;
253
254
  * Returns an adjective form suitable for possessive context
254
255
  * e.g. "your monthly quota", "your weekly limit".
255
256
  * Falls back to "billing period" for multi-unit cadences
256
- * where "every 3 months" would be grammatically awkward.
257
+ * or sub-hour units where the adjective form is grammatically awkward
258
+ * (e.g. "every 3 months", "every 5 minutes").
257
259
  */
258
260
  declare const formatDurationAdjective: (iso: string) => string;
259
261
  //#endregion
@@ -276,7 +278,6 @@ declare const formatTieredPriceBreakdown: (opts: {
276
278
  currency?: string;
277
279
  unitLabel: string;
278
280
  includedLabel: string;
279
- omitIncludedUpToAmount?: number;
280
281
  }) => string[] | undefined;
281
282
  //#endregion
282
283
  //#region src/utils/getPriceFromPlan.d.ts
@@ -1,2 +1,2 @@
1
- import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, d as FeatureItem, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, n as PricingCard, o as taxBehaviorLegendSentence, p as formatTieredPriceBreakdown, r as collectDefaultTaxBehaviors, s as derivePriceFromPlan, t as PricingTable, u as QuotaItem, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-Dsbd3E2Q.mjs";
1
+ import { _ as formatDuration, a as subscriptionTaxLegendSentence, c as getPriceFromPlan, d as FeatureItem, f as categorizeRateCards, g as formatPrice, h as formatMinorCurrencyAmount, i as planHasDefaultTaxBehavior, l as PlanEntitlements, m as formatStaticEntitlementConfig, n as PricingCard, o as taxBehaviorLegendSentence, p as formatTieredPriceBreakdown, r as collectDefaultTaxBehaviors, s as derivePriceFromPlan, t as PricingTable, u as QuotaItem, v as formatDurationAdjective, y as formatDurationInterval } from "./PricingTable-DNop2iX9.mjs";
2
2
  export { FeatureItem, PlanEntitlements, PricingCard, PricingTable, QuotaItem, categorizeRateCards, collectDefaultTaxBehaviors, derivePriceFromPlan, formatDuration, formatDurationAdjective, formatDurationInterval, formatMinorCurrencyAmount, formatPrice, formatStaticEntitlementConfig, formatTieredPriceBreakdown, getPriceFromPlan, planHasDefaultTaxBehavior, subscriptionTaxLegendSentence, taxBehaviorLegendSentence };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuplo/zudoku-plugin-monetization",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -37,7 +37,7 @@
37
37
  "react": "19.2.5",
38
38
  "react-dom": "19.2.5",
39
39
  "tsdown": "0.22.0",
40
- "zudoku": "0.77.0"
40
+ "zudoku": "0.79.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": ">=19.2.0",