@akira-io/billing-js 0.1.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/checkout.cjs +10 -0
- package/dist/checkout.cjs.map +1 -0
- package/dist/checkout.d.cts +3 -0
- package/dist/checkout.d.ts +3 -0
- package/dist/checkout.js +8 -0
- package/dist/checkout.js.map +1 -0
- package/dist/client-DpOXhuxx.d.cts +141 -0
- package/dist/client-DpOXhuxx.d.ts +141 -0
- package/dist/client.cjs +179 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +176 -0
- package/dist/client.js.map +1 -0
- package/dist/downloads.cjs +51 -0
- package/dist/downloads.cjs.map +1 -0
- package/dist/downloads.d.cts +34 -0
- package/dist/downloads.d.ts +34 -0
- package/dist/downloads.js +46 -0
- package/dist/downloads.js.map +1 -0
- package/dist/helpers.cjs +116 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +55 -0
- package/dist/helpers.d.ts +55 -0
- package/dist/helpers.js +109 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.cjs +439 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +416 -0
- package/dist/index.js.map +1 -0
- package/dist/pricing.cjs +104 -0
- package/dist/pricing.cjs.map +1 -0
- package/dist/pricing.d.cts +15 -0
- package/dist/pricing.d.ts +15 -0
- package/dist/pricing.js +101 -0
- package/dist/pricing.js.map +1 -0
- package/dist/react.cjs +219 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +39 -0
- package/dist/react.d.ts +39 -0
- package/dist/react.js +215 -0
- package/dist/react.js.map +1 -0
- package/dist/types-CH4Vkivj.d.cts +59 -0
- package/dist/types-CH4Vkivj.d.ts +59 -0
- package/dist/vue.cjs +218 -0
- package/dist/vue.cjs.map +1 -0
- package/dist/vue.d.cts +32 -0
- package/dist/vue.d.ts +32 -0
- package/dist/vue.js +214 -0
- package/dist/vue.js.map +1 -0
- package/package.json +113 -0
package/dist/helpers.cjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/checkout.ts
|
|
4
|
+
function checkoutUrl(baseUrl, productKey, planKey) {
|
|
5
|
+
return `${baseUrl.replace(/\/$/, "")}/subscribe/${productKey}/${planKey}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/pricing.ts
|
|
9
|
+
function formatPrice(amountInCents, currency) {
|
|
10
|
+
const symbol = currency.toLowerCase() === "eur" ? "\u20AC" : currency.toUpperCase() + " ";
|
|
11
|
+
const major = (amountInCents / 100).toFixed(amountInCents % 100 === 0 ? 0 : 2);
|
|
12
|
+
return `${symbol}${major}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/helpers.ts
|
|
16
|
+
function isFreeTier(tier) {
|
|
17
|
+
return tier.monthly?.amount === 0 && tier.yearly === null && tier.oneTime === null;
|
|
18
|
+
}
|
|
19
|
+
function isOneTimeTier(tier) {
|
|
20
|
+
return tier.oneTime !== null && tier.monthly === null;
|
|
21
|
+
}
|
|
22
|
+
function hasYearly(tier) {
|
|
23
|
+
return tier.yearly !== null && tier.monthly !== null;
|
|
24
|
+
}
|
|
25
|
+
function getCtaProps(tier, opts) {
|
|
26
|
+
const meta = opts.tierMeta;
|
|
27
|
+
const monthlyHref = tier.monthly ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.monthly.planKey) : null;
|
|
28
|
+
const yearlyHref = tier.yearly ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.yearly.planKey) : null;
|
|
29
|
+
const oneTimeHref = tier.oneTime ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.oneTime.planKey) : null;
|
|
30
|
+
if (tier.isComingSoon) {
|
|
31
|
+
return {
|
|
32
|
+
label: opts.comingSoonLabel ?? "Coming soon",
|
|
33
|
+
href: null,
|
|
34
|
+
disabled: true,
|
|
35
|
+
monthlyHref,
|
|
36
|
+
yearlyHref,
|
|
37
|
+
oneTimeHref
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (meta?.ctaHref) {
|
|
41
|
+
return {
|
|
42
|
+
label: meta.ctaLabel ?? "Get started",
|
|
43
|
+
href: meta.ctaHref,
|
|
44
|
+
disabled: false,
|
|
45
|
+
monthlyHref,
|
|
46
|
+
yearlyHref,
|
|
47
|
+
oneTimeHref
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
let primary;
|
|
51
|
+
let defaultLabel;
|
|
52
|
+
if (isFreeTier(tier)) {
|
|
53
|
+
primary = null;
|
|
54
|
+
defaultLabel = opts.freeLabel ?? "Get started";
|
|
55
|
+
} else if (isOneTimeTier(tier)) {
|
|
56
|
+
primary = oneTimeHref;
|
|
57
|
+
defaultLabel = opts.buyLabel ?? "Buy";
|
|
58
|
+
} else {
|
|
59
|
+
primary = opts.interval === "yearly" ? yearlyHref ?? monthlyHref : monthlyHref;
|
|
60
|
+
defaultLabel = opts.subscribeLabel ?? "Subscribe";
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
label: meta?.ctaLabel ?? defaultLabel,
|
|
64
|
+
href: primary,
|
|
65
|
+
disabled: false,
|
|
66
|
+
monthlyHref,
|
|
67
|
+
yearlyHref,
|
|
68
|
+
oneTimeHref
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function getActivePrice(tier, interval) {
|
|
72
|
+
if (interval === "yearly" && tier.yearly) {
|
|
73
|
+
return {
|
|
74
|
+
amount: formatPrice(tier.yearly.amount, tier.yearly.currency),
|
|
75
|
+
suffix: "/year",
|
|
76
|
+
raw: { amount: tier.yearly.amount, currency: tier.yearly.currency },
|
|
77
|
+
note: tier.yearly.monthsFree > 0 ? `${tier.yearly.monthsFree} months free` : void 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (interval === "oneTime" && tier.oneTime) {
|
|
81
|
+
return {
|
|
82
|
+
amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),
|
|
83
|
+
suffix: " one-time",
|
|
84
|
+
raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency }
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (tier.monthly) {
|
|
88
|
+
return {
|
|
89
|
+
amount: formatPrice(tier.monthly.amount, tier.monthly.currency),
|
|
90
|
+
suffix: "/month",
|
|
91
|
+
raw: { amount: tier.monthly.amount, currency: tier.monthly.currency }
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (tier.oneTime) {
|
|
95
|
+
return {
|
|
96
|
+
amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),
|
|
97
|
+
suffix: " one-time",
|
|
98
|
+
raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency }
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { amount: "\u2014", suffix: "", raw: null };
|
|
102
|
+
}
|
|
103
|
+
function defaultInterval(tiers) {
|
|
104
|
+
if (tiers.some(hasYearly)) return "monthly";
|
|
105
|
+
if (tiers.every((t) => t.monthly === null && t.oneTime !== null)) return "oneTime";
|
|
106
|
+
return "monthly";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.defaultInterval = defaultInterval;
|
|
110
|
+
exports.getActivePrice = getActivePrice;
|
|
111
|
+
exports.getCtaProps = getCtaProps;
|
|
112
|
+
exports.hasYearly = hasYearly;
|
|
113
|
+
exports.isFreeTier = isFreeTier;
|
|
114
|
+
exports.isOneTimeTier = isOneTimeTier;
|
|
115
|
+
//# sourceMappingURL=helpers.cjs.map
|
|
116
|
+
//# sourceMappingURL=helpers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checkout.ts","../src/pricing.ts","../src/helpers.ts"],"names":[],"mappings":"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","file":"helpers.cjs","sourcesContent":["export function checkoutUrl(baseUrl: string, productKey: string, planKey: string): string {\n return `${baseUrl.replace(/\\/$/, '')}/subscribe/${productKey}/${planKey}`;\n}\n","import type {\n BillingInterval,\n PricingFeature,\n PricingPayload,\n PricingTier,\n TierMeta,\n} from './types';\n\nexport type FetchPricingConfig = {\n baseUrl: string;\n productKey: string;\n tierMeta?: Record<string, TierMeta>;\n yearlyMonthsFree?: number;\n /** Override the global fetch (e.g. Node 18+ or custom retry). */\n fetcher?: typeof fetch;\n};\n\ntype ApiFeature = {\n key: string;\n name: string;\n description: string | null;\n};\n\ntype ApiPlan = {\n key: string;\n name: string;\n description: string | null;\n amount: number | null;\n currency: string | null;\n billing_interval: BillingInterval | null;\n trial_period_days: number;\n is_coming_soon?: boolean;\n features: ApiFeature[];\n};\n\ntype ApiPayload = {\n product: string;\n name: string;\n description: string | null;\n beta_active: boolean;\n plans: ApiPlan[];\n};\n\nconst INTERVAL_SUFFIXES = ['_monthly', '_yearly', '_month', '_year', '_one_time'];\n\nfunction tierKeyFromPlanKey(planKey: string): string {\n for (const suf of INTERVAL_SUFFIXES) {\n if (planKey.endsWith(suf)) {\n return planKey.slice(0, -suf.length);\n }\n }\n return planKey;\n}\n\nfunction titleCase(s: string): string {\n return s.replace(/[_-]+/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction emptyPayload(productKey: string): PricingPayload {\n return { product: productKey, betaActive: false, tiers: [] };\n}\n\nfunction shapeFromApi(payload: ApiPayload, config: FetchPricingConfig): PricingPayload {\n const meta = config.tierMeta ?? {};\n const monthsFree = config.yearlyMonthsFree ?? 2;\n const tiersMap = new Map<string, PricingTier>();\n\n for (const plan of payload.plans) {\n const tierKey = tierKeyFromPlanKey(plan.key);\n const m = meta[tierKey];\n\n if (!tiersMap.has(tierKey)) {\n tiersMap.set(tierKey, {\n key: tierKey,\n name: m?.label ?? titleCase(tierKey),\n tagline: m?.tagline ?? plan.description ?? '',\n highlighted: m?.highlighted ?? false,\n monthly: null,\n yearly: null,\n oneTime: null,\n isComingSoon: false,\n features: plan.features.map(\n (f): PricingFeature => ({ key: f.key, name: f.name, description: f.description }),\n ),\n });\n }\n\n const tier = tiersMap.get(tierKey)!;\n\n if (plan.is_coming_soon === true) {\n tier.isComingSoon = true;\n }\n\n if (tier.features.length === 0 && plan.features.length > 0) {\n tier.features = plan.features.map((f) => ({ key: f.key, name: f.name, description: f.description }));\n }\n\n if (plan.billing_interval === 'month' && plan.amount !== null && plan.currency !== null) {\n tier.monthly = { amount: plan.amount, currency: plan.currency, planKey: plan.key };\n continue;\n }\n\n if (plan.billing_interval === 'year' && plan.amount !== null && plan.currency !== null) {\n tier.yearly = { amount: plan.amount, currency: plan.currency, monthsFree, planKey: plan.key };\n continue;\n }\n\n if (plan.billing_interval === null) {\n const amount = plan.amount ?? 0;\n const currency = plan.currency ?? 'eur';\n if (amount === 0) {\n tier.monthly = { amount: 0, currency, planKey: plan.key };\n } else {\n tier.oneTime = { amount, currency, planKey: plan.key };\n }\n }\n }\n\n const tiers = Array.from(tiersMap.values()).sort((a, b) => {\n const oa = meta[a.key]?.order ?? 999;\n const ob = meta[b.key]?.order ?? 999;\n return oa - ob;\n });\n\n return {\n product: payload.product,\n betaActive: payload.beta_active,\n tiers,\n };\n}\n\nexport async function fetchPricing(config: FetchPricingConfig): Promise<PricingPayload> {\n const base = config.baseUrl?.replace(/\\/$/, '');\n if (!base) return emptyPayload(config.productKey);\n\n const f = config.fetcher ?? globalThis.fetch;\n if (!f) {\n throw new Error('No fetch implementation available. Pass a fetcher in config or use Node 18+.');\n }\n\n try {\n const res = await f(`${base}/api/v1/products/${config.productKey}/plans`, {\n headers: { Accept: 'application/json' },\n });\n if (!res.ok) return emptyPayload(config.productKey);\n const data = (await res.json()) as ApiPayload;\n return shapeFromApi(data, config);\n } catch {\n return emptyPayload(config.productKey);\n }\n}\n\nexport function formatPrice(amountInCents: number, currency: string): string {\n const symbol = currency.toLowerCase() === 'eur' ? '€' : currency.toUpperCase() + ' ';\n const major = (amountInCents / 100).toFixed(amountInCents % 100 === 0 ? 0 : 2);\n return `${symbol}${major}`;\n}\n\nexport type { PricingFeature, PricingPayload, PricingTier, TierMeta };\n","import { checkoutUrl } from './checkout';\nimport { formatPrice } from './pricing';\nimport type { PricingTier, TierMeta } from './types';\n\nexport type IntervalKey = 'monthly' | 'yearly' | 'oneTime';\n\nexport type CtaProps = {\n label: string;\n href: string | null;\n disabled: boolean;\n monthlyHref: string | null;\n yearlyHref: string | null;\n oneTimeHref: string | null;\n};\n\nexport type CtaOptions = {\n billingBaseUrl: string;\n productKey: string;\n tierMeta?: TierMeta;\n interval?: IntervalKey;\n freeLabel?: string;\n subscribeLabel?: string;\n buyLabel?: string;\n comingSoonLabel?: string;\n};\n\nexport function isFreeTier(tier: PricingTier): boolean {\n return tier.monthly?.amount === 0 && tier.yearly === null && tier.oneTime === null;\n}\n\nexport function isOneTimeTier(tier: PricingTier): boolean {\n return tier.oneTime !== null && tier.monthly === null;\n}\n\nexport function hasYearly(tier: PricingTier): boolean {\n return tier.yearly !== null && tier.monthly !== null;\n}\n\n/**\n * Resolves all CTA fields for a tier — label, primary href, per-interval\n * hrefs (so a UI toggle can hot-swap without re-deriving), and disabled\n * state for coming-soon plans. Replaces hand-rolled if/else trees in\n * landing pages.\n */\nexport function getCtaProps(tier: PricingTier, opts: CtaOptions): CtaProps {\n const meta = opts.tierMeta;\n const monthlyHref = tier.monthly\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.monthly.planKey)\n : null;\n const yearlyHref = tier.yearly\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.yearly.planKey)\n : null;\n const oneTimeHref = tier.oneTime\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.oneTime.planKey)\n : null;\n\n if (tier.isComingSoon) {\n return {\n label: opts.comingSoonLabel ?? 'Coming soon',\n href: null,\n disabled: true,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n }\n\n if (meta?.ctaHref) {\n return {\n label: meta.ctaLabel ?? 'Get started',\n href: meta.ctaHref,\n disabled: false,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n }\n\n let primary: string | null;\n let defaultLabel: string;\n\n if (isFreeTier(tier)) {\n primary = null;\n defaultLabel = opts.freeLabel ?? 'Get started';\n } else if (isOneTimeTier(tier)) {\n primary = oneTimeHref;\n defaultLabel = opts.buyLabel ?? 'Buy';\n } else {\n primary = opts.interval === 'yearly' ? yearlyHref ?? monthlyHref : monthlyHref;\n defaultLabel = opts.subscribeLabel ?? 'Subscribe';\n }\n\n return {\n label: meta?.ctaLabel ?? defaultLabel,\n href: primary,\n disabled: false,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n}\n\nexport type FormattedPrice = {\n amount: string;\n suffix: string;\n raw: { amount: number; currency: string } | null;\n note?: string;\n};\n\n/**\n * Returns the price + suffix to render for a tier given the active\n * interval. Falls back gracefully: a tier with only monthly always\n * shows monthly; a free tier shows €0; a one-time tier shows the amount\n * with a 'one-time' suffix.\n */\nexport function getActivePrice(tier: PricingTier, interval: IntervalKey): FormattedPrice {\n if (interval === 'yearly' && tier.yearly) {\n return {\n amount: formatPrice(tier.yearly.amount, tier.yearly.currency),\n suffix: '/year',\n raw: { amount: tier.yearly.amount, currency: tier.yearly.currency },\n note: tier.yearly.monthsFree > 0 ? `${tier.yearly.monthsFree} months free` : undefined,\n };\n }\n\n if (interval === 'oneTime' && tier.oneTime) {\n return {\n amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),\n suffix: ' one-time',\n raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency },\n };\n }\n\n if (tier.monthly) {\n return {\n amount: formatPrice(tier.monthly.amount, tier.monthly.currency),\n suffix: '/month',\n raw: { amount: tier.monthly.amount, currency: tier.monthly.currency },\n };\n }\n\n if (tier.oneTime) {\n return {\n amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),\n suffix: ' one-time',\n raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency },\n };\n }\n\n return { amount: '—', suffix: '', raw: null };\n}\n\n/**\n * Picks the natural default interval for a list of tiers: 'yearly' if\n * any tier has a yearly option, else 'monthly'. Useful as the initial\n * state for a billing-interval toggle.\n */\nexport function defaultInterval(tiers: PricingTier[]): IntervalKey {\n if (tiers.some(hasYearly)) return 'monthly';\n if (tiers.every((t) => t.monthly === null && t.oneTime !== null)) return 'oneTime';\n return 'monthly';\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { T as TierMeta, b as PricingTier } from './types-CH4Vkivj.cjs';
|
|
2
|
+
|
|
3
|
+
type IntervalKey = 'monthly' | 'yearly' | 'oneTime';
|
|
4
|
+
type CtaProps = {
|
|
5
|
+
label: string;
|
|
6
|
+
href: string | null;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
monthlyHref: string | null;
|
|
9
|
+
yearlyHref: string | null;
|
|
10
|
+
oneTimeHref: string | null;
|
|
11
|
+
};
|
|
12
|
+
type CtaOptions = {
|
|
13
|
+
billingBaseUrl: string;
|
|
14
|
+
productKey: string;
|
|
15
|
+
tierMeta?: TierMeta;
|
|
16
|
+
interval?: IntervalKey;
|
|
17
|
+
freeLabel?: string;
|
|
18
|
+
subscribeLabel?: string;
|
|
19
|
+
buyLabel?: string;
|
|
20
|
+
comingSoonLabel?: string;
|
|
21
|
+
};
|
|
22
|
+
declare function isFreeTier(tier: PricingTier): boolean;
|
|
23
|
+
declare function isOneTimeTier(tier: PricingTier): boolean;
|
|
24
|
+
declare function hasYearly(tier: PricingTier): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves all CTA fields for a tier — label, primary href, per-interval
|
|
27
|
+
* hrefs (so a UI toggle can hot-swap without re-deriving), and disabled
|
|
28
|
+
* state for coming-soon plans. Replaces hand-rolled if/else trees in
|
|
29
|
+
* landing pages.
|
|
30
|
+
*/
|
|
31
|
+
declare function getCtaProps(tier: PricingTier, opts: CtaOptions): CtaProps;
|
|
32
|
+
type FormattedPrice = {
|
|
33
|
+
amount: string;
|
|
34
|
+
suffix: string;
|
|
35
|
+
raw: {
|
|
36
|
+
amount: number;
|
|
37
|
+
currency: string;
|
|
38
|
+
} | null;
|
|
39
|
+
note?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Returns the price + suffix to render for a tier given the active
|
|
43
|
+
* interval. Falls back gracefully: a tier with only monthly always
|
|
44
|
+
* shows monthly; a free tier shows €0; a one-time tier shows the amount
|
|
45
|
+
* with a 'one-time' suffix.
|
|
46
|
+
*/
|
|
47
|
+
declare function getActivePrice(tier: PricingTier, interval: IntervalKey): FormattedPrice;
|
|
48
|
+
/**
|
|
49
|
+
* Picks the natural default interval for a list of tiers: 'yearly' if
|
|
50
|
+
* any tier has a yearly option, else 'monthly'. Useful as the initial
|
|
51
|
+
* state for a billing-interval toggle.
|
|
52
|
+
*/
|
|
53
|
+
declare function defaultInterval(tiers: PricingTier[]): IntervalKey;
|
|
54
|
+
|
|
55
|
+
export { type CtaOptions, type CtaProps, type FormattedPrice, type IntervalKey, defaultInterval, getActivePrice, getCtaProps, hasYearly, isFreeTier, isOneTimeTier };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { T as TierMeta, b as PricingTier } from './types-CH4Vkivj.js';
|
|
2
|
+
|
|
3
|
+
type IntervalKey = 'monthly' | 'yearly' | 'oneTime';
|
|
4
|
+
type CtaProps = {
|
|
5
|
+
label: string;
|
|
6
|
+
href: string | null;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
monthlyHref: string | null;
|
|
9
|
+
yearlyHref: string | null;
|
|
10
|
+
oneTimeHref: string | null;
|
|
11
|
+
};
|
|
12
|
+
type CtaOptions = {
|
|
13
|
+
billingBaseUrl: string;
|
|
14
|
+
productKey: string;
|
|
15
|
+
tierMeta?: TierMeta;
|
|
16
|
+
interval?: IntervalKey;
|
|
17
|
+
freeLabel?: string;
|
|
18
|
+
subscribeLabel?: string;
|
|
19
|
+
buyLabel?: string;
|
|
20
|
+
comingSoonLabel?: string;
|
|
21
|
+
};
|
|
22
|
+
declare function isFreeTier(tier: PricingTier): boolean;
|
|
23
|
+
declare function isOneTimeTier(tier: PricingTier): boolean;
|
|
24
|
+
declare function hasYearly(tier: PricingTier): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves all CTA fields for a tier — label, primary href, per-interval
|
|
27
|
+
* hrefs (so a UI toggle can hot-swap without re-deriving), and disabled
|
|
28
|
+
* state for coming-soon plans. Replaces hand-rolled if/else trees in
|
|
29
|
+
* landing pages.
|
|
30
|
+
*/
|
|
31
|
+
declare function getCtaProps(tier: PricingTier, opts: CtaOptions): CtaProps;
|
|
32
|
+
type FormattedPrice = {
|
|
33
|
+
amount: string;
|
|
34
|
+
suffix: string;
|
|
35
|
+
raw: {
|
|
36
|
+
amount: number;
|
|
37
|
+
currency: string;
|
|
38
|
+
} | null;
|
|
39
|
+
note?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Returns the price + suffix to render for a tier given the active
|
|
43
|
+
* interval. Falls back gracefully: a tier with only monthly always
|
|
44
|
+
* shows monthly; a free tier shows €0; a one-time tier shows the amount
|
|
45
|
+
* with a 'one-time' suffix.
|
|
46
|
+
*/
|
|
47
|
+
declare function getActivePrice(tier: PricingTier, interval: IntervalKey): FormattedPrice;
|
|
48
|
+
/**
|
|
49
|
+
* Picks the natural default interval for a list of tiers: 'yearly' if
|
|
50
|
+
* any tier has a yearly option, else 'monthly'. Useful as the initial
|
|
51
|
+
* state for a billing-interval toggle.
|
|
52
|
+
*/
|
|
53
|
+
declare function defaultInterval(tiers: PricingTier[]): IntervalKey;
|
|
54
|
+
|
|
55
|
+
export { type CtaOptions, type CtaProps, type FormattedPrice, type IntervalKey, defaultInterval, getActivePrice, getCtaProps, hasYearly, isFreeTier, isOneTimeTier };
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/checkout.ts
|
|
2
|
+
function checkoutUrl(baseUrl, productKey, planKey) {
|
|
3
|
+
return `${baseUrl.replace(/\/$/, "")}/subscribe/${productKey}/${planKey}`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/pricing.ts
|
|
7
|
+
function formatPrice(amountInCents, currency) {
|
|
8
|
+
const symbol = currency.toLowerCase() === "eur" ? "\u20AC" : currency.toUpperCase() + " ";
|
|
9
|
+
const major = (amountInCents / 100).toFixed(amountInCents % 100 === 0 ? 0 : 2);
|
|
10
|
+
return `${symbol}${major}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/helpers.ts
|
|
14
|
+
function isFreeTier(tier) {
|
|
15
|
+
return tier.monthly?.amount === 0 && tier.yearly === null && tier.oneTime === null;
|
|
16
|
+
}
|
|
17
|
+
function isOneTimeTier(tier) {
|
|
18
|
+
return tier.oneTime !== null && tier.monthly === null;
|
|
19
|
+
}
|
|
20
|
+
function hasYearly(tier) {
|
|
21
|
+
return tier.yearly !== null && tier.monthly !== null;
|
|
22
|
+
}
|
|
23
|
+
function getCtaProps(tier, opts) {
|
|
24
|
+
const meta = opts.tierMeta;
|
|
25
|
+
const monthlyHref = tier.monthly ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.monthly.planKey) : null;
|
|
26
|
+
const yearlyHref = tier.yearly ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.yearly.planKey) : null;
|
|
27
|
+
const oneTimeHref = tier.oneTime ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.oneTime.planKey) : null;
|
|
28
|
+
if (tier.isComingSoon) {
|
|
29
|
+
return {
|
|
30
|
+
label: opts.comingSoonLabel ?? "Coming soon",
|
|
31
|
+
href: null,
|
|
32
|
+
disabled: true,
|
|
33
|
+
monthlyHref,
|
|
34
|
+
yearlyHref,
|
|
35
|
+
oneTimeHref
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (meta?.ctaHref) {
|
|
39
|
+
return {
|
|
40
|
+
label: meta.ctaLabel ?? "Get started",
|
|
41
|
+
href: meta.ctaHref,
|
|
42
|
+
disabled: false,
|
|
43
|
+
monthlyHref,
|
|
44
|
+
yearlyHref,
|
|
45
|
+
oneTimeHref
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
let primary;
|
|
49
|
+
let defaultLabel;
|
|
50
|
+
if (isFreeTier(tier)) {
|
|
51
|
+
primary = null;
|
|
52
|
+
defaultLabel = opts.freeLabel ?? "Get started";
|
|
53
|
+
} else if (isOneTimeTier(tier)) {
|
|
54
|
+
primary = oneTimeHref;
|
|
55
|
+
defaultLabel = opts.buyLabel ?? "Buy";
|
|
56
|
+
} else {
|
|
57
|
+
primary = opts.interval === "yearly" ? yearlyHref ?? monthlyHref : monthlyHref;
|
|
58
|
+
defaultLabel = opts.subscribeLabel ?? "Subscribe";
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
label: meta?.ctaLabel ?? defaultLabel,
|
|
62
|
+
href: primary,
|
|
63
|
+
disabled: false,
|
|
64
|
+
monthlyHref,
|
|
65
|
+
yearlyHref,
|
|
66
|
+
oneTimeHref
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getActivePrice(tier, interval) {
|
|
70
|
+
if (interval === "yearly" && tier.yearly) {
|
|
71
|
+
return {
|
|
72
|
+
amount: formatPrice(tier.yearly.amount, tier.yearly.currency),
|
|
73
|
+
suffix: "/year",
|
|
74
|
+
raw: { amount: tier.yearly.amount, currency: tier.yearly.currency },
|
|
75
|
+
note: tier.yearly.monthsFree > 0 ? `${tier.yearly.monthsFree} months free` : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (interval === "oneTime" && tier.oneTime) {
|
|
79
|
+
return {
|
|
80
|
+
amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),
|
|
81
|
+
suffix: " one-time",
|
|
82
|
+
raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (tier.monthly) {
|
|
86
|
+
return {
|
|
87
|
+
amount: formatPrice(tier.monthly.amount, tier.monthly.currency),
|
|
88
|
+
suffix: "/month",
|
|
89
|
+
raw: { amount: tier.monthly.amount, currency: tier.monthly.currency }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (tier.oneTime) {
|
|
93
|
+
return {
|
|
94
|
+
amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),
|
|
95
|
+
suffix: " one-time",
|
|
96
|
+
raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { amount: "\u2014", suffix: "", raw: null };
|
|
100
|
+
}
|
|
101
|
+
function defaultInterval(tiers) {
|
|
102
|
+
if (tiers.some(hasYearly)) return "monthly";
|
|
103
|
+
if (tiers.every((t) => t.monthly === null && t.oneTime !== null)) return "oneTime";
|
|
104
|
+
return "monthly";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { defaultInterval, getActivePrice, getCtaProps, hasYearly, isFreeTier, isOneTimeTier };
|
|
108
|
+
//# sourceMappingURL=helpers.js.map
|
|
109
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/checkout.ts","../src/pricing.ts","../src/helpers.ts"],"names":[],"mappings":";AAAO,SAAS,WAAA,CAAY,OAAA,EAAiB,UAAA,EAAoB,OAAA,EAAyB;AACtF,EAAA,OAAO,CAAA,EAAG,QAAQ,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,WAAA,EAAc,UAAU,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA;AAC3E;;;ACsJO,SAAS,WAAA,CAAY,eAAuB,QAAA,EAA0B;AACzE,EAAA,MAAM,MAAA,GAAS,SAAS,WAAA,EAAY,KAAM,QAAQ,QAAA,GAAM,QAAA,CAAS,aAAY,GAAI,GAAA;AACjF,EAAA,MAAM,KAAA,GAAA,CAAS,gBAAgB,GAAA,EAAK,OAAA,CAAQ,gBAAgB,GAAA,KAAQ,CAAA,GAAI,IAAI,CAAC,CAAA;AAC7E,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,KAAK,CAAA,CAAA;AAC5B;;;AClIO,SAAS,WAAW,IAAA,EAA4B;AACnD,EAAA,OAAO,IAAA,CAAK,SAAS,MAAA,KAAW,CAAA,IAAK,KAAK,MAAA,KAAW,IAAA,IAAQ,KAAK,OAAA,KAAY,IAAA;AAClF;AAEO,SAAS,cAAc,IAAA,EAA4B;AACtD,EAAA,OAAO,IAAA,CAAK,OAAA,KAAY,IAAA,IAAQ,IAAA,CAAK,OAAA,KAAY,IAAA;AACrD;AAEO,SAAS,UAAU,IAAA,EAA4B;AAClD,EAAA,OAAO,IAAA,CAAK,MAAA,KAAW,IAAA,IAAQ,IAAA,CAAK,OAAA,KAAY,IAAA;AACpD;AAQO,SAAS,WAAA,CAAY,MAAmB,IAAA,EAA4B;AACvE,EAAA,MAAM,OAAO,IAAA,CAAK,QAAA;AAClB,EAAA,MAAM,WAAA,GAAc,IAAA,CAAK,OAAA,GACnB,WAAA,CAAY,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,GACtE,IAAA;AACN,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,MAAA,GAClB,WAAA,CAAY,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,GACrE,IAAA;AACN,EAAA,MAAM,WAAA,GAAc,IAAA,CAAK,OAAA,GACnB,WAAA,CAAY,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,UAAA,EAAY,IAAA,CAAK,OAAA,CAAQ,OAAO,CAAA,GACtE,IAAA;AAEN,EAAA,IAAI,KAAK,YAAA,EAAc;AACnB,IAAA,OAAO;AAAA,MACH,KAAA,EAAO,KAAK,eAAA,IAAmB,aAAA;AAAA,MAC/B,IAAA,EAAM,IAAA;AAAA,MACN,QAAA,EAAU,IAAA;AAAA,MACV,WAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AAEA,EAAA,IAAI,MAAM,OAAA,EAAS;AACf,IAAA,OAAO;AAAA,MACH,KAAA,EAAO,KAAK,QAAA,IAAY,aAAA;AAAA,MACxB,MAAM,IAAA,CAAK,OAAA;AAAA,MACX,QAAA,EAAU,KAAA;AAAA,MACV,WAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AAEA,EAAA,IAAI,OAAA;AACJ,EAAA,IAAI,YAAA;AAEJ,EAAA,IAAI,UAAA,CAAW,IAAI,CAAA,EAAG;AAClB,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,YAAA,GAAe,KAAK,SAAA,IAAa,aAAA;AAAA,EACrC,CAAA,MAAA,IAAW,aAAA,CAAc,IAAI,CAAA,EAAG;AAC5B,IAAA,OAAA,GAAU,WAAA;AACV,IAAA,YAAA,GAAe,KAAK,QAAA,IAAY,KAAA;AAAA,EACpC,CAAA,MAAO;AACH,IAAA,OAAA,GAAU,IAAA,CAAK,QAAA,KAAa,QAAA,GAAW,UAAA,IAAc,WAAA,GAAc,WAAA;AACnE,IAAA,YAAA,GAAe,KAAK,cAAA,IAAkB,WAAA;AAAA,EAC1C;AAEA,EAAA,OAAO;AAAA,IACH,KAAA,EAAO,MAAM,QAAA,IAAY,YAAA;AAAA,IACzB,IAAA,EAAM,OAAA;AAAA,IACN,QAAA,EAAU,KAAA;AAAA,IACV,WAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACJ;AACJ;AAeO,SAAS,cAAA,CAAe,MAAmB,QAAA,EAAuC;AACrF,EAAA,IAAI,QAAA,KAAa,QAAA,IAAY,IAAA,CAAK,MAAA,EAAQ;AACtC,IAAA,OAAO;AAAA,MACH,QAAQ,WAAA,CAAY,IAAA,CAAK,OAAO,MAAA,EAAQ,IAAA,CAAK,OAAO,QAAQ,CAAA;AAAA,MAC5D,MAAA,EAAQ,OAAA;AAAA,MACR,GAAA,EAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,OAAO,MAAA,EAAQ,QAAA,EAAU,IAAA,CAAK,MAAA,CAAO,QAAA,EAAS;AAAA,MAClE,IAAA,EAAM,KAAK,MAAA,CAAO,UAAA,GAAa,IAAI,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,UAAU,CAAA,YAAA,CAAA,GAAiB;AAAA,KACjF;AAAA,EACJ;AAEA,EAAA,IAAI,QAAA,KAAa,SAAA,IAAa,IAAA,CAAK,OAAA,EAAS;AACxC,IAAA,OAAO;AAAA,MACH,QAAQ,WAAA,CAAY,IAAA,CAAK,QAAQ,MAAA,EAAQ,IAAA,CAAK,QAAQ,QAAQ,CAAA;AAAA,MAC9D,MAAA,EAAQ,WAAA;AAAA,MACR,GAAA,EAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,QAAQ,MAAA,EAAQ,QAAA,EAAU,IAAA,CAAK,OAAA,CAAQ,QAAA;AAAS,KACxE;AAAA,EACJ;AAEA,EAAA,IAAI,KAAK,OAAA,EAAS;AACd,IAAA,OAAO;AAAA,MACH,QAAQ,WAAA,CAAY,IAAA,CAAK,QAAQ,MAAA,EAAQ,IAAA,CAAK,QAAQ,QAAQ,CAAA;AAAA,MAC9D,MAAA,EAAQ,QAAA;AAAA,MACR,GAAA,EAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,QAAQ,MAAA,EAAQ,QAAA,EAAU,IAAA,CAAK,OAAA,CAAQ,QAAA;AAAS,KACxE;AAAA,EACJ;AAEA,EAAA,IAAI,KAAK,OAAA,EAAS;AACd,IAAA,OAAO;AAAA,MACH,QAAQ,WAAA,CAAY,IAAA,CAAK,QAAQ,MAAA,EAAQ,IAAA,CAAK,QAAQ,QAAQ,CAAA;AAAA,MAC9D,MAAA,EAAQ,WAAA;AAAA,MACR,GAAA,EAAK,EAAE,MAAA,EAAQ,IAAA,CAAK,QAAQ,MAAA,EAAQ,QAAA,EAAU,IAAA,CAAK,OAAA,CAAQ,QAAA;AAAS,KACxE;AAAA,EACJ;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAK,MAAA,EAAQ,EAAA,EAAI,KAAK,IAAA,EAAK;AAChD;AAOO,SAAS,gBAAgB,KAAA,EAAmC;AAC/D,EAAA,IAAI,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA,EAAG,OAAO,SAAA;AAClC,EAAA,IAAI,KAAA,CAAM,KAAA,CAAM,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,KAAY,IAAA,IAAQ,CAAA,CAAE,OAAA,KAAY,IAAI,CAAA,EAAG,OAAO,SAAA;AACzE,EAAA,OAAO,SAAA;AACX","file":"helpers.js","sourcesContent":["export function checkoutUrl(baseUrl: string, productKey: string, planKey: string): string {\n return `${baseUrl.replace(/\\/$/, '')}/subscribe/${productKey}/${planKey}`;\n}\n","import type {\n BillingInterval,\n PricingFeature,\n PricingPayload,\n PricingTier,\n TierMeta,\n} from './types';\n\nexport type FetchPricingConfig = {\n baseUrl: string;\n productKey: string;\n tierMeta?: Record<string, TierMeta>;\n yearlyMonthsFree?: number;\n /** Override the global fetch (e.g. Node 18+ or custom retry). */\n fetcher?: typeof fetch;\n};\n\ntype ApiFeature = {\n key: string;\n name: string;\n description: string | null;\n};\n\ntype ApiPlan = {\n key: string;\n name: string;\n description: string | null;\n amount: number | null;\n currency: string | null;\n billing_interval: BillingInterval | null;\n trial_period_days: number;\n is_coming_soon?: boolean;\n features: ApiFeature[];\n};\n\ntype ApiPayload = {\n product: string;\n name: string;\n description: string | null;\n beta_active: boolean;\n plans: ApiPlan[];\n};\n\nconst INTERVAL_SUFFIXES = ['_monthly', '_yearly', '_month', '_year', '_one_time'];\n\nfunction tierKeyFromPlanKey(planKey: string): string {\n for (const suf of INTERVAL_SUFFIXES) {\n if (planKey.endsWith(suf)) {\n return planKey.slice(0, -suf.length);\n }\n }\n return planKey;\n}\n\nfunction titleCase(s: string): string {\n return s.replace(/[_-]+/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction emptyPayload(productKey: string): PricingPayload {\n return { product: productKey, betaActive: false, tiers: [] };\n}\n\nfunction shapeFromApi(payload: ApiPayload, config: FetchPricingConfig): PricingPayload {\n const meta = config.tierMeta ?? {};\n const monthsFree = config.yearlyMonthsFree ?? 2;\n const tiersMap = new Map<string, PricingTier>();\n\n for (const plan of payload.plans) {\n const tierKey = tierKeyFromPlanKey(plan.key);\n const m = meta[tierKey];\n\n if (!tiersMap.has(tierKey)) {\n tiersMap.set(tierKey, {\n key: tierKey,\n name: m?.label ?? titleCase(tierKey),\n tagline: m?.tagline ?? plan.description ?? '',\n highlighted: m?.highlighted ?? false,\n monthly: null,\n yearly: null,\n oneTime: null,\n isComingSoon: false,\n features: plan.features.map(\n (f): PricingFeature => ({ key: f.key, name: f.name, description: f.description }),\n ),\n });\n }\n\n const tier = tiersMap.get(tierKey)!;\n\n if (plan.is_coming_soon === true) {\n tier.isComingSoon = true;\n }\n\n if (tier.features.length === 0 && plan.features.length > 0) {\n tier.features = plan.features.map((f) => ({ key: f.key, name: f.name, description: f.description }));\n }\n\n if (plan.billing_interval === 'month' && plan.amount !== null && plan.currency !== null) {\n tier.monthly = { amount: plan.amount, currency: plan.currency, planKey: plan.key };\n continue;\n }\n\n if (plan.billing_interval === 'year' && plan.amount !== null && plan.currency !== null) {\n tier.yearly = { amount: plan.amount, currency: plan.currency, monthsFree, planKey: plan.key };\n continue;\n }\n\n if (plan.billing_interval === null) {\n const amount = plan.amount ?? 0;\n const currency = plan.currency ?? 'eur';\n if (amount === 0) {\n tier.monthly = { amount: 0, currency, planKey: plan.key };\n } else {\n tier.oneTime = { amount, currency, planKey: plan.key };\n }\n }\n }\n\n const tiers = Array.from(tiersMap.values()).sort((a, b) => {\n const oa = meta[a.key]?.order ?? 999;\n const ob = meta[b.key]?.order ?? 999;\n return oa - ob;\n });\n\n return {\n product: payload.product,\n betaActive: payload.beta_active,\n tiers,\n };\n}\n\nexport async function fetchPricing(config: FetchPricingConfig): Promise<PricingPayload> {\n const base = config.baseUrl?.replace(/\\/$/, '');\n if (!base) return emptyPayload(config.productKey);\n\n const f = config.fetcher ?? globalThis.fetch;\n if (!f) {\n throw new Error('No fetch implementation available. Pass a fetcher in config or use Node 18+.');\n }\n\n try {\n const res = await f(`${base}/api/v1/products/${config.productKey}/plans`, {\n headers: { Accept: 'application/json' },\n });\n if (!res.ok) return emptyPayload(config.productKey);\n const data = (await res.json()) as ApiPayload;\n return shapeFromApi(data, config);\n } catch {\n return emptyPayload(config.productKey);\n }\n}\n\nexport function formatPrice(amountInCents: number, currency: string): string {\n const symbol = currency.toLowerCase() === 'eur' ? '€' : currency.toUpperCase() + ' ';\n const major = (amountInCents / 100).toFixed(amountInCents % 100 === 0 ? 0 : 2);\n return `${symbol}${major}`;\n}\n\nexport type { PricingFeature, PricingPayload, PricingTier, TierMeta };\n","import { checkoutUrl } from './checkout';\nimport { formatPrice } from './pricing';\nimport type { PricingTier, TierMeta } from './types';\n\nexport type IntervalKey = 'monthly' | 'yearly' | 'oneTime';\n\nexport type CtaProps = {\n label: string;\n href: string | null;\n disabled: boolean;\n monthlyHref: string | null;\n yearlyHref: string | null;\n oneTimeHref: string | null;\n};\n\nexport type CtaOptions = {\n billingBaseUrl: string;\n productKey: string;\n tierMeta?: TierMeta;\n interval?: IntervalKey;\n freeLabel?: string;\n subscribeLabel?: string;\n buyLabel?: string;\n comingSoonLabel?: string;\n};\n\nexport function isFreeTier(tier: PricingTier): boolean {\n return tier.monthly?.amount === 0 && tier.yearly === null && tier.oneTime === null;\n}\n\nexport function isOneTimeTier(tier: PricingTier): boolean {\n return tier.oneTime !== null && tier.monthly === null;\n}\n\nexport function hasYearly(tier: PricingTier): boolean {\n return tier.yearly !== null && tier.monthly !== null;\n}\n\n/**\n * Resolves all CTA fields for a tier — label, primary href, per-interval\n * hrefs (so a UI toggle can hot-swap without re-deriving), and disabled\n * state for coming-soon plans. Replaces hand-rolled if/else trees in\n * landing pages.\n */\nexport function getCtaProps(tier: PricingTier, opts: CtaOptions): CtaProps {\n const meta = opts.tierMeta;\n const monthlyHref = tier.monthly\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.monthly.planKey)\n : null;\n const yearlyHref = tier.yearly\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.yearly.planKey)\n : null;\n const oneTimeHref = tier.oneTime\n ? checkoutUrl(opts.billingBaseUrl, opts.productKey, tier.oneTime.planKey)\n : null;\n\n if (tier.isComingSoon) {\n return {\n label: opts.comingSoonLabel ?? 'Coming soon',\n href: null,\n disabled: true,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n }\n\n if (meta?.ctaHref) {\n return {\n label: meta.ctaLabel ?? 'Get started',\n href: meta.ctaHref,\n disabled: false,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n }\n\n let primary: string | null;\n let defaultLabel: string;\n\n if (isFreeTier(tier)) {\n primary = null;\n defaultLabel = opts.freeLabel ?? 'Get started';\n } else if (isOneTimeTier(tier)) {\n primary = oneTimeHref;\n defaultLabel = opts.buyLabel ?? 'Buy';\n } else {\n primary = opts.interval === 'yearly' ? yearlyHref ?? monthlyHref : monthlyHref;\n defaultLabel = opts.subscribeLabel ?? 'Subscribe';\n }\n\n return {\n label: meta?.ctaLabel ?? defaultLabel,\n href: primary,\n disabled: false,\n monthlyHref,\n yearlyHref,\n oneTimeHref,\n };\n}\n\nexport type FormattedPrice = {\n amount: string;\n suffix: string;\n raw: { amount: number; currency: string } | null;\n note?: string;\n};\n\n/**\n * Returns the price + suffix to render for a tier given the active\n * interval. Falls back gracefully: a tier with only monthly always\n * shows monthly; a free tier shows €0; a one-time tier shows the amount\n * with a 'one-time' suffix.\n */\nexport function getActivePrice(tier: PricingTier, interval: IntervalKey): FormattedPrice {\n if (interval === 'yearly' && tier.yearly) {\n return {\n amount: formatPrice(tier.yearly.amount, tier.yearly.currency),\n suffix: '/year',\n raw: { amount: tier.yearly.amount, currency: tier.yearly.currency },\n note: tier.yearly.monthsFree > 0 ? `${tier.yearly.monthsFree} months free` : undefined,\n };\n }\n\n if (interval === 'oneTime' && tier.oneTime) {\n return {\n amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),\n suffix: ' one-time',\n raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency },\n };\n }\n\n if (tier.monthly) {\n return {\n amount: formatPrice(tier.monthly.amount, tier.monthly.currency),\n suffix: '/month',\n raw: { amount: tier.monthly.amount, currency: tier.monthly.currency },\n };\n }\n\n if (tier.oneTime) {\n return {\n amount: formatPrice(tier.oneTime.amount, tier.oneTime.currency),\n suffix: ' one-time',\n raw: { amount: tier.oneTime.amount, currency: tier.oneTime.currency },\n };\n }\n\n return { amount: '—', suffix: '', raw: null };\n}\n\n/**\n * Picks the natural default interval for a list of tiers: 'yearly' if\n * any tier has a yearly option, else 'monthly'. Useful as the initial\n * state for a billing-interval toggle.\n */\nexport function defaultInterval(tiers: PricingTier[]): IntervalKey {\n if (tiers.some(hasYearly)) return 'monthly';\n if (tiers.every((t) => t.monthly === null && t.oneTime !== null)) return 'oneTime';\n return 'monthly';\n}\n"]}
|