@nozle-js/react 0.11.0 → 0.12.0
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/README.md +47 -0
- package/dist/index.cjs +516 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -32
- package/dist/index.d.ts +183 -32
- package/dist/index.js +513 -94
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -165,7 +165,11 @@ function catalogQuery(client) {
|
|
|
165
165
|
const response = await client.catalogFetch("/api/v1/plans", { signal });
|
|
166
166
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
167
167
|
const data = await response.json();
|
|
168
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
plans: data.plans ?? [],
|
|
170
|
+
families: Array.isArray(data.families) ? data.families : null,
|
|
171
|
+
nozleBranding: data.meta?.nozle_branding === true
|
|
172
|
+
};
|
|
169
173
|
},
|
|
170
174
|
staleTime: 5 * 6e4,
|
|
171
175
|
// The pricing page shows a load error at once; the app can call refetch().
|
|
@@ -177,6 +181,7 @@ function usePlans() {
|
|
|
177
181
|
const query = useQuery(catalogQuery(client), queryClient);
|
|
178
182
|
return {
|
|
179
183
|
plans: query.data?.plans ?? [],
|
|
184
|
+
families: query.data?.families ?? null,
|
|
180
185
|
isLoading: query.isPending,
|
|
181
186
|
error: query.error ?? null,
|
|
182
187
|
refetch: query.refetch
|
|
@@ -1718,7 +1723,7 @@ function UpgradeModal(props) {
|
|
|
1718
1723
|
}
|
|
1719
1724
|
|
|
1720
1725
|
// src/components/billing/amounts.ts
|
|
1721
|
-
function formatMinorAmount(value, currency, locale = "en-US") {
|
|
1726
|
+
function formatMinorAmount(value, currency, locale = "en-US", options = {}) {
|
|
1722
1727
|
const raw = String(value);
|
|
1723
1728
|
if (!/^-?\d+$/.test(raw) || typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
1724
1729
|
return "Amount unavailable";
|
|
@@ -1734,12 +1739,16 @@ function formatMinorAmount(value, currency, locale = "en-US") {
|
|
|
1734
1739
|
const scale = 10n ** BigInt(digits);
|
|
1735
1740
|
const whole = absolute / scale;
|
|
1736
1741
|
const signed = minor < 0n ? whole === 0n ? -0 : -whole : whole;
|
|
1737
|
-
const
|
|
1742
|
+
const remainder = absolute % scale;
|
|
1743
|
+
const trim = options.trimZeroFraction === true && remainder === 0n;
|
|
1744
|
+
const decimal = String(remainder).padStart(digits, "0");
|
|
1738
1745
|
const digitFormatter = new Intl.NumberFormat(locale, {
|
|
1739
1746
|
useGrouping: false
|
|
1740
1747
|
});
|
|
1741
1748
|
const fraction = [...decimal].map((digit) => digitFormatter.format(Number(digit))).join("");
|
|
1742
|
-
return formatter.formatToParts(signed).
|
|
1749
|
+
return formatter.formatToParts(signed).filter(
|
|
1750
|
+
(part) => !trim || part.type !== "fraction" && part.type !== "decimal"
|
|
1751
|
+
).map((part) => part.type === "fraction" ? fraction : part.value).join("");
|
|
1743
1752
|
} catch {
|
|
1744
1753
|
return "Amount unavailable";
|
|
1745
1754
|
}
|
|
@@ -3455,6 +3464,360 @@ function useCheckout2() {
|
|
|
3455
3464
|
);
|
|
3456
3465
|
return { fetchClientSecret, checkout, isLoading, error };
|
|
3457
3466
|
}
|
|
3467
|
+
|
|
3468
|
+
// src/components/pricing/catalog.ts
|
|
3469
|
+
var PRICING_INTERVALS = [
|
|
3470
|
+
"weekly",
|
|
3471
|
+
"monthly",
|
|
3472
|
+
"quarterly",
|
|
3473
|
+
"semiannual",
|
|
3474
|
+
"yearly"
|
|
3475
|
+
];
|
|
3476
|
+
var INTERVAL_LABELS2 = {
|
|
3477
|
+
weekly: "Weekly",
|
|
3478
|
+
monthly: "Monthly",
|
|
3479
|
+
quarterly: "Quarterly",
|
|
3480
|
+
semiannual: "Every 6 months",
|
|
3481
|
+
yearly: "Annual"
|
|
3482
|
+
};
|
|
3483
|
+
var BILLED_WORDS = {
|
|
3484
|
+
weekly: "weekly",
|
|
3485
|
+
monthly: "monthly",
|
|
3486
|
+
quarterly: "quarterly",
|
|
3487
|
+
semiannual: "every 6 months",
|
|
3488
|
+
yearly: "yearly"
|
|
3489
|
+
};
|
|
3490
|
+
var PERIODS_PER_YEAR = {
|
|
3491
|
+
weekly: 52n,
|
|
3492
|
+
monthly: 12n,
|
|
3493
|
+
quarterly: 4n,
|
|
3494
|
+
semiannual: 2n,
|
|
3495
|
+
yearly: 1n
|
|
3496
|
+
};
|
|
3497
|
+
function isPricingInterval(value) {
|
|
3498
|
+
return typeof value === "string" && PRICING_INTERVALS.includes(value);
|
|
3499
|
+
}
|
|
3500
|
+
function intervalRank(interval) {
|
|
3501
|
+
return PRICING_INTERVALS.indexOf(interval);
|
|
3502
|
+
}
|
|
3503
|
+
function normalizePricingInterval(value) {
|
|
3504
|
+
if (value === "annual") return "yearly";
|
|
3505
|
+
return isPricingInterval(value) ? value : null;
|
|
3506
|
+
}
|
|
3507
|
+
function hasPosition(value) {
|
|
3508
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3509
|
+
}
|
|
3510
|
+
function comparePositions(a, b) {
|
|
3511
|
+
const aSet = hasPosition(a);
|
|
3512
|
+
const bSet = hasPosition(b);
|
|
3513
|
+
if (aSet && bSet) return a - b;
|
|
3514
|
+
if (aSet) return -1;
|
|
3515
|
+
if (bSet) return 1;
|
|
3516
|
+
return 0;
|
|
3517
|
+
}
|
|
3518
|
+
var DEFAULT_LOCALE = "en-US";
|
|
3519
|
+
function resolvePricingLocale(locale) {
|
|
3520
|
+
if (typeof locale !== "string" || locale.trim() === "") return DEFAULT_LOCALE;
|
|
3521
|
+
try {
|
|
3522
|
+
Intl.getCanonicalLocales(locale);
|
|
3523
|
+
return locale;
|
|
3524
|
+
} catch {
|
|
3525
|
+
return DEFAULT_LOCALE;
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function isLive(plan) {
|
|
3529
|
+
return plan.pending_deletion !== true;
|
|
3530
|
+
}
|
|
3531
|
+
function hasFamilies(catalog) {
|
|
3532
|
+
return Array.isArray(catalog.families);
|
|
3533
|
+
}
|
|
3534
|
+
function getPricingGroups(catalog) {
|
|
3535
|
+
const plans = catalog.plans ?? [];
|
|
3536
|
+
if (!hasFamilies(catalog)) {
|
|
3537
|
+
return plans.filter(isLive).map((plan) => ({ key: plan.code, family: null, plans: [plan] }));
|
|
3538
|
+
}
|
|
3539
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
3540
|
+
for (const plan of plans) {
|
|
3541
|
+
if (isLive(plan) && !byCode.has(plan.code)) byCode.set(plan.code, plan);
|
|
3542
|
+
}
|
|
3543
|
+
return catalog.families.map((family, index) => ({ family, index })).filter(({ family }) => family.show_on_pricing !== false).sort(
|
|
3544
|
+
(a, b) => comparePositions(a.family.pricing_position, b.family.pricing_position) || a.index - b.index
|
|
3545
|
+
).map(({ family }) => {
|
|
3546
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3547
|
+
const familyPlans = [];
|
|
3548
|
+
for (const code of family.plan_codes ?? []) {
|
|
3549
|
+
const plan = byCode.get(code);
|
|
3550
|
+
if (plan && !seen.has(code)) {
|
|
3551
|
+
seen.add(code);
|
|
3552
|
+
familyPlans.push(plan);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return { key: family.code, family, plans: familyPlans };
|
|
3556
|
+
}).filter((group) => group.plans.length > 0);
|
|
3557
|
+
}
|
|
3558
|
+
function toMinor(value) {
|
|
3559
|
+
const raw = String(value);
|
|
3560
|
+
if (!/^-?\d+$/.test(raw)) return null;
|
|
3561
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) return null;
|
|
3562
|
+
return BigInt(raw);
|
|
3563
|
+
}
|
|
3564
|
+
function divideRounded(numerator, denominator) {
|
|
3565
|
+
if (numerator < 0n) return -divideRounded(-numerator, denominator);
|
|
3566
|
+
return (2n * numerator + denominator) / (2n * denominator);
|
|
3567
|
+
}
|
|
3568
|
+
function perMonthMinor(amount, interval) {
|
|
3569
|
+
return divideRounded(amount * PERIODS_PER_YEAR[interval], 12n);
|
|
3570
|
+
}
|
|
3571
|
+
function fmt(amount, currency, locale) {
|
|
3572
|
+
return formatMinorAmount(String(amount), currency, locale, {
|
|
3573
|
+
trimZeroFraction: true
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
function savingsAgainstMonthly(plan, monthly) {
|
|
3577
|
+
if (!monthly || monthly === plan) return null;
|
|
3578
|
+
if (!isPricingInterval(plan.interval) || plan.interval === "monthly") return null;
|
|
3579
|
+
if (monthly.amount_currency !== plan.amount_currency) return null;
|
|
3580
|
+
const monthlyAmount = toMinor(monthly.amount_cents);
|
|
3581
|
+
const amount = toMinor(plan.amount_cents);
|
|
3582
|
+
if (monthlyAmount === null || amount === null) return null;
|
|
3583
|
+
const base = monthlyAmount * 12n;
|
|
3584
|
+
const cost = amount * PERIODS_PER_YEAR[plan.interval];
|
|
3585
|
+
if (base <= 0n || cost >= base) return null;
|
|
3586
|
+
const percent = Number(divideRounded(100n * (base - cost), base));
|
|
3587
|
+
return percent > 0 ? percent : null;
|
|
3588
|
+
}
|
|
3589
|
+
function monthlyPlanOf(group) {
|
|
3590
|
+
return group.family ? group.plans.find((plan) => plan.interval === "monthly") : void 0;
|
|
3591
|
+
}
|
|
3592
|
+
function getAvailableIntervals(catalog) {
|
|
3593
|
+
const present = /* @__PURE__ */ new Set();
|
|
3594
|
+
for (const group of getPricingGroups(catalog)) {
|
|
3595
|
+
for (const plan of group.plans) present.add(plan.interval);
|
|
3596
|
+
}
|
|
3597
|
+
return PRICING_INTERVALS.filter((interval) => present.has(interval));
|
|
3598
|
+
}
|
|
3599
|
+
function getPricingIntervalOptions(catalog) {
|
|
3600
|
+
const groups = getPricingGroups(catalog);
|
|
3601
|
+
return getAvailableIntervals(catalog).map((interval) => {
|
|
3602
|
+
let best = null;
|
|
3603
|
+
if (interval !== "monthly") {
|
|
3604
|
+
for (const group of groups) {
|
|
3605
|
+
const plan = group.plans.find((p) => p.interval === interval);
|
|
3606
|
+
const saving = plan ? savingsAgainstMonthly(plan, monthlyPlanOf(group)) : null;
|
|
3607
|
+
if (saving !== null && (best === null || saving > best)) best = saving;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
return { interval, label: INTERVAL_LABELS2[interval], savingsPercent: best };
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
function getDefaultInterval(catalog, preferred) {
|
|
3614
|
+
const available = getAvailableIntervals(catalog);
|
|
3615
|
+
const wanted = normalizePricingInterval(preferred);
|
|
3616
|
+
if (wanted && available.includes(wanted)) return wanted;
|
|
3617
|
+
if (available.includes("monthly")) return "monthly";
|
|
3618
|
+
return available[0] ?? null;
|
|
3619
|
+
}
|
|
3620
|
+
function pickPlan(plans, interval) {
|
|
3621
|
+
if (interval === null) return plans[0];
|
|
3622
|
+
const exact = plans.find((plan) => plan.interval === interval);
|
|
3623
|
+
if (exact) return exact;
|
|
3624
|
+
const target = intervalRank(interval);
|
|
3625
|
+
let best = plans[0];
|
|
3626
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
3627
|
+
for (const plan of plans) {
|
|
3628
|
+
const rank = intervalRank(plan.interval);
|
|
3629
|
+
if (rank < 0) continue;
|
|
3630
|
+
const score = Math.abs(rank - target) * 2 + (rank > target ? 1 : 0);
|
|
3631
|
+
if (score < bestScore) {
|
|
3632
|
+
best = plan;
|
|
3633
|
+
bestScore = score;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return best;
|
|
3637
|
+
}
|
|
3638
|
+
function isTrueBoolean(privilege) {
|
|
3639
|
+
const { value } = privilege;
|
|
3640
|
+
if (value === true) return true;
|
|
3641
|
+
return privilege.value_type === "boolean" && typeof value === "string" && value.toLowerCase() === "true";
|
|
3642
|
+
}
|
|
3643
|
+
function formatPrivilegeValue(privilege, numberFormat) {
|
|
3644
|
+
const { value } = privilege;
|
|
3645
|
+
if (value === null || value === void 0) return null;
|
|
3646
|
+
if (isTrueBoolean(privilege)) return null;
|
|
3647
|
+
if (value === false) return "No";
|
|
3648
|
+
if (typeof value === "number") {
|
|
3649
|
+
return Number.isFinite(value) ? numberFormat.format(value) : null;
|
|
3650
|
+
}
|
|
3651
|
+
const text2 = String(value).trim();
|
|
3652
|
+
if (text2 === "") return null;
|
|
3653
|
+
if (privilege.value_type === "boolean" && text2.toLowerCase() === "false") {
|
|
3654
|
+
return "No";
|
|
3655
|
+
}
|
|
3656
|
+
if (privilege.value_type === "integer" && /^-?\d+$/.test(text2)) {
|
|
3657
|
+
const parsed = Number(text2);
|
|
3658
|
+
if (Number.isSafeInteger(parsed)) return numberFormat.format(parsed);
|
|
3659
|
+
}
|
|
3660
|
+
return text2;
|
|
3661
|
+
}
|
|
3662
|
+
function featureLabel(name, entitlement, numberFormat) {
|
|
3663
|
+
const values = (entitlement?.privileges ?? []).map((privilege) => ({
|
|
3664
|
+
privilege,
|
|
3665
|
+
text: formatPrivilegeValue(privilege, numberFormat)
|
|
3666
|
+
})).filter(
|
|
3667
|
+
(entry) => entry.text !== null
|
|
3668
|
+
);
|
|
3669
|
+
if (values.length === 1) return `${name}: ${values[0].text}`;
|
|
3670
|
+
if (values.length > 1) {
|
|
3671
|
+
const parts = values.map(({ privilege, text: text2 }) => {
|
|
3672
|
+
const privilegeName = privilege.name?.trim() || privilege.code;
|
|
3673
|
+
return `${privilegeName}: ${text2}`;
|
|
3674
|
+
});
|
|
3675
|
+
return `${name} (${parts.join(", ")})`;
|
|
3676
|
+
}
|
|
3677
|
+
const limit = entitlement?.usage_limit;
|
|
3678
|
+
if (typeof limit === "number" && Number.isFinite(limit)) {
|
|
3679
|
+
return `${name}: ${numberFormat.format(limit)}`;
|
|
3680
|
+
}
|
|
3681
|
+
return name;
|
|
3682
|
+
}
|
|
3683
|
+
function trimmed(value) {
|
|
3684
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3685
|
+
}
|
|
3686
|
+
function familyFeatures(family, plan, locale) {
|
|
3687
|
+
const numberFormat = new Intl.NumberFormat(locale);
|
|
3688
|
+
const entitlements = /* @__PURE__ */ new Map();
|
|
3689
|
+
for (const entitlement of plan.entitlements ?? []) {
|
|
3690
|
+
if (!entitlements.has(entitlement.code)) {
|
|
3691
|
+
entitlements.set(entitlement.code, entitlement);
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
return (family.features ?? []).filter((feature) => feature.show_on_pricing !== false).map((feature, index) => {
|
|
3695
|
+
const entitlement = entitlements.get(feature.code);
|
|
3696
|
+
const name = trimmed(feature.name) || trimmed(entitlement?.name) || feature.code;
|
|
3697
|
+
return { feature, entitlement, index, name };
|
|
3698
|
+
}).sort(
|
|
3699
|
+
(a, b) => comparePositions(a.feature.display_position, b.feature.display_position) || a.name.localeCompare(b.name, locale) || a.index - b.index
|
|
3700
|
+
).map(({ feature, entitlement, name }) => {
|
|
3701
|
+
const label = featureLabel(name, entitlement, numberFormat);
|
|
3702
|
+
const description = trimmed(feature.description);
|
|
3703
|
+
return feature.show_description === true && description ? { label, description } : { label };
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
function planCardFeatures(plan) {
|
|
3707
|
+
return planFeatures(plan).map((label) => ({ label }));
|
|
3708
|
+
}
|
|
3709
|
+
function normalizeFeatures(items) {
|
|
3710
|
+
return items.map((item) => typeof item === "string" ? { label: item } : item).filter(
|
|
3711
|
+
(item) => Boolean(item) && typeof item.label === "string"
|
|
3712
|
+
);
|
|
3713
|
+
}
|
|
3714
|
+
function overrideFeatures(overrides, group, plan, cardIndex) {
|
|
3715
|
+
if (!overrides) return null;
|
|
3716
|
+
if (Array.isArray(overrides)) {
|
|
3717
|
+
const entry = overrides[cardIndex];
|
|
3718
|
+
return entry ? normalizeFeatures(entry) : null;
|
|
3719
|
+
}
|
|
3720
|
+
const keys = [group.key, plan.code, ...group.plans.map((p) => p.code)];
|
|
3721
|
+
for (const key of keys) {
|
|
3722
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) {
|
|
3723
|
+
return normalizeFeatures(overrides[key] ?? []);
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return null;
|
|
3727
|
+
}
|
|
3728
|
+
function yearlyMinor(plan) {
|
|
3729
|
+
const amount = toMinor(plan.amount_cents);
|
|
3730
|
+
if (amount === null || !isPricingInterval(plan.interval)) return null;
|
|
3731
|
+
return amount * PERIODS_PER_YEAR[plan.interval];
|
|
3732
|
+
}
|
|
3733
|
+
function planPriceStrings(plan, locale) {
|
|
3734
|
+
const amount = toMinor(plan.amount_cents);
|
|
3735
|
+
return {
|
|
3736
|
+
price: fmt(amount ?? plan.amount_cents, plan.amount_currency, locale),
|
|
3737
|
+
pricePeriod: plan.interval ? `/${intervalLabel(plan.interval)}` : "",
|
|
3738
|
+
billing: "",
|
|
3739
|
+
savingsPercent: null
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function priceStrings(plan, monthly, locale) {
|
|
3743
|
+
const amount = toMinor(plan.amount_cents);
|
|
3744
|
+
const currency = plan.amount_currency;
|
|
3745
|
+
if (!isPricingInterval(plan.interval) || amount === null) {
|
|
3746
|
+
return {
|
|
3747
|
+
price: fmt(amount ?? plan.amount_cents, currency, locale),
|
|
3748
|
+
pricePeriod: "",
|
|
3749
|
+
billing: plan.interval ? `Billed ${plan.interval}` : "",
|
|
3750
|
+
savingsPercent: null
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
const price = fmt(perMonthMinor(amount, plan.interval), currency, locale);
|
|
3754
|
+
if (plan.interval === "monthly") {
|
|
3755
|
+
return { price, pricePeriod: "/mo", billing: "Billed monthly", savingsPercent: null };
|
|
3756
|
+
}
|
|
3757
|
+
const savingsPercent = savingsAgainstMonthly(plan, monthly);
|
|
3758
|
+
const billed = `Billed ${fmt(amount, currency, locale)} ${BILLED_WORDS[plan.interval]}`;
|
|
3759
|
+
return {
|
|
3760
|
+
price,
|
|
3761
|
+
pricePeriod: "/mo",
|
|
3762
|
+
billing: savingsPercent === null ? billed : `${billed} \xB7 save ${savingsPercent}%`,
|
|
3763
|
+
savingsPercent
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
function getPricingCards(catalog, options = {}) {
|
|
3767
|
+
const locale = resolvePricingLocale(options.locale);
|
|
3768
|
+
const families = hasFamilies(catalog);
|
|
3769
|
+
const requested = normalizePricingInterval(options.interval ?? null);
|
|
3770
|
+
const interval = requested ?? getDefaultInterval(catalog);
|
|
3771
|
+
const contactSales = new Set(options.contactSales ?? []);
|
|
3772
|
+
const isContactSales = (group) => contactSales.has(group.key) || group.plans.some((plan) => contactSales.has(plan.code));
|
|
3773
|
+
let groups = getPricingGroups(catalog);
|
|
3774
|
+
if (!families) {
|
|
3775
|
+
if (options.sort) {
|
|
3776
|
+
const sort = options.sort;
|
|
3777
|
+
groups = groups.map((group, index) => ({ group, index })).sort((a, b) => sort(a.group.plans[0], b.group.plans[0]) || a.index - b.index).map(({ group }) => group);
|
|
3778
|
+
} else {
|
|
3779
|
+
const sales = groups.filter(isContactSales);
|
|
3780
|
+
const byPlan = new Map(groups.map((group) => [group.plans[0], group]));
|
|
3781
|
+
groups = [
|
|
3782
|
+
...sortPlansByPrice(groups.filter((group) => !isContactSales(group)).map((group) => group.plans[0])).map((plan) => byPlan.get(plan)),
|
|
3783
|
+
...sales
|
|
3784
|
+
];
|
|
3785
|
+
}
|
|
3786
|
+
if (requested) {
|
|
3787
|
+
groups = groups.filter(
|
|
3788
|
+
(group) => group.plans[0].interval === requested || isContactSales(group)
|
|
3789
|
+
);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
const highlight = options.highlight ?? null;
|
|
3793
|
+
const highlightedKey = highlight !== null ? groups.find(
|
|
3794
|
+
(group) => group.key === highlight || group.plans.some((plan) => plan.code === highlight)
|
|
3795
|
+
)?.key : groups.find((group) => group.family?.pricing_highlighted === true)?.key;
|
|
3796
|
+
const badge = (family) => options.badge !== void 0 ? options.badge === null ? null : trimmed(options.badge) || null : trimmed(family?.badge_text) || "Most Popular";
|
|
3797
|
+
return groups.map((group, cardIndex) => {
|
|
3798
|
+
const plan = pickPlan(group.plans, interval);
|
|
3799
|
+
const family = group.family;
|
|
3800
|
+
const name = trimmed(family?.name) || plan.name;
|
|
3801
|
+
const description = trimmed(family?.description) || trimmed(plan.description) || null;
|
|
3802
|
+
const highlighted = highlightedKey !== void 0 && highlightedKey === group.key;
|
|
3803
|
+
const features = overrideFeatures(options.features, group, plan, cardIndex) ?? (family ? familyFeatures(family, plan, locale) : planCardFeatures(plan));
|
|
3804
|
+
return {
|
|
3805
|
+
key: group.key,
|
|
3806
|
+
family,
|
|
3807
|
+
plans: group.plans,
|
|
3808
|
+
plan,
|
|
3809
|
+
isFallbackInterval: families && interval !== null && plan.interval !== interval,
|
|
3810
|
+
name,
|
|
3811
|
+
description,
|
|
3812
|
+
...family ? priceStrings(plan, monthlyPlanOf(group), locale) : planPriceStrings(plan, locale),
|
|
3813
|
+
features,
|
|
3814
|
+
highlighted,
|
|
3815
|
+
badge: highlighted ? badge(family) : null,
|
|
3816
|
+
cta: trimmed(family?.cta_text) || `Get ${name}`,
|
|
3817
|
+
enterprise: isContactSales(group)
|
|
3818
|
+
};
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3458
3821
|
function PanelPlanCard({
|
|
3459
3822
|
frame,
|
|
3460
3823
|
el,
|
|
@@ -3462,6 +3825,7 @@ function PanelPlanCard({
|
|
|
3462
3825
|
description,
|
|
3463
3826
|
price,
|
|
3464
3827
|
interval,
|
|
3828
|
+
priceNote,
|
|
3465
3829
|
features,
|
|
3466
3830
|
isCurrent,
|
|
3467
3831
|
isHighlighted,
|
|
@@ -3526,6 +3890,7 @@ function PanelPlanCard({
|
|
|
3526
3890
|
interval
|
|
3527
3891
|
] })
|
|
3528
3892
|
] }),
|
|
3893
|
+
priceNote && /* @__PURE__ */ jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "0.25rem 0 0" }, children: priceNote }),
|
|
3529
3894
|
description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", { ...text.secondary, color: muted5, margin: "0.25rem 0 0" }), children: description })
|
|
3530
3895
|
] }),
|
|
3531
3896
|
/* @__PURE__ */ jsx(
|
|
@@ -3542,7 +3907,13 @@ function PanelPlanCard({
|
|
|
3542
3907
|
...text.secondary,
|
|
3543
3908
|
color: muted5
|
|
3544
3909
|
}),
|
|
3545
|
-
children: features.map((feature) =>
|
|
3910
|
+
children: features.map((feature, index) => {
|
|
3911
|
+
const item = typeof feature === "string" ? { label: feature } : feature;
|
|
3912
|
+
return /* @__PURE__ */ jsxs("li", { ...el("planCard__feature"), children: [
|
|
3913
|
+
item.label,
|
|
3914
|
+
item.description && /* @__PURE__ */ jsx("span", { style: { display: "block", ...text.meta, color: muted5 }, children: item.description })
|
|
3915
|
+
] }, `${index}:${item.label}`);
|
|
3916
|
+
})
|
|
3546
3917
|
}
|
|
3547
3918
|
),
|
|
3548
3919
|
children
|
|
@@ -3551,61 +3922,66 @@ function PanelPlanCard({
|
|
|
3551
3922
|
}
|
|
3552
3923
|
);
|
|
3553
3924
|
}
|
|
3554
|
-
function featureLines(override, plan, index) {
|
|
3555
|
-
const given = Array.isArray(override) ? override[index] : override?.[plan.code];
|
|
3556
|
-
return given ?? planFeatures(plan);
|
|
3557
|
-
}
|
|
3558
|
-
var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
|
|
3559
|
-
function fmtPrice(cents, cur) {
|
|
3560
|
-
const s = SYMBOLS[cur] ?? cur + " ";
|
|
3561
|
-
const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
|
|
3562
|
-
return `${s}${val}`;
|
|
3563
|
-
}
|
|
3564
|
-
var C = {
|
|
3565
|
-
cardBg: T.card,
|
|
3566
|
-
hl: T.primary,
|
|
3567
|
-
border: T.border,
|
|
3568
|
-
radius: T.radius,
|
|
3569
|
-
text: T.foreground,
|
|
3570
|
-
muted: T.mutedForeground,
|
|
3571
|
-
mutedBg: T.muted,
|
|
3572
|
-
hlFg: T.primaryForeground
|
|
3573
|
-
};
|
|
3574
3925
|
function Check() {
|
|
3575
3926
|
return /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
3576
3927
|
}
|
|
3577
|
-
function
|
|
3928
|
+
function IntervalSwitch({
|
|
3929
|
+
options,
|
|
3930
|
+
selected,
|
|
3931
|
+
onChange,
|
|
3932
|
+
annualDiscountLabel,
|
|
3933
|
+
el
|
|
3934
|
+
}) {
|
|
3578
3935
|
const pill = {
|
|
3579
3936
|
display: "inline-flex",
|
|
3937
|
+
flexWrap: "wrap",
|
|
3938
|
+
justifyContent: "center",
|
|
3580
3939
|
borderRadius: "9999px",
|
|
3581
3940
|
padding: "4px",
|
|
3582
|
-
background:
|
|
3941
|
+
background: T.muted,
|
|
3583
3942
|
gap: "2px"
|
|
3584
3943
|
};
|
|
3585
3944
|
const tab = (active) => ({
|
|
3586
3945
|
padding: "8px 20px",
|
|
3587
3946
|
borderRadius: "9999px",
|
|
3588
3947
|
border: "none",
|
|
3589
|
-
background: active ?
|
|
3948
|
+
background: active ? T.card : "transparent",
|
|
3590
3949
|
...text.action,
|
|
3591
|
-
color: active ?
|
|
3950
|
+
color: active ? T.foreground : T.mutedForeground,
|
|
3592
3951
|
cursor: "pointer",
|
|
3593
3952
|
boxShadow: active ? T.shadow : "none",
|
|
3594
3953
|
transition: "all 0.15s ease"
|
|
3595
3954
|
});
|
|
3596
|
-
return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3955
|
+
return /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsx("div", { role: "group", "aria-label": "Billing interval", ...el("pricingTable__toggle", pill), children: options.map((option) => {
|
|
3956
|
+
const active = option.interval === selected;
|
|
3957
|
+
const note = option.interval === "yearly" && annualDiscountLabel ? annualDiscountLabel : option.interval !== "monthly" && option.savingsPercent !== null ? `Save ${option.savingsPercent}%` : null;
|
|
3958
|
+
return /* @__PURE__ */ jsxs(
|
|
3959
|
+
"button",
|
|
3960
|
+
{
|
|
3961
|
+
type: "button",
|
|
3962
|
+
"data-interval": option.interval,
|
|
3963
|
+
onClick: () => onChange(option.interval),
|
|
3964
|
+
...el("pricingTable__toggleButton", tab(active)),
|
|
3965
|
+
"aria-pressed": active,
|
|
3966
|
+
"aria-label": note ? `${option.label}, ${note.replace(/^Save /, "save ")}` : void 0,
|
|
3967
|
+
children: [
|
|
3968
|
+
option.label,
|
|
3969
|
+
note && /* @__PURE__ */ jsx("span", { style: {
|
|
3970
|
+
marginLeft: "6px",
|
|
3971
|
+
...text.meta,
|
|
3972
|
+
fontWeight: 600,
|
|
3973
|
+
color: active ? T.primary : T.mutedForeground
|
|
3974
|
+
}, children: note })
|
|
3975
|
+
]
|
|
3976
|
+
},
|
|
3977
|
+
option.interval
|
|
3978
|
+
);
|
|
3979
|
+
}) }) });
|
|
3600
3980
|
}
|
|
3601
3981
|
function Card({
|
|
3602
|
-
|
|
3603
|
-
features,
|
|
3982
|
+
card,
|
|
3604
3983
|
isCurrent,
|
|
3605
|
-
isHighlighted,
|
|
3606
3984
|
filled: filledStyle,
|
|
3607
|
-
highlightLabel,
|
|
3608
|
-
isEnt,
|
|
3609
3985
|
enterpriseEmail,
|
|
3610
3986
|
onSelect,
|
|
3611
3987
|
el,
|
|
@@ -3613,12 +3989,14 @@ function Card({
|
|
|
3613
3989
|
isUpgrade
|
|
3614
3990
|
}) {
|
|
3615
3991
|
const [hover, setHover] = useState(false);
|
|
3992
|
+
const { plan, features } = card;
|
|
3993
|
+
const isHighlighted = card.highlighted;
|
|
3994
|
+
const isEnt = card.enterprise;
|
|
3616
3995
|
const filled = isHighlighted && filledStyle;
|
|
3617
|
-
const text2 = filled ? T.highlightForeground :
|
|
3618
|
-
const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` :
|
|
3619
|
-
const accent = filled ? T.highlightForeground :
|
|
3620
|
-
const badgeLabel = isCurrent ? "Current Plan" :
|
|
3621
|
-
const interval = `/${intervalLabel(plan.interval)}`;
|
|
3996
|
+
const text2 = filled ? T.highlightForeground : T.foreground;
|
|
3997
|
+
const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : T.mutedForeground;
|
|
3998
|
+
const accent = filled ? T.highlightForeground : T.primary;
|
|
3999
|
+
const badgeLabel = isCurrent ? "Current Plan" : card.badge;
|
|
3622
4000
|
const handleClick = () => {
|
|
3623
4001
|
if (isCurrent) return;
|
|
3624
4002
|
if (isEnt && enterpriseEmail) {
|
|
@@ -3627,6 +4005,7 @@ function Card({
|
|
|
3627
4005
|
}
|
|
3628
4006
|
onSelect?.();
|
|
3629
4007
|
};
|
|
4008
|
+
const ctaText = isCurrent ? "Current Plan" : card.family?.cta_text?.trim() ? card.cta : isEnt ? "Contact Sales" : card.cta;
|
|
3630
4009
|
const emphasised = isUpgrade ?? isHighlighted;
|
|
3631
4010
|
const cta = onSelect && /* @__PURE__ */ jsx(
|
|
3632
4011
|
CTAButton,
|
|
@@ -3635,24 +4014,32 @@ function Card({
|
|
|
3635
4014
|
disabled: isCurrent,
|
|
3636
4015
|
variant: isCurrent ? "current" : isEnt ? "outline" : !emphasised ? "secondary" : filled ? "inverse" : isHighlighted ? "highlight" : "primary",
|
|
3637
4016
|
el,
|
|
3638
|
-
children:
|
|
4017
|
+
children: ctaText
|
|
3639
4018
|
}
|
|
3640
4019
|
);
|
|
4020
|
+
const dataAttributes = {
|
|
4021
|
+
"data-card-key": card.key,
|
|
4022
|
+
"data-family-code": card.family?.code,
|
|
4023
|
+
"data-plan-code": plan.code,
|
|
4024
|
+
"data-interval": plan.interval,
|
|
4025
|
+
"data-highlighted": isHighlighted ? "true" : void 0
|
|
4026
|
+
};
|
|
3641
4027
|
if (panel) {
|
|
3642
4028
|
return /* @__PURE__ */ jsx(
|
|
3643
4029
|
PanelPlanCard,
|
|
3644
4030
|
{
|
|
3645
|
-
frame: (style) => el("planCard", style),
|
|
4031
|
+
frame: (style) => ({ ...el("planCard", style), ...dataAttributes }),
|
|
3646
4032
|
el,
|
|
3647
|
-
name:
|
|
3648
|
-
description:
|
|
3649
|
-
price: isEnt ? "Custom" :
|
|
3650
|
-
interval: isEnt ? void 0 :
|
|
3651
|
-
|
|
4033
|
+
name: card.name,
|
|
4034
|
+
description: card.description,
|
|
4035
|
+
price: isEnt ? "Custom" : card.price,
|
|
4036
|
+
interval: isEnt || !card.pricePeriod ? void 0 : card.pricePeriod.replace(/^\//, ""),
|
|
4037
|
+
priceNote: isEnt ? null : card.billing,
|
|
4038
|
+
features,
|
|
3652
4039
|
isCurrent,
|
|
3653
4040
|
isHighlighted,
|
|
3654
4041
|
filled: filledStyle,
|
|
3655
|
-
highlightLabel,
|
|
4042
|
+
highlightLabel: card.badge,
|
|
3656
4043
|
children: cta
|
|
3657
4044
|
}
|
|
3658
4045
|
);
|
|
@@ -3660,17 +4047,18 @@ function Card({
|
|
|
3660
4047
|
return /* @__PURE__ */ jsxs(
|
|
3661
4048
|
"div",
|
|
3662
4049
|
{
|
|
4050
|
+
...dataAttributes,
|
|
3663
4051
|
onMouseEnter: () => setHover(true),
|
|
3664
4052
|
onMouseLeave: () => setHover(false),
|
|
3665
4053
|
...el("planCard", {
|
|
3666
4054
|
position: "relative",
|
|
3667
4055
|
display: "flex",
|
|
3668
4056
|
flexDirection: "column",
|
|
3669
|
-
background: filled ? T.highlight :
|
|
4057
|
+
background: filled ? T.highlight : T.card,
|
|
3670
4058
|
color: text2,
|
|
3671
|
-
border: border(isHighlighted ? T.highlight :
|
|
4059
|
+
border: border(isHighlighted ? T.highlight : T.border),
|
|
3672
4060
|
borderTopWidth: isHighlighted && !filled ? `calc(${T.borderWidth} + 2px)` : T.borderWidth,
|
|
3673
|
-
borderRadius:
|
|
4061
|
+
borderRadius: T.radius,
|
|
3674
4062
|
padding: "2rem",
|
|
3675
4063
|
transition: "box-shadow 0.2s ease, transform 0.2s ease",
|
|
3676
4064
|
boxShadow: T.shadow,
|
|
@@ -3695,7 +4083,7 @@ function Card({
|
|
|
3695
4083
|
top: "0",
|
|
3696
4084
|
left: "50%",
|
|
3697
4085
|
transform: "translate(-50%, -50%)",
|
|
3698
|
-
background:
|
|
4086
|
+
background: T.muted,
|
|
3699
4087
|
...text.label,
|
|
3700
4088
|
padding: "4px 12px",
|
|
3701
4089
|
borderRadius: "9999px",
|
|
@@ -3706,18 +4094,21 @@ function Card({
|
|
|
3706
4094
|
...text.heading,
|
|
3707
4095
|
color: text2,
|
|
3708
4096
|
margin: 0
|
|
3709
|
-
}), children:
|
|
3710
|
-
|
|
4097
|
+
}), children: card.name }),
|
|
4098
|
+
card.description && /* @__PURE__ */ jsx("p", { ...el("planCard__description", {
|
|
3711
4099
|
...text.secondary,
|
|
3712
4100
|
color: muted5,
|
|
3713
4101
|
margin: "6px 0 0"
|
|
3714
|
-
}), children:
|
|
4102
|
+
}), children: card.description })
|
|
3715
4103
|
] }),
|
|
3716
|
-
/* @__PURE__ */ jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxs(
|
|
3717
|
-
/* @__PURE__ */
|
|
3718
|
-
|
|
4104
|
+
/* @__PURE__ */ jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4105
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
|
|
4106
|
+
/* @__PURE__ */ jsx("span", { "data-testid": "pricing-price", style: { ...text.metric, color: text2 }, children: card.price }),
|
|
4107
|
+
card.pricePeriod && /* @__PURE__ */ jsx("span", { style: { ...text.meta, color: muted5, marginLeft: "4px" }, children: card.pricePeriod })
|
|
4108
|
+
] }),
|
|
4109
|
+
card.billing && /* @__PURE__ */ jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "8px 0 0" }, children: card.billing })
|
|
3719
4110
|
] }) }),
|
|
3720
|
-
features
|
|
4111
|
+
features.length > 0 && /* @__PURE__ */ jsx("ul", { ...el("planCard__features", {
|
|
3721
4112
|
listStyle: "none",
|
|
3722
4113
|
padding: 0,
|
|
3723
4114
|
margin: "0 0 2rem",
|
|
@@ -3725,7 +4116,7 @@ function Card({
|
|
|
3725
4116
|
flexDirection: "column",
|
|
3726
4117
|
gap: "0.75rem",
|
|
3727
4118
|
flex: 1
|
|
3728
|
-
}), children: features.map((feat) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", {
|
|
4119
|
+
}), children: features.map((feat, index) => /* @__PURE__ */ jsxs("li", { ...el("planCard__feature", {
|
|
3729
4120
|
display: "flex",
|
|
3730
4121
|
alignItems: "flex-start",
|
|
3731
4122
|
gap: "10px",
|
|
@@ -3733,9 +4124,12 @@ function Card({
|
|
|
3733
4124
|
color: text2
|
|
3734
4125
|
}), children: [
|
|
3735
4126
|
/* @__PURE__ */ jsx("span", { style: { color: accent, marginTop: "2px" }, children: /* @__PURE__ */ jsx(Check, {}) }),
|
|
3736
|
-
/* @__PURE__ */
|
|
3737
|
-
|
|
3738
|
-
|
|
4127
|
+
/* @__PURE__ */ jsxs("span", { style: { display: "flex", flexDirection: "column" }, children: [
|
|
4128
|
+
/* @__PURE__ */ jsx("span", { children: feat.label }),
|
|
4129
|
+
feat.description && /* @__PURE__ */ jsx("span", { style: { ...text.meta, color: muted5, marginTop: "2px" }, children: feat.description })
|
|
4130
|
+
] })
|
|
4131
|
+
] }, `${index}:${feat.label}`)) }),
|
|
4132
|
+
features.length === 0 && /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
|
|
3739
4133
|
cta
|
|
3740
4134
|
]
|
|
3741
4135
|
}
|
|
@@ -3749,7 +4143,7 @@ function CTAButton({
|
|
|
3749
4143
|
el
|
|
3750
4144
|
}) {
|
|
3751
4145
|
const [hover, setHover] = useState(false);
|
|
3752
|
-
const look = variant === "current" ? { ...buttonStyle("secondary"), background:
|
|
4146
|
+
const look = variant === "current" ? { ...buttonStyle("secondary"), background: T.muted, color: T.mutedForeground, cursor: "default" } : variant === "highlight" ? { ...buttonStyle("primary"), background: T.highlight, color: T.highlightForeground, border: primaryButtonBorder(T.highlight) } : variant === "inverse" ? { ...buttonStyle("primary"), background: T.highlightForeground, color: T.highlight, border: primaryButtonBorder(T.highlightForeground) } : buttonStyle(variant);
|
|
3753
4147
|
return /* @__PURE__ */ jsx(
|
|
3754
4148
|
"button",
|
|
3755
4149
|
{
|
|
@@ -3774,9 +4168,9 @@ function Skeleton({ rootProps }) {
|
|
|
3774
4168
|
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
3775
4169
|
gap: "1.5rem"
|
|
3776
4170
|
}, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { style: {
|
|
3777
|
-
background:
|
|
4171
|
+
background: T.card,
|
|
3778
4172
|
border: border(),
|
|
3779
|
-
borderRadius:
|
|
4173
|
+
borderRadius: T.radius,
|
|
3780
4174
|
padding: "2rem",
|
|
3781
4175
|
height: "380px"
|
|
3782
4176
|
}, children: [
|
|
@@ -3790,6 +4184,7 @@ function Skeleton({ rootProps }) {
|
|
|
3790
4184
|
function PricingTable({
|
|
3791
4185
|
currentPlanCode,
|
|
3792
4186
|
plans: propPlans,
|
|
4187
|
+
families: propFamilies,
|
|
3793
4188
|
features,
|
|
3794
4189
|
sort,
|
|
3795
4190
|
onSelect,
|
|
@@ -3798,36 +4193,40 @@ function PricingTable({
|
|
|
3798
4193
|
onCheckoutResult,
|
|
3799
4194
|
onCheckoutError,
|
|
3800
4195
|
showToggle = true,
|
|
4196
|
+
defaultInterval,
|
|
3801
4197
|
highlightPlan,
|
|
3802
4198
|
highlightStyle = "outline",
|
|
3803
|
-
highlightLabel
|
|
4199
|
+
highlightLabel,
|
|
3804
4200
|
className,
|
|
3805
4201
|
contactSalesPlans,
|
|
3806
4202
|
enterpriseEmail,
|
|
4203
|
+
annualDiscountLabel,
|
|
4204
|
+
locale: localeProp,
|
|
3807
4205
|
appearance
|
|
3808
4206
|
}) {
|
|
3809
4207
|
const { root, el, planCardLayout } = useAppearance(appearance);
|
|
3810
|
-
const
|
|
4208
|
+
const locale = resolvePricingLocale(localeProp);
|
|
3811
4209
|
const billing = useOptionalBillingContext();
|
|
3812
4210
|
const client = billing?.client;
|
|
3813
|
-
const
|
|
4211
|
+
const fetched = useQuery(
|
|
3814
4212
|
{ ...catalogQuery(client), enabled: !propPlans && Boolean(client) },
|
|
3815
4213
|
billing?.queryClient ?? idleQueryClient
|
|
3816
4214
|
);
|
|
3817
|
-
const
|
|
3818
|
-
const loading = !propPlans && Boolean(client) &&
|
|
3819
|
-
const loadError = propPlans ? null : !client ? "BillingProvider with a publishableKey is required to load plans" :
|
|
4215
|
+
const catalog = propPlans ? { plans: propPlans, families: Array.isArray(propFamilies) ? propFamilies : null } : { plans: fetched.data?.plans ?? [], families: fetched.data?.families ?? null };
|
|
4216
|
+
const loading = !propPlans && Boolean(client) && fetched.isPending;
|
|
4217
|
+
const loadError = propPlans ? null : !client ? "BillingProvider with a publishableKey is required to load plans" : fetched.error ? `Plans could not be loaded: ${fetched.error.message}` : null;
|
|
3820
4218
|
const [currentPlan, setCurrentPlan] = useState(currentPlanCode ?? null);
|
|
3821
|
-
const [
|
|
4219
|
+
const [selectedInterval, setSelectedInterval] = useState(null);
|
|
3822
4220
|
const [processing, setProcessing] = useState(false);
|
|
3823
4221
|
const [checkoutError, setCheckoutError] = useState(null);
|
|
3824
4222
|
useEffect(() => {
|
|
3825
4223
|
if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
|
|
3826
4224
|
}, [currentPlanCode]);
|
|
3827
|
-
|
|
4225
|
+
const withFamilies = Array.isArray(catalog.families);
|
|
4226
|
+
async function selectPlan(plan, family) {
|
|
3828
4227
|
setCheckoutError(null);
|
|
3829
4228
|
if (onSelect) {
|
|
3830
|
-
onSelect(plan);
|
|
4229
|
+
onSelect(plan, family);
|
|
3831
4230
|
return;
|
|
3832
4231
|
}
|
|
3833
4232
|
try {
|
|
@@ -3853,37 +4252,57 @@ function PricingTable({
|
|
|
3853
4252
|
onCheckoutError?.(error);
|
|
3854
4253
|
}
|
|
3855
4254
|
}
|
|
3856
|
-
const
|
|
3857
|
-
const
|
|
3858
|
-
const
|
|
3859
|
-
const
|
|
4255
|
+
const available = getAvailableIntervals(catalog);
|
|
4256
|
+
const interval = selectedInterval !== null && available.includes(selectedInterval) ? selectedInterval : getDefaultInterval(catalog, defaultInterval);
|
|
4257
|
+
const switchVisible = showToggle && available.length > 1 && interval !== null;
|
|
4258
|
+
const cards = getPricingCards(catalog, {
|
|
4259
|
+
interval: withFamilies || switchVisible ? interval : null,
|
|
4260
|
+
locale,
|
|
4261
|
+
highlight: highlightPlan,
|
|
4262
|
+
badge: highlightLabel,
|
|
4263
|
+
features,
|
|
4264
|
+
contactSales: contactSalesPlans,
|
|
4265
|
+
sort
|
|
4266
|
+
});
|
|
4267
|
+
const current = catalog.plans.find((p) => p.code === currentPlan);
|
|
4268
|
+
const currentYearly = current ? yearlyMinor(current) : null;
|
|
4269
|
+
const isUpgrade = (plan) => {
|
|
4270
|
+
if (!current || current.amount_currency !== plan.amount_currency) return void 0;
|
|
4271
|
+
const yearly = yearlyMinor(plan);
|
|
4272
|
+
return currentYearly !== null && yearly !== null ? yearly > currentYearly : plan.amount_cents > current.amount_cents;
|
|
4273
|
+
};
|
|
3860
4274
|
const rootProps = root("pricingTable", text.body, className);
|
|
3861
4275
|
if (loading) return /* @__PURE__ */ jsx(Skeleton, { rootProps });
|
|
3862
4276
|
return /* @__PURE__ */ jsxs("div", { ...rootProps, children: [
|
|
3863
|
-
|
|
4277
|
+
switchVisible && interval !== null && /* @__PURE__ */ jsx(
|
|
4278
|
+
IntervalSwitch,
|
|
4279
|
+
{
|
|
4280
|
+
options: getPricingIntervalOptions(catalog),
|
|
4281
|
+
selected: interval,
|
|
4282
|
+
onChange: setSelectedInterval,
|
|
4283
|
+
annualDiscountLabel,
|
|
4284
|
+
el
|
|
4285
|
+
}
|
|
4286
|
+
),
|
|
3864
4287
|
processing && /* @__PURE__ */ jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
|
|
3865
4288
|
(checkoutError ?? loadError) && /* @__PURE__ */ jsx("p", { role: "alert", style: { color: T.dangerText }, children: checkoutError ?? loadError }),
|
|
3866
4289
|
/* @__PURE__ */ jsx("div", { ...el("pricingTable__grid", {
|
|
3867
4290
|
display: "grid",
|
|
3868
4291
|
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
3869
4292
|
gap: "1.5rem"
|
|
3870
|
-
}), children:
|
|
4293
|
+
}), children: cards.map((card) => /* @__PURE__ */ jsx(
|
|
3871
4294
|
Card,
|
|
3872
4295
|
{
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
isUpgrade: current && current.amount_currency === plan.amount_currency ? plan.amount_cents > current.amount_cents : void 0,
|
|
3877
|
-
isHighlighted: highlightPlan === plan.code,
|
|
4296
|
+
card,
|
|
4297
|
+
isCurrent: currentPlan === card.plan.code,
|
|
4298
|
+
isUpgrade: isUpgrade(card.plan),
|
|
3878
4299
|
filled: highlightStyle === "filled",
|
|
3879
|
-
highlightLabel,
|
|
3880
|
-
isEnt: isEnterprise(plan),
|
|
3881
4300
|
enterpriseEmail,
|
|
3882
|
-
onSelect: () => void selectPlan(plan),
|
|
4301
|
+
onSelect: () => void selectPlan(card.plan, card.family),
|
|
3883
4302
|
el,
|
|
3884
4303
|
panel: planCardLayout === "panel"
|
|
3885
4304
|
},
|
|
3886
|
-
|
|
4305
|
+
card.key
|
|
3887
4306
|
)) }),
|
|
3888
4307
|
/* @__PURE__ */ jsx(NozleBranding, {})
|
|
3889
4308
|
] });
|
|
@@ -4903,6 +5322,6 @@ function UsageTrend({
|
|
|
4903
5322
|
] }, point.key)) }) });
|
|
4904
5323
|
}
|
|
4905
5324
|
|
|
4906
|
-
export { BillingContext, BillingPortal, BillingPortalError, BillingProvider, BillingRequestError, CancellationControl, Checkout, CheckoutButton, CreditBalance, CreditHistory, CurrentPlan, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanChangeControl, PlanComparison, PlanGate, PoweredByNozle, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageBreakdown, UsageDashboard, UsageGate, UsageMeter, UsageTrend, comparisonRows, currentPlanMeters, getUsageMeterColor, planFeatures, sortPlansByPrice, usageItems, useAppearance, useBillingContext, useBillingRefresh, useCheckout, useCheckout2 as useCheckoutSession, useCreditBalance, useCreditHistory, useEntitlement, useInvoiceDownload, useInvoices, useNozleBranding, useNozleClient, useOptionalBillingContext, usePlans, useSubscription, useUpgradePreview, useUsage };
|
|
5325
|
+
export { BillingContext, BillingPortal, BillingPortalError, BillingProvider, BillingRequestError, CancellationControl, Checkout, CheckoutButton, CreditBalance, CreditHistory, CurrentPlan, FeatureGate, LockedOverlay, PaymentMethodDisplay, PlanBadge, PlanCard, PlanChangeControl, PlanComparison, PlanGate, PoweredByNozle, PricingTable, UpgradeButton, UpgradeModal, UpgradePrompt, UsageAlert, UsageBreakdown, UsageDashboard, UsageGate, UsageMeter, UsageTrend, comparisonRows, currentPlanMeters, getAvailableIntervals, getDefaultInterval, getPricingCards, getPricingIntervalOptions, getUsageMeterColor, planFeatures, sortPlansByPrice, usageItems, useAppearance, useBillingContext, useBillingRefresh, useCheckout, useCheckout2 as useCheckoutSession, useCreditBalance, useCreditHistory, useEntitlement, useInvoiceDownload, useInvoices, useNozleBranding, useNozleClient, useOptionalBillingContext, usePlans, useSubscription, useUpgradePreview, useUsage };
|
|
4907
5326
|
//# sourceMappingURL=index.js.map
|
|
4908
5327
|
//# sourceMappingURL=index.js.map
|