@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.cjs
CHANGED
|
@@ -411,7 +411,11 @@ function catalogQuery(client) {
|
|
|
411
411
|
const response = await client.catalogFetch("/api/v1/plans", { signal });
|
|
412
412
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
413
413
|
const data = await response.json();
|
|
414
|
-
return {
|
|
414
|
+
return {
|
|
415
|
+
plans: data.plans ?? [],
|
|
416
|
+
families: Array.isArray(data.families) ? data.families : null,
|
|
417
|
+
nozleBranding: data.meta?.nozle_branding === true
|
|
418
|
+
};
|
|
415
419
|
},
|
|
416
420
|
staleTime: 5 * 6e4,
|
|
417
421
|
// The pricing page shows a load error at once; the app can call refetch().
|
|
@@ -423,6 +427,7 @@ function usePlans() {
|
|
|
423
427
|
const query = reactQuery.useQuery(catalogQuery(client), queryClient);
|
|
424
428
|
return {
|
|
425
429
|
plans: query.data?.plans ?? [],
|
|
430
|
+
families: query.data?.families ?? null,
|
|
426
431
|
isLoading: query.isPending,
|
|
427
432
|
error: query.error ?? null,
|
|
428
433
|
refetch: query.refetch
|
|
@@ -1964,7 +1969,7 @@ function UpgradeModal(props) {
|
|
|
1964
1969
|
}
|
|
1965
1970
|
|
|
1966
1971
|
// src/components/billing/amounts.ts
|
|
1967
|
-
function formatMinorAmount(value, currency, locale = "en-US") {
|
|
1972
|
+
function formatMinorAmount(value, currency, locale = "en-US", options = {}) {
|
|
1968
1973
|
const raw = String(value);
|
|
1969
1974
|
if (!/^-?\d+$/.test(raw) || typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
1970
1975
|
return "Amount unavailable";
|
|
@@ -1980,12 +1985,16 @@ function formatMinorAmount(value, currency, locale = "en-US") {
|
|
|
1980
1985
|
const scale = 10n ** BigInt(digits);
|
|
1981
1986
|
const whole = absolute / scale;
|
|
1982
1987
|
const signed = minor < 0n ? whole === 0n ? -0 : -whole : whole;
|
|
1983
|
-
const
|
|
1988
|
+
const remainder = absolute % scale;
|
|
1989
|
+
const trim = options.trimZeroFraction === true && remainder === 0n;
|
|
1990
|
+
const decimal = String(remainder).padStart(digits, "0");
|
|
1984
1991
|
const digitFormatter = new Intl.NumberFormat(locale, {
|
|
1985
1992
|
useGrouping: false
|
|
1986
1993
|
});
|
|
1987
1994
|
const fraction = [...decimal].map((digit) => digitFormatter.format(Number(digit))).join("");
|
|
1988
|
-
return formatter.formatToParts(signed).
|
|
1995
|
+
return formatter.formatToParts(signed).filter(
|
|
1996
|
+
(part) => !trim || part.type !== "fraction" && part.type !== "decimal"
|
|
1997
|
+
).map((part) => part.type === "fraction" ? fraction : part.value).join("");
|
|
1989
1998
|
} catch {
|
|
1990
1999
|
return "Amount unavailable";
|
|
1991
2000
|
}
|
|
@@ -3701,6 +3710,360 @@ function useCheckout2() {
|
|
|
3701
3710
|
);
|
|
3702
3711
|
return { fetchClientSecret, checkout, isLoading, error };
|
|
3703
3712
|
}
|
|
3713
|
+
|
|
3714
|
+
// src/components/pricing/catalog.ts
|
|
3715
|
+
var PRICING_INTERVALS = [
|
|
3716
|
+
"weekly",
|
|
3717
|
+
"monthly",
|
|
3718
|
+
"quarterly",
|
|
3719
|
+
"semiannual",
|
|
3720
|
+
"yearly"
|
|
3721
|
+
];
|
|
3722
|
+
var INTERVAL_LABELS2 = {
|
|
3723
|
+
weekly: "Weekly",
|
|
3724
|
+
monthly: "Monthly",
|
|
3725
|
+
quarterly: "Quarterly",
|
|
3726
|
+
semiannual: "Every 6 months",
|
|
3727
|
+
yearly: "Annual"
|
|
3728
|
+
};
|
|
3729
|
+
var BILLED_WORDS = {
|
|
3730
|
+
weekly: "weekly",
|
|
3731
|
+
monthly: "monthly",
|
|
3732
|
+
quarterly: "quarterly",
|
|
3733
|
+
semiannual: "every 6 months",
|
|
3734
|
+
yearly: "yearly"
|
|
3735
|
+
};
|
|
3736
|
+
var PERIODS_PER_YEAR = {
|
|
3737
|
+
weekly: 52n,
|
|
3738
|
+
monthly: 12n,
|
|
3739
|
+
quarterly: 4n,
|
|
3740
|
+
semiannual: 2n,
|
|
3741
|
+
yearly: 1n
|
|
3742
|
+
};
|
|
3743
|
+
function isPricingInterval(value) {
|
|
3744
|
+
return typeof value === "string" && PRICING_INTERVALS.includes(value);
|
|
3745
|
+
}
|
|
3746
|
+
function intervalRank(interval) {
|
|
3747
|
+
return PRICING_INTERVALS.indexOf(interval);
|
|
3748
|
+
}
|
|
3749
|
+
function normalizePricingInterval(value) {
|
|
3750
|
+
if (value === "annual") return "yearly";
|
|
3751
|
+
return isPricingInterval(value) ? value : null;
|
|
3752
|
+
}
|
|
3753
|
+
function hasPosition(value) {
|
|
3754
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3755
|
+
}
|
|
3756
|
+
function comparePositions(a, b) {
|
|
3757
|
+
const aSet = hasPosition(a);
|
|
3758
|
+
const bSet = hasPosition(b);
|
|
3759
|
+
if (aSet && bSet) return a - b;
|
|
3760
|
+
if (aSet) return -1;
|
|
3761
|
+
if (bSet) return 1;
|
|
3762
|
+
return 0;
|
|
3763
|
+
}
|
|
3764
|
+
var DEFAULT_LOCALE = "en-US";
|
|
3765
|
+
function resolvePricingLocale(locale) {
|
|
3766
|
+
if (typeof locale !== "string" || locale.trim() === "") return DEFAULT_LOCALE;
|
|
3767
|
+
try {
|
|
3768
|
+
Intl.getCanonicalLocales(locale);
|
|
3769
|
+
return locale;
|
|
3770
|
+
} catch {
|
|
3771
|
+
return DEFAULT_LOCALE;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
function isLive(plan) {
|
|
3775
|
+
return plan.pending_deletion !== true;
|
|
3776
|
+
}
|
|
3777
|
+
function hasFamilies(catalog) {
|
|
3778
|
+
return Array.isArray(catalog.families);
|
|
3779
|
+
}
|
|
3780
|
+
function getPricingGroups(catalog) {
|
|
3781
|
+
const plans = catalog.plans ?? [];
|
|
3782
|
+
if (!hasFamilies(catalog)) {
|
|
3783
|
+
return plans.filter(isLive).map((plan) => ({ key: plan.code, family: null, plans: [plan] }));
|
|
3784
|
+
}
|
|
3785
|
+
const byCode = /* @__PURE__ */ new Map();
|
|
3786
|
+
for (const plan of plans) {
|
|
3787
|
+
if (isLive(plan) && !byCode.has(plan.code)) byCode.set(plan.code, plan);
|
|
3788
|
+
}
|
|
3789
|
+
return catalog.families.map((family, index) => ({ family, index })).filter(({ family }) => family.show_on_pricing !== false).sort(
|
|
3790
|
+
(a, b) => comparePositions(a.family.pricing_position, b.family.pricing_position) || a.index - b.index
|
|
3791
|
+
).map(({ family }) => {
|
|
3792
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3793
|
+
const familyPlans = [];
|
|
3794
|
+
for (const code of family.plan_codes ?? []) {
|
|
3795
|
+
const plan = byCode.get(code);
|
|
3796
|
+
if (plan && !seen.has(code)) {
|
|
3797
|
+
seen.add(code);
|
|
3798
|
+
familyPlans.push(plan);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return { key: family.code, family, plans: familyPlans };
|
|
3802
|
+
}).filter((group) => group.plans.length > 0);
|
|
3803
|
+
}
|
|
3804
|
+
function toMinor(value) {
|
|
3805
|
+
const raw = String(value);
|
|
3806
|
+
if (!/^-?\d+$/.test(raw)) return null;
|
|
3807
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) return null;
|
|
3808
|
+
return BigInt(raw);
|
|
3809
|
+
}
|
|
3810
|
+
function divideRounded(numerator, denominator) {
|
|
3811
|
+
if (numerator < 0n) return -divideRounded(-numerator, denominator);
|
|
3812
|
+
return (2n * numerator + denominator) / (2n * denominator);
|
|
3813
|
+
}
|
|
3814
|
+
function perMonthMinor(amount, interval) {
|
|
3815
|
+
return divideRounded(amount * PERIODS_PER_YEAR[interval], 12n);
|
|
3816
|
+
}
|
|
3817
|
+
function fmt(amount, currency, locale) {
|
|
3818
|
+
return formatMinorAmount(String(amount), currency, locale, {
|
|
3819
|
+
trimZeroFraction: true
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
function savingsAgainstMonthly(plan, monthly) {
|
|
3823
|
+
if (!monthly || monthly === plan) return null;
|
|
3824
|
+
if (!isPricingInterval(plan.interval) || plan.interval === "monthly") return null;
|
|
3825
|
+
if (monthly.amount_currency !== plan.amount_currency) return null;
|
|
3826
|
+
const monthlyAmount = toMinor(monthly.amount_cents);
|
|
3827
|
+
const amount = toMinor(plan.amount_cents);
|
|
3828
|
+
if (monthlyAmount === null || amount === null) return null;
|
|
3829
|
+
const base = monthlyAmount * 12n;
|
|
3830
|
+
const cost = amount * PERIODS_PER_YEAR[plan.interval];
|
|
3831
|
+
if (base <= 0n || cost >= base) return null;
|
|
3832
|
+
const percent = Number(divideRounded(100n * (base - cost), base));
|
|
3833
|
+
return percent > 0 ? percent : null;
|
|
3834
|
+
}
|
|
3835
|
+
function monthlyPlanOf(group) {
|
|
3836
|
+
return group.family ? group.plans.find((plan) => plan.interval === "monthly") : void 0;
|
|
3837
|
+
}
|
|
3838
|
+
function getAvailableIntervals(catalog) {
|
|
3839
|
+
const present = /* @__PURE__ */ new Set();
|
|
3840
|
+
for (const group of getPricingGroups(catalog)) {
|
|
3841
|
+
for (const plan of group.plans) present.add(plan.interval);
|
|
3842
|
+
}
|
|
3843
|
+
return PRICING_INTERVALS.filter((interval) => present.has(interval));
|
|
3844
|
+
}
|
|
3845
|
+
function getPricingIntervalOptions(catalog) {
|
|
3846
|
+
const groups = getPricingGroups(catalog);
|
|
3847
|
+
return getAvailableIntervals(catalog).map((interval) => {
|
|
3848
|
+
let best = null;
|
|
3849
|
+
if (interval !== "monthly") {
|
|
3850
|
+
for (const group of groups) {
|
|
3851
|
+
const plan = group.plans.find((p) => p.interval === interval);
|
|
3852
|
+
const saving = plan ? savingsAgainstMonthly(plan, monthlyPlanOf(group)) : null;
|
|
3853
|
+
if (saving !== null && (best === null || saving > best)) best = saving;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
return { interval, label: INTERVAL_LABELS2[interval], savingsPercent: best };
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
function getDefaultInterval(catalog, preferred) {
|
|
3860
|
+
const available = getAvailableIntervals(catalog);
|
|
3861
|
+
const wanted = normalizePricingInterval(preferred);
|
|
3862
|
+
if (wanted && available.includes(wanted)) return wanted;
|
|
3863
|
+
if (available.includes("monthly")) return "monthly";
|
|
3864
|
+
return available[0] ?? null;
|
|
3865
|
+
}
|
|
3866
|
+
function pickPlan(plans, interval) {
|
|
3867
|
+
if (interval === null) return plans[0];
|
|
3868
|
+
const exact = plans.find((plan) => plan.interval === interval);
|
|
3869
|
+
if (exact) return exact;
|
|
3870
|
+
const target = intervalRank(interval);
|
|
3871
|
+
let best = plans[0];
|
|
3872
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
3873
|
+
for (const plan of plans) {
|
|
3874
|
+
const rank = intervalRank(plan.interval);
|
|
3875
|
+
if (rank < 0) continue;
|
|
3876
|
+
const score = Math.abs(rank - target) * 2 + (rank > target ? 1 : 0);
|
|
3877
|
+
if (score < bestScore) {
|
|
3878
|
+
best = plan;
|
|
3879
|
+
bestScore = score;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
return best;
|
|
3883
|
+
}
|
|
3884
|
+
function isTrueBoolean(privilege) {
|
|
3885
|
+
const { value } = privilege;
|
|
3886
|
+
if (value === true) return true;
|
|
3887
|
+
return privilege.value_type === "boolean" && typeof value === "string" && value.toLowerCase() === "true";
|
|
3888
|
+
}
|
|
3889
|
+
function formatPrivilegeValue(privilege, numberFormat) {
|
|
3890
|
+
const { value } = privilege;
|
|
3891
|
+
if (value === null || value === void 0) return null;
|
|
3892
|
+
if (isTrueBoolean(privilege)) return null;
|
|
3893
|
+
if (value === false) return "No";
|
|
3894
|
+
if (typeof value === "number") {
|
|
3895
|
+
return Number.isFinite(value) ? numberFormat.format(value) : null;
|
|
3896
|
+
}
|
|
3897
|
+
const text2 = String(value).trim();
|
|
3898
|
+
if (text2 === "") return null;
|
|
3899
|
+
if (privilege.value_type === "boolean" && text2.toLowerCase() === "false") {
|
|
3900
|
+
return "No";
|
|
3901
|
+
}
|
|
3902
|
+
if (privilege.value_type === "integer" && /^-?\d+$/.test(text2)) {
|
|
3903
|
+
const parsed = Number(text2);
|
|
3904
|
+
if (Number.isSafeInteger(parsed)) return numberFormat.format(parsed);
|
|
3905
|
+
}
|
|
3906
|
+
return text2;
|
|
3907
|
+
}
|
|
3908
|
+
function featureLabel(name, entitlement, numberFormat) {
|
|
3909
|
+
const values = (entitlement?.privileges ?? []).map((privilege) => ({
|
|
3910
|
+
privilege,
|
|
3911
|
+
text: formatPrivilegeValue(privilege, numberFormat)
|
|
3912
|
+
})).filter(
|
|
3913
|
+
(entry) => entry.text !== null
|
|
3914
|
+
);
|
|
3915
|
+
if (values.length === 1) return `${name}: ${values[0].text}`;
|
|
3916
|
+
if (values.length > 1) {
|
|
3917
|
+
const parts = values.map(({ privilege, text: text2 }) => {
|
|
3918
|
+
const privilegeName = privilege.name?.trim() || privilege.code;
|
|
3919
|
+
return `${privilegeName}: ${text2}`;
|
|
3920
|
+
});
|
|
3921
|
+
return `${name} (${parts.join(", ")})`;
|
|
3922
|
+
}
|
|
3923
|
+
const limit = entitlement?.usage_limit;
|
|
3924
|
+
if (typeof limit === "number" && Number.isFinite(limit)) {
|
|
3925
|
+
return `${name}: ${numberFormat.format(limit)}`;
|
|
3926
|
+
}
|
|
3927
|
+
return name;
|
|
3928
|
+
}
|
|
3929
|
+
function trimmed(value) {
|
|
3930
|
+
return typeof value === "string" ? value.trim() : "";
|
|
3931
|
+
}
|
|
3932
|
+
function familyFeatures(family, plan, locale) {
|
|
3933
|
+
const numberFormat = new Intl.NumberFormat(locale);
|
|
3934
|
+
const entitlements = /* @__PURE__ */ new Map();
|
|
3935
|
+
for (const entitlement of plan.entitlements ?? []) {
|
|
3936
|
+
if (!entitlements.has(entitlement.code)) {
|
|
3937
|
+
entitlements.set(entitlement.code, entitlement);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return (family.features ?? []).filter((feature) => feature.show_on_pricing !== false).map((feature, index) => {
|
|
3941
|
+
const entitlement = entitlements.get(feature.code);
|
|
3942
|
+
const name = trimmed(feature.name) || trimmed(entitlement?.name) || feature.code;
|
|
3943
|
+
return { feature, entitlement, index, name };
|
|
3944
|
+
}).sort(
|
|
3945
|
+
(a, b) => comparePositions(a.feature.display_position, b.feature.display_position) || a.name.localeCompare(b.name, locale) || a.index - b.index
|
|
3946
|
+
).map(({ feature, entitlement, name }) => {
|
|
3947
|
+
const label = featureLabel(name, entitlement, numberFormat);
|
|
3948
|
+
const description = trimmed(feature.description);
|
|
3949
|
+
return feature.show_description === true && description ? { label, description } : { label };
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
function planCardFeatures(plan) {
|
|
3953
|
+
return planFeatures(plan).map((label) => ({ label }));
|
|
3954
|
+
}
|
|
3955
|
+
function normalizeFeatures(items) {
|
|
3956
|
+
return items.map((item) => typeof item === "string" ? { label: item } : item).filter(
|
|
3957
|
+
(item) => Boolean(item) && typeof item.label === "string"
|
|
3958
|
+
);
|
|
3959
|
+
}
|
|
3960
|
+
function overrideFeatures(overrides, group, plan, cardIndex) {
|
|
3961
|
+
if (!overrides) return null;
|
|
3962
|
+
if (Array.isArray(overrides)) {
|
|
3963
|
+
const entry = overrides[cardIndex];
|
|
3964
|
+
return entry ? normalizeFeatures(entry) : null;
|
|
3965
|
+
}
|
|
3966
|
+
const keys = [group.key, plan.code, ...group.plans.map((p) => p.code)];
|
|
3967
|
+
for (const key of keys) {
|
|
3968
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) {
|
|
3969
|
+
return normalizeFeatures(overrides[key] ?? []);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
function yearlyMinor(plan) {
|
|
3975
|
+
const amount = toMinor(plan.amount_cents);
|
|
3976
|
+
if (amount === null || !isPricingInterval(plan.interval)) return null;
|
|
3977
|
+
return amount * PERIODS_PER_YEAR[plan.interval];
|
|
3978
|
+
}
|
|
3979
|
+
function planPriceStrings(plan, locale) {
|
|
3980
|
+
const amount = toMinor(plan.amount_cents);
|
|
3981
|
+
return {
|
|
3982
|
+
price: fmt(amount ?? plan.amount_cents, plan.amount_currency, locale),
|
|
3983
|
+
pricePeriod: plan.interval ? `/${intervalLabel(plan.interval)}` : "",
|
|
3984
|
+
billing: "",
|
|
3985
|
+
savingsPercent: null
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
function priceStrings(plan, monthly, locale) {
|
|
3989
|
+
const amount = toMinor(plan.amount_cents);
|
|
3990
|
+
const currency = plan.amount_currency;
|
|
3991
|
+
if (!isPricingInterval(plan.interval) || amount === null) {
|
|
3992
|
+
return {
|
|
3993
|
+
price: fmt(amount ?? plan.amount_cents, currency, locale),
|
|
3994
|
+
pricePeriod: "",
|
|
3995
|
+
billing: plan.interval ? `Billed ${plan.interval}` : "",
|
|
3996
|
+
savingsPercent: null
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
const price = fmt(perMonthMinor(amount, plan.interval), currency, locale);
|
|
4000
|
+
if (plan.interval === "monthly") {
|
|
4001
|
+
return { price, pricePeriod: "/mo", billing: "Billed monthly", savingsPercent: null };
|
|
4002
|
+
}
|
|
4003
|
+
const savingsPercent = savingsAgainstMonthly(plan, monthly);
|
|
4004
|
+
const billed = `Billed ${fmt(amount, currency, locale)} ${BILLED_WORDS[plan.interval]}`;
|
|
4005
|
+
return {
|
|
4006
|
+
price,
|
|
4007
|
+
pricePeriod: "/mo",
|
|
4008
|
+
billing: savingsPercent === null ? billed : `${billed} \xB7 save ${savingsPercent}%`,
|
|
4009
|
+
savingsPercent
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
function getPricingCards(catalog, options = {}) {
|
|
4013
|
+
const locale = resolvePricingLocale(options.locale);
|
|
4014
|
+
const families = hasFamilies(catalog);
|
|
4015
|
+
const requested = normalizePricingInterval(options.interval ?? null);
|
|
4016
|
+
const interval = requested ?? getDefaultInterval(catalog);
|
|
4017
|
+
const contactSales = new Set(options.contactSales ?? []);
|
|
4018
|
+
const isContactSales = (group) => contactSales.has(group.key) || group.plans.some((plan) => contactSales.has(plan.code));
|
|
4019
|
+
let groups = getPricingGroups(catalog);
|
|
4020
|
+
if (!families) {
|
|
4021
|
+
if (options.sort) {
|
|
4022
|
+
const sort = options.sort;
|
|
4023
|
+
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);
|
|
4024
|
+
} else {
|
|
4025
|
+
const sales = groups.filter(isContactSales);
|
|
4026
|
+
const byPlan = new Map(groups.map((group) => [group.plans[0], group]));
|
|
4027
|
+
groups = [
|
|
4028
|
+
...sortPlansByPrice(groups.filter((group) => !isContactSales(group)).map((group) => group.plans[0])).map((plan) => byPlan.get(plan)),
|
|
4029
|
+
...sales
|
|
4030
|
+
];
|
|
4031
|
+
}
|
|
4032
|
+
if (requested) {
|
|
4033
|
+
groups = groups.filter(
|
|
4034
|
+
(group) => group.plans[0].interval === requested || isContactSales(group)
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
const highlight = options.highlight ?? null;
|
|
4039
|
+
const highlightedKey = highlight !== null ? groups.find(
|
|
4040
|
+
(group) => group.key === highlight || group.plans.some((plan) => plan.code === highlight)
|
|
4041
|
+
)?.key : groups.find((group) => group.family?.pricing_highlighted === true)?.key;
|
|
4042
|
+
const badge = (family) => options.badge !== void 0 ? options.badge === null ? null : trimmed(options.badge) || null : trimmed(family?.badge_text) || "Most Popular";
|
|
4043
|
+
return groups.map((group, cardIndex) => {
|
|
4044
|
+
const plan = pickPlan(group.plans, interval);
|
|
4045
|
+
const family = group.family;
|
|
4046
|
+
const name = trimmed(family?.name) || plan.name;
|
|
4047
|
+
const description = trimmed(family?.description) || trimmed(plan.description) || null;
|
|
4048
|
+
const highlighted = highlightedKey !== void 0 && highlightedKey === group.key;
|
|
4049
|
+
const features = overrideFeatures(options.features, group, plan, cardIndex) ?? (family ? familyFeatures(family, plan, locale) : planCardFeatures(plan));
|
|
4050
|
+
return {
|
|
4051
|
+
key: group.key,
|
|
4052
|
+
family,
|
|
4053
|
+
plans: group.plans,
|
|
4054
|
+
plan,
|
|
4055
|
+
isFallbackInterval: families && interval !== null && plan.interval !== interval,
|
|
4056
|
+
name,
|
|
4057
|
+
description,
|
|
4058
|
+
...family ? priceStrings(plan, monthlyPlanOf(group), locale) : planPriceStrings(plan, locale),
|
|
4059
|
+
features,
|
|
4060
|
+
highlighted,
|
|
4061
|
+
badge: highlighted ? badge(family) : null,
|
|
4062
|
+
cta: trimmed(family?.cta_text) || `Get ${name}`,
|
|
4063
|
+
enterprise: isContactSales(group)
|
|
4064
|
+
};
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
3704
4067
|
function PanelPlanCard({
|
|
3705
4068
|
frame,
|
|
3706
4069
|
el,
|
|
@@ -3708,6 +4071,7 @@ function PanelPlanCard({
|
|
|
3708
4071
|
description,
|
|
3709
4072
|
price,
|
|
3710
4073
|
interval,
|
|
4074
|
+
priceNote,
|
|
3711
4075
|
features,
|
|
3712
4076
|
isCurrent,
|
|
3713
4077
|
isHighlighted,
|
|
@@ -3772,6 +4136,7 @@ function PanelPlanCard({
|
|
|
3772
4136
|
interval
|
|
3773
4137
|
] })
|
|
3774
4138
|
] }),
|
|
4139
|
+
priceNote && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "0.25rem 0 0" }, children: priceNote }),
|
|
3775
4140
|
description && /* @__PURE__ */ jsxRuntime.jsx("p", { ...el("planCard__description", { ...text.secondary, color: muted5, margin: "0.25rem 0 0" }), children: description })
|
|
3776
4141
|
] }),
|
|
3777
4142
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3788,7 +4153,13 @@ function PanelPlanCard({
|
|
|
3788
4153
|
...text.secondary,
|
|
3789
4154
|
color: muted5
|
|
3790
4155
|
}),
|
|
3791
|
-
children: features.map((feature) =>
|
|
4156
|
+
children: features.map((feature, index) => {
|
|
4157
|
+
const item = typeof feature === "string" ? { label: feature } : feature;
|
|
4158
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { ...el("planCard__feature"), children: [
|
|
4159
|
+
item.label,
|
|
4160
|
+
item.description && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { display: "block", ...text.meta, color: muted5 }, children: item.description })
|
|
4161
|
+
] }, `${index}:${item.label}`);
|
|
4162
|
+
})
|
|
3792
4163
|
}
|
|
3793
4164
|
),
|
|
3794
4165
|
children
|
|
@@ -3797,59 +4168,66 @@ function PanelPlanCard({
|
|
|
3797
4168
|
}
|
|
3798
4169
|
);
|
|
3799
4170
|
}
|
|
3800
|
-
function featureLines(override, plan, index) {
|
|
3801
|
-
const given = Array.isArray(override) ? override[index] : override?.[plan.code];
|
|
3802
|
-
return given ?? planFeatures(plan);
|
|
3803
|
-
}
|
|
3804
|
-
var SYMBOLS = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5" };
|
|
3805
|
-
function fmtPrice(cents, cur) {
|
|
3806
|
-
const s = SYMBOLS[cur] ?? cur + " ";
|
|
3807
|
-
const val = cents % 100 === 0 ? String(cents / 100) : (cents / 100).toFixed(2);
|
|
3808
|
-
return `${s}${val}`;
|
|
3809
|
-
}
|
|
3810
|
-
var C = {
|
|
3811
|
-
cardBg: T.card,
|
|
3812
|
-
hl: T.primary,
|
|
3813
|
-
border: T.border,
|
|
3814
|
-
radius: T.radius,
|
|
3815
|
-
text: T.foreground,
|
|
3816
|
-
muted: T.mutedForeground,
|
|
3817
|
-
mutedBg: T.muted};
|
|
3818
4171
|
function Check() {
|
|
3819
4172
|
return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.5 8.5L6.5 11.5L12.5 4.5", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
3820
4173
|
}
|
|
3821
|
-
function
|
|
4174
|
+
function IntervalSwitch({
|
|
4175
|
+
options,
|
|
4176
|
+
selected,
|
|
4177
|
+
onChange,
|
|
4178
|
+
annualDiscountLabel,
|
|
4179
|
+
el
|
|
4180
|
+
}) {
|
|
3822
4181
|
const pill = {
|
|
3823
4182
|
display: "inline-flex",
|
|
4183
|
+
flexWrap: "wrap",
|
|
4184
|
+
justifyContent: "center",
|
|
3824
4185
|
borderRadius: "9999px",
|
|
3825
4186
|
padding: "4px",
|
|
3826
|
-
background:
|
|
4187
|
+
background: T.muted,
|
|
3827
4188
|
gap: "2px"
|
|
3828
4189
|
};
|
|
3829
4190
|
const tab = (active) => ({
|
|
3830
4191
|
padding: "8px 20px",
|
|
3831
4192
|
borderRadius: "9999px",
|
|
3832
4193
|
border: "none",
|
|
3833
|
-
background: active ?
|
|
4194
|
+
background: active ? T.card : "transparent",
|
|
3834
4195
|
...text.action,
|
|
3835
|
-
color: active ?
|
|
4196
|
+
color: active ? T.foreground : T.mutedForeground,
|
|
3836
4197
|
cursor: "pointer",
|
|
3837
4198
|
boxShadow: active ? T.shadow : "none",
|
|
3838
4199
|
transition: "all 0.15s ease"
|
|
3839
4200
|
});
|
|
3840
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
4201
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", justifyContent: "center", marginBottom: "2.5rem" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { role: "group", "aria-label": "Billing interval", ...el("pricingTable__toggle", pill), children: options.map((option) => {
|
|
4202
|
+
const active = option.interval === selected;
|
|
4203
|
+
const note = option.interval === "yearly" && annualDiscountLabel ? annualDiscountLabel : option.interval !== "monthly" && option.savingsPercent !== null ? `Save ${option.savingsPercent}%` : null;
|
|
4204
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4205
|
+
"button",
|
|
4206
|
+
{
|
|
4207
|
+
type: "button",
|
|
4208
|
+
"data-interval": option.interval,
|
|
4209
|
+
onClick: () => onChange(option.interval),
|
|
4210
|
+
...el("pricingTable__toggleButton", tab(active)),
|
|
4211
|
+
"aria-pressed": active,
|
|
4212
|
+
"aria-label": note ? `${option.label}, ${note.replace(/^Save /, "save ")}` : void 0,
|
|
4213
|
+
children: [
|
|
4214
|
+
option.label,
|
|
4215
|
+
note && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
|
|
4216
|
+
marginLeft: "6px",
|
|
4217
|
+
...text.meta,
|
|
4218
|
+
fontWeight: 600,
|
|
4219
|
+
color: active ? T.primary : T.mutedForeground
|
|
4220
|
+
}, children: note })
|
|
4221
|
+
]
|
|
4222
|
+
},
|
|
4223
|
+
option.interval
|
|
4224
|
+
);
|
|
4225
|
+
}) }) });
|
|
3844
4226
|
}
|
|
3845
4227
|
function Card({
|
|
3846
|
-
|
|
3847
|
-
features,
|
|
4228
|
+
card: card2,
|
|
3848
4229
|
isCurrent,
|
|
3849
|
-
isHighlighted,
|
|
3850
4230
|
filled: filledStyle,
|
|
3851
|
-
highlightLabel,
|
|
3852
|
-
isEnt,
|
|
3853
4231
|
enterpriseEmail,
|
|
3854
4232
|
onSelect,
|
|
3855
4233
|
el,
|
|
@@ -3857,12 +4235,14 @@ function Card({
|
|
|
3857
4235
|
isUpgrade
|
|
3858
4236
|
}) {
|
|
3859
4237
|
const [hover, setHover] = React2.useState(false);
|
|
4238
|
+
const { plan, features } = card2;
|
|
4239
|
+
const isHighlighted = card2.highlighted;
|
|
4240
|
+
const isEnt = card2.enterprise;
|
|
3860
4241
|
const filled = isHighlighted && filledStyle;
|
|
3861
|
-
const text2 = filled ? T.highlightForeground :
|
|
3862
|
-
const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` :
|
|
3863
|
-
const accent = filled ? T.highlightForeground :
|
|
3864
|
-
const badgeLabel = isCurrent ? "Current Plan" :
|
|
3865
|
-
const interval = `/${intervalLabel(plan.interval)}`;
|
|
4242
|
+
const text2 = filled ? T.highlightForeground : T.foreground;
|
|
4243
|
+
const muted5 = filled ? `color-mix(in srgb, ${T.highlightForeground} 75%, ${T.highlight})` : T.mutedForeground;
|
|
4244
|
+
const accent = filled ? T.highlightForeground : T.primary;
|
|
4245
|
+
const badgeLabel = isCurrent ? "Current Plan" : card2.badge;
|
|
3866
4246
|
const handleClick = () => {
|
|
3867
4247
|
if (isCurrent) return;
|
|
3868
4248
|
if (isEnt && enterpriseEmail) {
|
|
@@ -3871,6 +4251,7 @@ function Card({
|
|
|
3871
4251
|
}
|
|
3872
4252
|
onSelect?.();
|
|
3873
4253
|
};
|
|
4254
|
+
const ctaText = isCurrent ? "Current Plan" : card2.family?.cta_text?.trim() ? card2.cta : isEnt ? "Contact Sales" : card2.cta;
|
|
3874
4255
|
const emphasised = isUpgrade ?? isHighlighted;
|
|
3875
4256
|
const cta = onSelect && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3876
4257
|
CTAButton,
|
|
@@ -3879,24 +4260,32 @@ function Card({
|
|
|
3879
4260
|
disabled: isCurrent,
|
|
3880
4261
|
variant: isCurrent ? "current" : isEnt ? "outline" : !emphasised ? "secondary" : filled ? "inverse" : isHighlighted ? "highlight" : "primary",
|
|
3881
4262
|
el,
|
|
3882
|
-
children:
|
|
4263
|
+
children: ctaText
|
|
3883
4264
|
}
|
|
3884
4265
|
);
|
|
4266
|
+
const dataAttributes = {
|
|
4267
|
+
"data-card-key": card2.key,
|
|
4268
|
+
"data-family-code": card2.family?.code,
|
|
4269
|
+
"data-plan-code": plan.code,
|
|
4270
|
+
"data-interval": plan.interval,
|
|
4271
|
+
"data-highlighted": isHighlighted ? "true" : void 0
|
|
4272
|
+
};
|
|
3885
4273
|
if (panel) {
|
|
3886
4274
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3887
4275
|
PanelPlanCard,
|
|
3888
4276
|
{
|
|
3889
|
-
frame: (style) => el("planCard", style),
|
|
4277
|
+
frame: (style) => ({ ...el("planCard", style), ...dataAttributes }),
|
|
3890
4278
|
el,
|
|
3891
|
-
name:
|
|
3892
|
-
description:
|
|
3893
|
-
price: isEnt ? "Custom" :
|
|
3894
|
-
interval: isEnt ? void 0 :
|
|
3895
|
-
|
|
4279
|
+
name: card2.name,
|
|
4280
|
+
description: card2.description,
|
|
4281
|
+
price: isEnt ? "Custom" : card2.price,
|
|
4282
|
+
interval: isEnt || !card2.pricePeriod ? void 0 : card2.pricePeriod.replace(/^\//, ""),
|
|
4283
|
+
priceNote: isEnt ? null : card2.billing,
|
|
4284
|
+
features,
|
|
3896
4285
|
isCurrent,
|
|
3897
4286
|
isHighlighted,
|
|
3898
4287
|
filled: filledStyle,
|
|
3899
|
-
highlightLabel,
|
|
4288
|
+
highlightLabel: card2.badge,
|
|
3900
4289
|
children: cta
|
|
3901
4290
|
}
|
|
3902
4291
|
);
|
|
@@ -3904,17 +4293,18 @@ function Card({
|
|
|
3904
4293
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3905
4294
|
"div",
|
|
3906
4295
|
{
|
|
4296
|
+
...dataAttributes,
|
|
3907
4297
|
onMouseEnter: () => setHover(true),
|
|
3908
4298
|
onMouseLeave: () => setHover(false),
|
|
3909
4299
|
...el("planCard", {
|
|
3910
4300
|
position: "relative",
|
|
3911
4301
|
display: "flex",
|
|
3912
4302
|
flexDirection: "column",
|
|
3913
|
-
background: filled ? T.highlight :
|
|
4303
|
+
background: filled ? T.highlight : T.card,
|
|
3914
4304
|
color: text2,
|
|
3915
|
-
border: border(isHighlighted ? T.highlight :
|
|
4305
|
+
border: border(isHighlighted ? T.highlight : T.border),
|
|
3916
4306
|
borderTopWidth: isHighlighted && !filled ? `calc(${T.borderWidth} + 2px)` : T.borderWidth,
|
|
3917
|
-
borderRadius:
|
|
4307
|
+
borderRadius: T.radius,
|
|
3918
4308
|
padding: "2rem",
|
|
3919
4309
|
transition: "box-shadow 0.2s ease, transform 0.2s ease",
|
|
3920
4310
|
boxShadow: T.shadow,
|
|
@@ -3939,7 +4329,7 @@ function Card({
|
|
|
3939
4329
|
top: "0",
|
|
3940
4330
|
left: "50%",
|
|
3941
4331
|
transform: "translate(-50%, -50%)",
|
|
3942
|
-
background:
|
|
4332
|
+
background: T.muted,
|
|
3943
4333
|
...text.label,
|
|
3944
4334
|
padding: "4px 12px",
|
|
3945
4335
|
borderRadius: "9999px",
|
|
@@ -3950,18 +4340,21 @@ function Card({
|
|
|
3950
4340
|
...text.heading,
|
|
3951
4341
|
color: text2,
|
|
3952
4342
|
margin: 0
|
|
3953
|
-
}), children:
|
|
3954
|
-
|
|
4343
|
+
}), children: card2.name }),
|
|
4344
|
+
card2.description && /* @__PURE__ */ jsxRuntime.jsx("p", { ...el("planCard__description", {
|
|
3955
4345
|
...text.secondary,
|
|
3956
4346
|
color: muted5,
|
|
3957
4347
|
margin: "6px 0 0"
|
|
3958
|
-
}), children:
|
|
4348
|
+
}), children: card2.description })
|
|
3959
4349
|
] }),
|
|
3960
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3961
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3962
|
-
|
|
4350
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ...el("planCard__price", { marginBottom: "1.5rem" }), children: isEnt ? /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...text.metric, color: text2 }, children: "Custom" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4351
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: "2px" }, children: [
|
|
4352
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-testid": "pricing-price", style: { ...text.metric, color: text2 }, children: card2.price }),
|
|
4353
|
+
card2.pricePeriod && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...text.meta, color: muted5, marginLeft: "4px" }, children: card2.pricePeriod })
|
|
4354
|
+
] }),
|
|
4355
|
+
card2.billing && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-testid": "pricing-billing", style: { ...text.meta, color: muted5, margin: "8px 0 0" }, children: card2.billing })
|
|
3963
4356
|
] }) }),
|
|
3964
|
-
features
|
|
4357
|
+
features.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { ...el("planCard__features", {
|
|
3965
4358
|
listStyle: "none",
|
|
3966
4359
|
padding: 0,
|
|
3967
4360
|
margin: "0 0 2rem",
|
|
@@ -3969,7 +4362,7 @@ function Card({
|
|
|
3969
4362
|
flexDirection: "column",
|
|
3970
4363
|
gap: "0.75rem",
|
|
3971
4364
|
flex: 1
|
|
3972
|
-
}), children: features.map((feat) => /* @__PURE__ */ jsxRuntime.jsxs("li", { ...el("planCard__feature", {
|
|
4365
|
+
}), children: features.map((feat, index) => /* @__PURE__ */ jsxRuntime.jsxs("li", { ...el("planCard__feature", {
|
|
3973
4366
|
display: "flex",
|
|
3974
4367
|
alignItems: "flex-start",
|
|
3975
4368
|
gap: "10px",
|
|
@@ -3977,9 +4370,12 @@ function Card({
|
|
|
3977
4370
|
color: text2
|
|
3978
4371
|
}), children: [
|
|
3979
4372
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: accent, marginTop: "2px" }, children: /* @__PURE__ */ jsxRuntime.jsx(Check, {}) }),
|
|
3980
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3981
|
-
|
|
3982
|
-
|
|
4373
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "flex", flexDirection: "column" }, children: [
|
|
4374
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: feat.label }),
|
|
4375
|
+
feat.description && /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...text.meta, color: muted5, marginTop: "2px" }, children: feat.description })
|
|
4376
|
+
] })
|
|
4377
|
+
] }, `${index}:${feat.label}`)) }),
|
|
4378
|
+
features.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
|
|
3983
4379
|
cta
|
|
3984
4380
|
]
|
|
3985
4381
|
}
|
|
@@ -3993,7 +4389,7 @@ function CTAButton({
|
|
|
3993
4389
|
el
|
|
3994
4390
|
}) {
|
|
3995
4391
|
const [hover, setHover] = React2.useState(false);
|
|
3996
|
-
const look = variant === "current" ? { ...buttonStyle("secondary"), background:
|
|
4392
|
+
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);
|
|
3997
4393
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3998
4394
|
"button",
|
|
3999
4395
|
{
|
|
@@ -4018,9 +4414,9 @@ function Skeleton({ rootProps }) {
|
|
|
4018
4414
|
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
4019
4415
|
gap: "1.5rem"
|
|
4020
4416
|
}, children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
|
|
4021
|
-
background:
|
|
4417
|
+
background: T.card,
|
|
4022
4418
|
border: border(),
|
|
4023
|
-
borderRadius:
|
|
4419
|
+
borderRadius: T.radius,
|
|
4024
4420
|
padding: "2rem",
|
|
4025
4421
|
height: "380px"
|
|
4026
4422
|
}, children: [
|
|
@@ -4034,6 +4430,7 @@ function Skeleton({ rootProps }) {
|
|
|
4034
4430
|
function PricingTable({
|
|
4035
4431
|
currentPlanCode,
|
|
4036
4432
|
plans: propPlans,
|
|
4433
|
+
families: propFamilies,
|
|
4037
4434
|
features,
|
|
4038
4435
|
sort,
|
|
4039
4436
|
onSelect,
|
|
@@ -4042,36 +4439,40 @@ function PricingTable({
|
|
|
4042
4439
|
onCheckoutResult,
|
|
4043
4440
|
onCheckoutError,
|
|
4044
4441
|
showToggle = true,
|
|
4442
|
+
defaultInterval,
|
|
4045
4443
|
highlightPlan,
|
|
4046
4444
|
highlightStyle = "outline",
|
|
4047
|
-
highlightLabel
|
|
4445
|
+
highlightLabel,
|
|
4048
4446
|
className,
|
|
4049
4447
|
contactSalesPlans,
|
|
4050
4448
|
enterpriseEmail,
|
|
4449
|
+
annualDiscountLabel,
|
|
4450
|
+
locale: localeProp,
|
|
4051
4451
|
appearance
|
|
4052
4452
|
}) {
|
|
4053
4453
|
const { root, el, planCardLayout } = useAppearance(appearance);
|
|
4054
|
-
const
|
|
4454
|
+
const locale = resolvePricingLocale(localeProp);
|
|
4055
4455
|
const billing = useOptionalBillingContext();
|
|
4056
4456
|
const client = billing?.client;
|
|
4057
|
-
const
|
|
4457
|
+
const fetched = reactQuery.useQuery(
|
|
4058
4458
|
{ ...catalogQuery(client), enabled: !propPlans && Boolean(client) },
|
|
4059
4459
|
billing?.queryClient ?? idleQueryClient
|
|
4060
4460
|
);
|
|
4061
|
-
const
|
|
4062
|
-
const loading = !propPlans && Boolean(client) &&
|
|
4063
|
-
const loadError = propPlans ? null : !client ? "BillingProvider with a publishableKey is required to load plans" :
|
|
4461
|
+
const catalog = propPlans ? { plans: propPlans, families: Array.isArray(propFamilies) ? propFamilies : null } : { plans: fetched.data?.plans ?? [], families: fetched.data?.families ?? null };
|
|
4462
|
+
const loading = !propPlans && Boolean(client) && fetched.isPending;
|
|
4463
|
+
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;
|
|
4064
4464
|
const [currentPlan, setCurrentPlan] = React2.useState(currentPlanCode ?? null);
|
|
4065
|
-
const [
|
|
4465
|
+
const [selectedInterval, setSelectedInterval] = React2.useState(null);
|
|
4066
4466
|
const [processing, setProcessing] = React2.useState(false);
|
|
4067
4467
|
const [checkoutError, setCheckoutError] = React2.useState(null);
|
|
4068
4468
|
React2.useEffect(() => {
|
|
4069
4469
|
if (currentPlanCode !== void 0) setCurrentPlan(currentPlanCode);
|
|
4070
4470
|
}, [currentPlanCode]);
|
|
4071
|
-
|
|
4471
|
+
const withFamilies = Array.isArray(catalog.families);
|
|
4472
|
+
async function selectPlan(plan, family) {
|
|
4072
4473
|
setCheckoutError(null);
|
|
4073
4474
|
if (onSelect) {
|
|
4074
|
-
onSelect(plan);
|
|
4475
|
+
onSelect(plan, family);
|
|
4075
4476
|
return;
|
|
4076
4477
|
}
|
|
4077
4478
|
try {
|
|
@@ -4097,37 +4498,57 @@ function PricingTable({
|
|
|
4097
4498
|
onCheckoutError?.(error);
|
|
4098
4499
|
}
|
|
4099
4500
|
}
|
|
4100
|
-
const
|
|
4101
|
-
const
|
|
4102
|
-
const
|
|
4103
|
-
const
|
|
4501
|
+
const available = getAvailableIntervals(catalog);
|
|
4502
|
+
const interval = selectedInterval !== null && available.includes(selectedInterval) ? selectedInterval : getDefaultInterval(catalog, defaultInterval);
|
|
4503
|
+
const switchVisible = showToggle && available.length > 1 && interval !== null;
|
|
4504
|
+
const cards = getPricingCards(catalog, {
|
|
4505
|
+
interval: withFamilies || switchVisible ? interval : null,
|
|
4506
|
+
locale,
|
|
4507
|
+
highlight: highlightPlan,
|
|
4508
|
+
badge: highlightLabel,
|
|
4509
|
+
features,
|
|
4510
|
+
contactSales: contactSalesPlans,
|
|
4511
|
+
sort
|
|
4512
|
+
});
|
|
4513
|
+
const current = catalog.plans.find((p) => p.code === currentPlan);
|
|
4514
|
+
const currentYearly = current ? yearlyMinor(current) : null;
|
|
4515
|
+
const isUpgrade = (plan) => {
|
|
4516
|
+
if (!current || current.amount_currency !== plan.amount_currency) return void 0;
|
|
4517
|
+
const yearly = yearlyMinor(plan);
|
|
4518
|
+
return currentYearly !== null && yearly !== null ? yearly > currentYearly : plan.amount_cents > current.amount_cents;
|
|
4519
|
+
};
|
|
4104
4520
|
const rootProps = root("pricingTable", text.body, className);
|
|
4105
4521
|
if (loading) return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { rootProps });
|
|
4106
4522
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ...rootProps, children: [
|
|
4107
|
-
|
|
4523
|
+
switchVisible && interval !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4524
|
+
IntervalSwitch,
|
|
4525
|
+
{
|
|
4526
|
+
options: getPricingIntervalOptions(catalog),
|
|
4527
|
+
selected: interval,
|
|
4528
|
+
onChange: setSelectedInterval,
|
|
4529
|
+
annualDiscountLabel,
|
|
4530
|
+
el
|
|
4531
|
+
}
|
|
4532
|
+
),
|
|
4108
4533
|
processing && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", children: "Payment is processing. Confirmation may take a little while." }),
|
|
4109
4534
|
(checkoutError ?? loadError) && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", style: { color: T.dangerText }, children: checkoutError ?? loadError }),
|
|
4110
4535
|
/* @__PURE__ */ jsxRuntime.jsx("div", { ...el("pricingTable__grid", {
|
|
4111
4536
|
display: "grid",
|
|
4112
4537
|
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
|
4113
4538
|
gap: "1.5rem"
|
|
4114
|
-
}), children:
|
|
4539
|
+
}), children: cards.map((card2) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4115
4540
|
Card,
|
|
4116
4541
|
{
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
isUpgrade: current && current.amount_currency === plan.amount_currency ? plan.amount_cents > current.amount_cents : void 0,
|
|
4121
|
-
isHighlighted: highlightPlan === plan.code,
|
|
4542
|
+
card: card2,
|
|
4543
|
+
isCurrent: currentPlan === card2.plan.code,
|
|
4544
|
+
isUpgrade: isUpgrade(card2.plan),
|
|
4122
4545
|
filled: highlightStyle === "filled",
|
|
4123
|
-
highlightLabel,
|
|
4124
|
-
isEnt: isEnterprise(plan),
|
|
4125
4546
|
enterpriseEmail,
|
|
4126
|
-
onSelect: () => void selectPlan(plan),
|
|
4547
|
+
onSelect: () => void selectPlan(card2.plan, card2.family),
|
|
4127
4548
|
el,
|
|
4128
4549
|
panel: planCardLayout === "panel"
|
|
4129
4550
|
},
|
|
4130
|
-
|
|
4551
|
+
card2.key
|
|
4131
4552
|
)) }),
|
|
4132
4553
|
/* @__PURE__ */ jsxRuntime.jsx(NozleBranding, {})
|
|
4133
4554
|
] });
|
|
@@ -5179,6 +5600,10 @@ exports.UsageMeter = UsageMeter;
|
|
|
5179
5600
|
exports.UsageTrend = UsageTrend;
|
|
5180
5601
|
exports.comparisonRows = comparisonRows;
|
|
5181
5602
|
exports.currentPlanMeters = currentPlanMeters;
|
|
5603
|
+
exports.getAvailableIntervals = getAvailableIntervals;
|
|
5604
|
+
exports.getDefaultInterval = getDefaultInterval;
|
|
5605
|
+
exports.getPricingCards = getPricingCards;
|
|
5606
|
+
exports.getPricingIntervalOptions = getPricingIntervalOptions;
|
|
5182
5607
|
exports.getUsageMeterColor = getUsageMeterColor;
|
|
5183
5608
|
exports.planFeatures = planFeatures;
|
|
5184
5609
|
exports.sortPlansByPrice = sortPlansByPrice;
|