@zuplo/zudoku-plugin-monetization 0.0.41 → 0.0.42
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/{PricingTable-DNop2iX9.mjs → PricingTable-BlcXx4-5.mjs} +149 -130
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +903 -824
- package/dist/pricing-ui.d.mts +61 -32
- package/dist/pricing-ui.mjs +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment } from "react";
|
|
2
|
-
import { parse } from "tinyduration";
|
|
3
2
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { parse } from "tinyduration";
|
|
4
4
|
import { clsx } from "clsx";
|
|
5
5
|
import { twMerge } from "tailwind-merge";
|
|
6
6
|
//#region src/utils/formatDuration.ts
|
|
@@ -136,6 +136,17 @@ const formatTieredPriceBreakdown = (opts) => {
|
|
|
136
136
|
return lines.length > 0 ? lines : void 0;
|
|
137
137
|
};
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/utils/tierHasPositivePrice.ts
|
|
140
|
+
/**
|
|
141
|
+
* Whether a price tier charges anything — a non-zero flat or per-unit amount.
|
|
142
|
+
* Amounts are decimal strings and a missing part counts as zero, so this
|
|
143
|
+
* distinguishes a genuinely priced tier from an all-zero ("Included") tier.
|
|
144
|
+
* Shared by the pricing-label path ({@link formatPlanPrice}) and rate-card
|
|
145
|
+
* categorization ({@link categorizeRateCards}) so both decide "is this tier
|
|
146
|
+
* free?" the same way.
|
|
147
|
+
*/
|
|
148
|
+
const tierHasPositivePrice = (tier) => parseFloat(tier.flatPrice?.amount ?? "0") > 0 || parseFloat(tier.unitPrice?.amount ?? "0") > 0;
|
|
149
|
+
//#endregion
|
|
139
150
|
//#region src/utils/categorizeRateCards.ts
|
|
140
151
|
const categorizeRateCards = (rateCards, options) => {
|
|
141
152
|
const { currency, units, planBillingCadence } = options ?? {};
|
|
@@ -152,7 +163,7 @@ const categorizeRateCards = (rateCards, options) => {
|
|
|
152
163
|
return "month";
|
|
153
164
|
};
|
|
154
165
|
const firstTier = rc.price?.type === "tiered" && rc.price.tiers.length > 0 ? rc.price.tiers[0] : void 0;
|
|
155
|
-
const firstTierIsPriced = !!firstTier && (
|
|
166
|
+
const firstTierIsPriced = !!firstTier && tierHasPositivePrice(firstTier);
|
|
156
167
|
if (et.type === "metered" && et.issueAfterReset != null && !firstTierIsPriced) {
|
|
157
168
|
let tierPrices;
|
|
158
169
|
if (rc.price?.type === "tiered" && rc.price.tiers) tierPrices = formatTieredPriceBreakdown({
|
|
@@ -176,7 +187,7 @@ const categorizeRateCards = (rateCards, options) => {
|
|
|
176
187
|
const unitLabel = unitLabelFor(rc);
|
|
177
188
|
if (rc.price.type === "tiered" && rc.price.tiers.length > 0) {
|
|
178
189
|
const tiers = rc.price.tiers;
|
|
179
|
-
if (!tiers.some(
|
|
190
|
+
if (!tiers.some(tierHasPositivePrice)) {
|
|
180
191
|
features.push({
|
|
181
192
|
key: rc.featureKey ?? rc.key,
|
|
182
193
|
name: rc.name
|
|
@@ -331,28 +342,23 @@ const QuotaItem = ({ quota, className }) => {
|
|
|
331
342
|
});
|
|
332
343
|
};
|
|
333
344
|
//#endregion
|
|
334
|
-
//#region src/pricing-ui/
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
//#region src/pricing-ui/EntitlementList.tsx
|
|
346
|
+
/**
|
|
347
|
+
* Vertical list of a resolved entitlement set — quotas first, then features —
|
|
348
|
+
* with an optional leading `header` (e.g. a phase name) rendered inside the same
|
|
349
|
+
* spacing container. Returns `null` when there are no quotas or features, so
|
|
350
|
+
* callers can gate a section header / border without leaving an empty container
|
|
351
|
+
* behind.
|
|
352
|
+
*
|
|
353
|
+
* Shared by {@link PlanEntitlements} (one list per phase) and the subscription
|
|
354
|
+
* entitlement views so quotas and features always render identically.
|
|
355
|
+
*/
|
|
356
|
+
const EntitlementList = ({ quotas, features, header, itemClassName }) => {
|
|
341
357
|
if (quotas.length === 0 && features.length === 0) return null;
|
|
342
358
|
return /* @__PURE__ */ jsxs("div", {
|
|
343
359
|
className: "space-y-2",
|
|
344
360
|
children: [
|
|
345
|
-
|
|
346
|
-
className: "text-sm font-medium text-card-foreground",
|
|
347
|
-
children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
|
|
348
|
-
className: "text-muted-foreground font-normal",
|
|
349
|
-
children: [
|
|
350
|
-
" ",
|
|
351
|
-
"— ",
|
|
352
|
-
formatDuration(phase.duration)
|
|
353
|
-
]
|
|
354
|
-
})]
|
|
355
|
-
}),
|
|
361
|
+
header,
|
|
356
362
|
quotas.map((quota) => /* @__PURE__ */ jsx(QuotaItem, {
|
|
357
363
|
quota,
|
|
358
364
|
className: itemClassName
|
|
@@ -364,6 +370,31 @@ const PhaseSection = ({ phase, currency, showName, billingCadence, units, itemCl
|
|
|
364
370
|
]
|
|
365
371
|
});
|
|
366
372
|
};
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/pricing-ui/PlanEntitlements.tsx
|
|
375
|
+
const PhaseSection = ({ phase, currency, showName, billingCadence, units, itemClassName }) => {
|
|
376
|
+
const { quotas, features } = categorizeRateCards(phase.rateCards, {
|
|
377
|
+
currency,
|
|
378
|
+
units,
|
|
379
|
+
planBillingCadence: billingCadence
|
|
380
|
+
});
|
|
381
|
+
return /* @__PURE__ */ jsx(EntitlementList, {
|
|
382
|
+
quotas,
|
|
383
|
+
features,
|
|
384
|
+
itemClassName,
|
|
385
|
+
header: showName ? /* @__PURE__ */ jsxs("div", {
|
|
386
|
+
className: "text-sm font-medium text-card-foreground",
|
|
387
|
+
children: [phase.name, phase.duration && /* @__PURE__ */ jsxs("span", {
|
|
388
|
+
className: "text-muted-foreground font-normal",
|
|
389
|
+
children: [
|
|
390
|
+
" ",
|
|
391
|
+
"— ",
|
|
392
|
+
formatDuration(phase.duration)
|
|
393
|
+
]
|
|
394
|
+
})]
|
|
395
|
+
}) : void 0
|
|
396
|
+
});
|
|
397
|
+
};
|
|
367
398
|
const PlanEntitlements = ({ phases, currency, billingCadence, units, itemClassName }) => {
|
|
368
399
|
return /* @__PURE__ */ jsx("div", {
|
|
369
400
|
className: "space-y-4",
|
|
@@ -378,87 +409,100 @@ const PlanEntitlements = ({ phases, currency, billingCadence, units, itemClassNa
|
|
|
378
409
|
});
|
|
379
410
|
};
|
|
380
411
|
//#endregion
|
|
381
|
-
//#region src/
|
|
412
|
+
//#region src/pricing-ui/PlanPriceTag.tsx
|
|
413
|
+
/**
|
|
414
|
+
* Headline price for a plan/subscription: `$X/cadence`, "Pay as you go", or
|
|
415
|
+
* "Free", from a {@link PlanPriceLabel}. Shared by the subscription details
|
|
416
|
+
* page, the Switch Plan baseline, each plan-change card, and the checkout /
|
|
417
|
+
* plan-change summary cards so they all render the price identically.
|
|
418
|
+
*
|
|
419
|
+
* `size` selects the typographic treatment:
|
|
420
|
+
* - `"inline"` (default): compact, primary-colored text for use beside a name.
|
|
421
|
+
* - `"lg"`: a large foreground headline for a summary card's price column.
|
|
422
|
+
*
|
|
423
|
+
* Pass `description` to surface the "Usage-based pricing" subline under the
|
|
424
|
+
* "Pay as you go" headline (used where there's room for it).
|
|
425
|
+
*/
|
|
426
|
+
const PlanPriceTag = ({ label, currency, billingCadence, description = false, size = "inline" }) => {
|
|
427
|
+
const isLg = size === "lg";
|
|
428
|
+
if (label.type === "priced") return /* @__PURE__ */ jsxs("span", {
|
|
429
|
+
className: isLg ? "text-2xl font-bold" : "text-primary font-medium text-lg",
|
|
430
|
+
children: [formatPrice(label.amount, currency), billingCadence && /* @__PURE__ */ jsxs("span", {
|
|
431
|
+
className: "text-muted-foreground font-normal",
|
|
432
|
+
children: ["/", formatDuration(billingCadence)]
|
|
433
|
+
})]
|
|
434
|
+
});
|
|
435
|
+
if (label.type === "payg") return /* @__PURE__ */ jsxs("span", {
|
|
436
|
+
className: isLg ? "text-2xl font-bold text-balance" : "text-primary font-medium",
|
|
437
|
+
children: [label.main, description && /* @__PURE__ */ jsx("span", {
|
|
438
|
+
className: isLg ? "block text-sm text-muted-foreground font-normal mt-1" : "block text-xs text-muted-foreground font-normal",
|
|
439
|
+
children: label.sub
|
|
440
|
+
})]
|
|
441
|
+
});
|
|
442
|
+
return /* @__PURE__ */ jsx("span", {
|
|
443
|
+
className: isLg ? "text-2xl text-muted-foreground font-bold" : "text-primary font-medium",
|
|
444
|
+
children: "Free"
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/utils/getPlanPrice.ts
|
|
382
449
|
const sumFlatFeeAmounts = (rateCards) => {
|
|
383
450
|
let total = 0;
|
|
384
|
-
for (const rc of rateCards) if (rc.type === "flat_fee" && rc.price) {
|
|
451
|
+
for (const rc of rateCards) if (rc.type === "flat_fee" && rc.price && rc.billingCadence !== null) {
|
|
385
452
|
const amount = Number(rc.price.amount);
|
|
386
453
|
if (Number.isFinite(amount)) total += amount;
|
|
387
454
|
}
|
|
388
455
|
return total;
|
|
389
456
|
};
|
|
390
457
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* `undefined` because there is no sensible monthly equivalent.
|
|
396
|
-
*/
|
|
397
|
-
const cadenceToMonths = (iso) => {
|
|
398
|
-
try {
|
|
399
|
-
const d = parse(iso);
|
|
400
|
-
let months = 0;
|
|
401
|
-
if (d.years) months += d.years * 12;
|
|
402
|
-
if (d.months) months += d.months;
|
|
403
|
-
if (d.weeks) months += d.weeks * (12 / 52);
|
|
404
|
-
if (d.days) months += d.days * (1 / 30);
|
|
405
|
-
return months > 0 ? months : void 0;
|
|
406
|
-
} catch {
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
/**
|
|
411
|
-
* Derive a (monthly, yearly) headline price from a plan's last phase by
|
|
412
|
-
* summing all `flat_fee` rate-card amounts and converting from the plan's
|
|
413
|
-
* `billingCadence` to a monthly equivalent.
|
|
458
|
+
* The plan's headline recurring price: the sum of every recurring `flat_fee`
|
|
459
|
+
* rate-card amount on the plan's steady-state (last) phase, expressed in the
|
|
460
|
+
* plan's own `billingCadence`. One-time fees (`flat_fee` with
|
|
461
|
+
* `billingCadence: null`, e.g. a setup fee) are excluded.
|
|
414
462
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
463
|
+
* This is derived entirely from the plan's rate cards. It deliberately does
|
|
464
|
+
* NOT read any server-provided `monthlyPrice` / `yearlyPrice` and performs no
|
|
465
|
+
* cadence conversion, so it stays correct for any billing cadence — hourly
|
|
466
|
+
* (`PT1H`), weekly, monthly, yearly, etc. Callers pair the returned amount
|
|
467
|
+
* with `formatDuration(plan.billingCadence)` to render e.g. `$2.99/hour`.
|
|
418
468
|
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
469
|
+
* Returns `0` when there are no phases or no recurring flat fee, which callers
|
|
470
|
+
* render as "Free" (or "Pay as you go" when the plan bills on usage — see
|
|
471
|
+
* {@link formatPlanPrice}).
|
|
422
472
|
*/
|
|
423
|
-
const
|
|
473
|
+
const getPlanPrice = (plan) => {
|
|
424
474
|
const lastPhase = plan.phases?.at(-1);
|
|
425
|
-
if (!lastPhase) return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
monthly: null,
|
|
437
|
-
yearly: null
|
|
438
|
-
};
|
|
439
|
-
const monthly = flatPrice / months;
|
|
440
|
-
return {
|
|
441
|
-
monthly,
|
|
442
|
-
yearly: monthly * 12
|
|
443
|
-
};
|
|
475
|
+
if (!lastPhase) return 0;
|
|
476
|
+
return sumFlatFeeAmounts(lastPhase.rateCards ?? []);
|
|
477
|
+
};
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/utils/formatPlanPrice.ts
|
|
480
|
+
const isPricedUsageRateCard = (rc) => {
|
|
481
|
+
if (rc.type !== "usage_based" || !rc.price) return false;
|
|
482
|
+
const p = rc.price;
|
|
483
|
+
if (p.type === "unit") return parseFloat(p.amount) > 0;
|
|
484
|
+
if (p.type === "tiered") return p.tiers.some(tierHasPositivePrice);
|
|
485
|
+
return true;
|
|
444
486
|
};
|
|
487
|
+
const hasPricedUsageRateCard = (plan) => plan.phases.some((phase) => phase.rateCards.some(isPricedUsageRateCard));
|
|
445
488
|
/**
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* be resolved are reported as `0`, which the pricing card renders as
|
|
450
|
-
* "Free".
|
|
489
|
+
* Headline pricing for plan cards. Centralizes the "Pay as you go" detection:
|
|
490
|
+
* plans whose flat-fee total is zero but that bill on usage shouldn't render
|
|
491
|
+
* as "Free" - they're charged per-unit.
|
|
451
492
|
*/
|
|
452
|
-
const
|
|
453
|
-
if (plan.
|
|
454
|
-
|
|
455
|
-
|
|
493
|
+
const formatPlanPrice = (plan) => {
|
|
494
|
+
if (!plan.phases || plan.phases.length === 0) return { type: "free" };
|
|
495
|
+
const amount = getPlanPrice(plan);
|
|
496
|
+
if (amount > 0) return {
|
|
497
|
+
type: "priced",
|
|
498
|
+
amount
|
|
456
499
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
500
|
+
if (hasPricedUsageRateCard(plan)) return {
|
|
501
|
+
type: "payg",
|
|
502
|
+
main: "Pay as you go",
|
|
503
|
+
sub: "Usage-based pricing"
|
|
461
504
|
};
|
|
505
|
+
return { type: "free" };
|
|
462
506
|
};
|
|
463
507
|
//#endregion
|
|
464
508
|
//#region src/utils/pricingTaxLegend.ts
|
|
@@ -494,41 +538,24 @@ const subscriptionTaxLegendSentence = (behavior) => {
|
|
|
494
538
|
}
|
|
495
539
|
};
|
|
496
540
|
//#endregion
|
|
497
|
-
//#region src/utils/
|
|
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));
|
|
541
|
+
//#region src/utils/isCustomPlan.ts
|
|
506
542
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
543
|
+
* A plan is "custom" (contact-sales, no self-serve price) when its metadata
|
|
544
|
+
* flags it. Mirrors the convention used by the pricing card
|
|
545
|
+
* (`PricingCard.tsx`), accepting boolean `true` or the string `"true"` — plan
|
|
546
|
+
* metadata values arrive as strings from the API but may be set as booleans in
|
|
547
|
+
* code/fixtures.
|
|
510
548
|
*/
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
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" };
|
|
549
|
+
const isCustomPlan = (plan) => {
|
|
550
|
+
const flag = plan.metadata?.isCustom;
|
|
551
|
+
return flag === true || flag === "true";
|
|
525
552
|
};
|
|
526
553
|
//#endregion
|
|
527
554
|
//#region src/pricing-ui/PricingCard.tsx
|
|
528
|
-
const PricingCard = ({ plan, isPopular = false,
|
|
555
|
+
const PricingCard = ({ plan, isPopular = false, units, action, className }) => {
|
|
529
556
|
if (plan.phases.length === 0) return null;
|
|
530
557
|
const priceLabel = formatPlanPrice(plan);
|
|
531
|
-
const isCustom = plan
|
|
558
|
+
const isCustom = isCustomPlan(plan);
|
|
532
559
|
const billingInterval = formatDuration(plan.billingCadence);
|
|
533
560
|
return /* @__PURE__ */ jsxs("div", {
|
|
534
561
|
className: cn("relative rounded-lg border p-6 flex flex-col", isPopular && "border-primary border-2", className),
|
|
@@ -564,20 +591,13 @@ const PricingCard = ({ plan, isPopular = false, showYearlyPrice = true, units, a
|
|
|
564
591
|
})] }) : priceLabel.type === "free" ? /* @__PURE__ */ jsx("span", {
|
|
565
592
|
className: "text-3xl font-bold text-card-foreground",
|
|
566
593
|
children: "Free"
|
|
567
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
] })
|
|
594
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
595
|
+
className: "text-3xl font-bold text-card-foreground",
|
|
596
|
+
children: formatPrice(priceLabel.amount, plan.currency)
|
|
597
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
598
|
+
className: "text-muted-foreground text-sm",
|
|
599
|
+
children: ["/", billingInterval]
|
|
600
|
+
})] })
|
|
581
601
|
}),
|
|
582
602
|
plan.paymentRequired === false && /* @__PURE__ */ jsx("div", {
|
|
583
603
|
className: "text-sm text-muted-foreground mt-1",
|
|
@@ -607,7 +627,7 @@ const DefaultEmptyState = () => /* @__PURE__ */ jsxs("div", {
|
|
|
607
627
|
children: "Make sure your plans are set up and published."
|
|
608
628
|
})]
|
|
609
629
|
});
|
|
610
|
-
const PricingTable = ({ plans,
|
|
630
|
+
const PricingTable = ({ plans, units, renderAction, renderCard, isPopular = (plan) => plan.metadata?.zuplo_most_popular === "true", emptyState, showTaxLegend = true, className, cardClassName }) => {
|
|
611
631
|
if (plans.length === 0) return /* @__PURE__ */ jsx(Fragment$1, { children: emptyState ?? /* @__PURE__ */ jsx(DefaultEmptyState, {}) });
|
|
612
632
|
const firstPlan = plans[0];
|
|
613
633
|
const taxLegendSentence = showTaxLegend && firstPlan ? taxBehaviorLegendSentence(collectDefaultTaxBehaviors(firstPlan)) : void 0;
|
|
@@ -618,7 +638,6 @@ const PricingTable = ({ plans, showYearlyPrice = true, units, renderAction, rend
|
|
|
618
638
|
const defaultCard = /* @__PURE__ */ jsx(PricingCard, {
|
|
619
639
|
plan,
|
|
620
640
|
isPopular: popular,
|
|
621
|
-
showYearlyPrice,
|
|
622
641
|
units,
|
|
623
642
|
action: renderAction?.(plan, popular),
|
|
624
643
|
className: cardClassName
|
|
@@ -641,4 +660,4 @@ const PricingTable = ({ plans, showYearlyPrice = true, units, renderAction, rend
|
|
|
641
660
|
})] });
|
|
642
661
|
};
|
|
643
662
|
//#endregion
|
|
644
|
-
export {
|
|
663
|
+
export { formatDurationInterval as S, formatStaticEntitlementConfig as _, planHasDefaultTaxBehavior as a, formatDuration as b, formatPlanPrice as c, PlanEntitlements as d, EntitlementList as f, formatTieredPriceBreakdown as g, categorizeRateCards as h, collectDefaultTaxBehaviors as i, getPlanPrice as l, FeatureItem as m, PricingCard as n, subscriptionTaxLegendSentence as o, QuotaItem as p, isCustomPlan as r, taxBehaviorLegendSentence as s, PricingTable as t, PlanPriceTag as u, formatMinorCurrencyAmount as v, formatDurationAdjective as x, formatPrice as y };
|