@funnelsgrove/payments 0.1.0 → 0.1.2

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.
Files changed (44) hide show
  1. package/README.md +32 -3
  2. package/dist/components/shared/ApplePaySubscribeButton.d.ts +15 -0
  3. package/dist/components/shared/ApplePaySubscribeButton.js +76 -0
  4. package/dist/components/shared/CheckoutBrandAssets.d.ts +12 -0
  5. package/dist/components/shared/CheckoutBrandAssets.js +56 -0
  6. package/dist/components/shared/GooglePaySubscribeButton.d.ts +14 -0
  7. package/dist/components/shared/GooglePaySubscribeButton.js +94 -0
  8. package/dist/components/shared/SharedStripeCheckoutDialog.d.ts +44 -0
  9. package/dist/components/shared/SharedStripeCheckoutDialog.js +717 -0
  10. package/dist/components/shared/SharedStripeCheckoutV2Dialog.d.ts +60 -0
  11. package/dist/components/shared/SharedStripeCheckoutV2Dialog.js +988 -0
  12. package/dist/components/shared/StripeExpressCheckoutButton.d.ts +18 -0
  13. package/dist/components/shared/StripeExpressCheckoutButton.js +164 -0
  14. package/dist/components/shared/StripeExpressCheckoutElement.d.ts +8 -0
  15. package/dist/components/shared/StripeExpressCheckoutElement.js +19 -0
  16. package/dist/components/shared/StripePlanSelector.d.ts +5 -7
  17. package/dist/components/shared/StripePlanSelector.js +16 -57
  18. package/dist/components/shared/walletPlatform.d.ts +11 -0
  19. package/dist/components/shared/walletPlatform.js +39 -0
  20. package/dist/config/billing.config.d.ts +60 -9
  21. package/dist/config/billing.config.js +7 -2
  22. package/dist/hooks/useResolvedPaywallPlans.d.ts +15 -0
  23. package/dist/hooks/useResolvedPaywallPlans.js +68 -0
  24. package/dist/index.d.ts +21 -5
  25. package/dist/index.js +21 -5
  26. package/dist/providers/paymentProvider.types.d.ts +4 -0
  27. package/dist/providers/paymentProvider.types.js +4 -0
  28. package/dist/providers/stripe/ApplePaySubscriptionCheckoutSlot.d.ts +5 -0
  29. package/dist/providers/stripe/ApplePaySubscriptionCheckoutSlot.js +21 -0
  30. package/dist/providers/stripe/GooglePaySubscriptionCheckoutSlot.d.ts +7 -0
  31. package/dist/providers/stripe/GooglePaySubscriptionCheckoutSlot.js +44 -0
  32. package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.d.ts +32 -0
  33. package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.js +69 -0
  34. package/dist/providers/stripe/useStripeSubscriptionCheckoutSession.d.ts +30 -0
  35. package/dist/providers/stripe/useStripeSubscriptionCheckoutSession.js +164 -0
  36. package/dist/services/paywallOffer.service.d.ts +65 -0
  37. package/dist/services/paywallOffer.service.js +312 -0
  38. package/dist/services/planCatalog.service.d.ts +28 -0
  39. package/dist/services/planCatalog.service.js +60 -0
  40. package/dist/services/runtimeBillingPlanCatalog.service.d.ts +6 -0
  41. package/dist/services/runtimeBillingPlanCatalog.service.js +24 -0
  42. package/dist/services/stripe.service.d.ts +66 -6
  43. package/dist/services/stripe.service.js +409 -54
  44. package/package.json +3 -3
@@ -1,12 +1,21 @@
1
1
  var _a, _b, _c, _d;
2
2
  import { loadStripe } from '@stripe/stripe-js';
3
- import { billingPlanList, } from '../config/billing.config';
4
- import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, } from '@funnelsgrove/runtime';
5
- const STRIPE_TEST_PUBLISHABLE_KEY = ((_b = (_a = process.env.NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLE_KEY) !== null && _a !== void 0 ? _a : process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) !== null && _b !== void 0 ? _b : '').trim();
6
- const STRIPE_LIVE_PUBLISHABLE_KEY = ((_d = (_c = process.env.NEXT_PUBLIC_STRIPE_LIVE_PUBLISHABLE_KEY) !== null && _c !== void 0 ? _c : process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) !== null && _d !== void 0 ? _d : '').trim();
3
+ import { billingPlanList, } from '../config/billing.config.js';
4
+ import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, resolvePreviewPaymentPublishableKey, runtimeEnvConfig, } from '@funnelsgrove/runtime';
5
+ import { isBillingPlanMappingCatalog, resolveMappedProjectPlans, } from './planCatalog.service.js';
6
+ const STRIPE_TEST_PUBLISHABLE_KEY = ((_b = (_a = runtimeEnvConfig.stripeTestPublishableKey) !== null && _a !== void 0 ? _a : runtimeEnvConfig.stripePublishableKey) !== null && _b !== void 0 ? _b : '').trim();
7
+ const STRIPE_LIVE_PUBLISHABLE_KEY = ((_d = (_c = runtimeEnvConfig.stripeLivePublishableKey) !== null && _c !== void 0 ? _c : runtimeEnvConfig.stripePublishableKey) !== null && _d !== void 0 ? _d : '').trim();
7
8
  const stripePromiseByKey = new Map();
8
9
  const syncedPlansPromiseByMode = new Map();
9
- const fallbackPaywallPlans = billingPlanList.map((plan) => (Object.assign(Object.assign({}, plan), { source: 'config' })));
10
+ export const buildPaywallPlanResolutionSignature = (options) => {
11
+ var _a, _b, _c, _d;
12
+ const locale = ((_a = options === null || options === void 0 ? void 0 : options.locale) === null || _a === void 0 ? void 0 : _a.trim()) || '';
13
+ const pricing = options === null || options === void 0 ? void 0 : options.pricing;
14
+ const countryCode = ((_b = pricing === null || pricing === void 0 ? void 0 : pricing.countryCode) === null || _b === void 0 ? void 0 : _b.trim()) || '';
15
+ const defaultOfferKey = ((_c = pricing === null || pricing === void 0 ? void 0 : pricing.defaultOfferKey) === null || _c === void 0 ? void 0 : _c.trim()) || '';
16
+ const offerOrder = ((_d = pricing === null || pricing === void 0 ? void 0 : pricing.offerOrder) === null || _d === void 0 ? void 0 : _d.map((offerKey) => offerKey.trim()).join('|')) || '';
17
+ return [locale, countryCode, defaultOfferKey, offerOrder].join('::');
18
+ };
10
19
  const isRecord = (value) => {
11
20
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
12
21
  };
@@ -17,6 +26,35 @@ const asTrimmedStringOrNull = (value) => {
17
26
  const trimmed = value.trim();
18
27
  return trimmed || null;
19
28
  };
29
+ const trimTrailingSlash = (value) => {
30
+ return value.replace(/\/+$/, '');
31
+ };
32
+ const toNormalizedPath = (path) => {
33
+ return path.startsWith('/') ? path : `/${path}`;
34
+ };
35
+ const resolveStripeRuntimeConfig = (runtimeConfig) => {
36
+ var _a, _b, _c;
37
+ return {
38
+ apiBaseUrl: asTrimmedStringOrNull(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl),
39
+ funnelId: (_a = asTrimmedStringOrNull(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId)) !== null && _a !== void 0 ? _a : asTrimmedStringOrNull(FUNNEL_ID),
40
+ funnelVersionId: (_b = asTrimmedStringOrNull(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelVersionId)) !== null && _b !== void 0 ? _b : asTrimmedStringOrNull(runtimeEnvConfig.funnelVersionId),
41
+ funnelSdkPublishableKey: resolvePreviewPaymentPublishableKey((_c = asTrimmedStringOrNull(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)) !== null && _c !== void 0 ? _c : asTrimmedStringOrNull(FUNNEL_SDK_PUBLISHABLE_KEY)) || null,
42
+ };
43
+ };
44
+ const buildStripeRuntimeApiUrl = (path, runtimeConfig) => {
45
+ const { apiBaseUrl } = resolveStripeRuntimeConfig(runtimeConfig);
46
+ if (!apiBaseUrl) {
47
+ return buildMainApiUrl(path);
48
+ }
49
+ return `${trimTrailingSlash(apiBaseUrl)}${toNormalizedPath(path)}`;
50
+ };
51
+ const buildStripeRuntimeHeaders = (runtimeConfig, headers) => {
52
+ const { funnelSdkPublishableKey } = resolveStripeRuntimeConfig(runtimeConfig);
53
+ if (!funnelSdkPublishableKey) {
54
+ return Object.assign({}, (headers !== null && headers !== void 0 ? headers : {}));
55
+ }
56
+ return Object.assign({ 'x-sdk-publishable-key': funnelSdkPublishableKey }, (headers !== null && headers !== void 0 ? headers : {}));
57
+ };
20
58
  const asFiniteNumberOrNull = (value) => {
21
59
  if (typeof value !== 'number' || !Number.isFinite(value)) {
22
60
  return null;
@@ -30,6 +68,56 @@ const asPositiveIntOrNull = (value) => {
30
68
  }
31
69
  return Math.round(numeric);
32
70
  };
71
+ const asBillingPlanIntervalOrNull = (value) => {
72
+ var _a;
73
+ const normalized = (_a = asTrimmedStringOrNull(value)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
74
+ if (normalized !== 'day' &&
75
+ normalized !== 'week' &&
76
+ normalized !== 'month' &&
77
+ normalized !== 'year') {
78
+ return null;
79
+ }
80
+ return normalized;
81
+ };
82
+ const parseBillingIntervalFromText = (value) => {
83
+ const normalized = (value === null || value === void 0 ? void 0 : value.trim().toLowerCase()) || '';
84
+ if (!normalized) {
85
+ return null;
86
+ }
87
+ const explicitMatch = normalized.match(/(?:^|[^a-z])(\d+)?\s*-?\s*(day|week|month|year)s?(?:[^a-z]|$)/);
88
+ if (explicitMatch === null || explicitMatch === void 0 ? void 0 : explicitMatch[2]) {
89
+ return {
90
+ interval: explicitMatch[2],
91
+ intervalCount: explicitMatch[1] ? Number(explicitMatch[1]) : 1,
92
+ };
93
+ }
94
+ const slashMatch = normalized.match(/\/\s*(day|week|month|year)s?\b/);
95
+ if (slashMatch === null || slashMatch === void 0 ? void 0 : slashMatch[1]) {
96
+ return {
97
+ interval: slashMatch[1],
98
+ intervalCount: 1,
99
+ };
100
+ }
101
+ return null;
102
+ };
103
+ const resolveConfigBillingInterval = (plan) => {
104
+ var _a, _b, _c, _d;
105
+ const inferred = (_a = parseBillingIntervalFromText(plan.priceLabel)) !== null && _a !== void 0 ? _a : parseBillingIntervalFromText(plan.title);
106
+ const interval = (_b = plan.billingInterval) !== null && _b !== void 0 ? _b : inferred === null || inferred === void 0 ? void 0 : inferred.interval;
107
+ if (!interval) {
108
+ return {};
109
+ }
110
+ return {
111
+ interval,
112
+ intervalCount: (_d = (_c = plan.billingIntervalCount) !== null && _c !== void 0 ? _c : inferred === null || inferred === void 0 ? void 0 : inferred.intervalCount) !== null && _d !== void 0 ? _d : 1,
113
+ };
114
+ };
115
+ const toConfigPaywallPlan = (key, plan) => {
116
+ var _a, _b, _c;
117
+ const interval = resolveConfigBillingInterval(plan);
118
+ return Object.assign(Object.assign({}, plan), { id: ((_a = plan.id) === null || _a === void 0 ? void 0 : _a.trim()) || key.trim() || ((_b = plan.providerPlanId) === null || _b === void 0 ? void 0 : _b.trim()) || plan.projectPlanId.trim(), perDayLabel: ((_c = plan.perDayLabel) === null || _c === void 0 ? void 0 : _c.trim()) || 'per day', billingInterval: interval.interval, billingIntervalCount: interval.intervalCount, source: 'config' });
119
+ };
120
+ const fallbackPaywallPlans = buildConfigPaywallPlans(billingPlanList);
33
121
  const toMinorAmount = (value) => {
34
122
  const numeric = asFiniteNumberOrNull(value);
35
123
  if (numeric === null || numeric <= 0) {
@@ -37,6 +125,9 @@ const toMinorAmount = (value) => {
37
125
  }
38
126
  return Math.round(numeric);
39
127
  };
128
+ const toPlainObject = (value) => {
129
+ return isRecord(value) ? value : {};
130
+ };
40
131
  const toCurrencyCode = (value) => {
41
132
  const raw = asTrimmedStringOrNull(value);
42
133
  if (!raw) {
@@ -44,16 +135,20 @@ const toCurrencyCode = (value) => {
44
135
  }
45
136
  return raw.toUpperCase();
46
137
  };
47
- const formatMoneyFromMinor = (amountMinor, currencyCode) => {
48
- return new Intl.NumberFormat('en-US', {
138
+ const resolveFormattingLocale = (locale) => {
139
+ const trimmedLocale = typeof locale === 'string' ? locale.trim() : '';
140
+ return trimmedLocale || 'en-US';
141
+ };
142
+ const formatMoneyFromMinor = (amountMinor, currencyCode, locale) => {
143
+ return new Intl.NumberFormat(resolveFormattingLocale(locale), {
49
144
  style: 'currency',
50
145
  currency: currencyCode,
51
146
  minimumFractionDigits: 2,
52
147
  maximumFractionDigits: 2,
53
148
  }).format(amountMinor / 100);
54
149
  };
55
- const formatMoneyFromMajor = (amountMajor, currencyCode) => {
56
- return new Intl.NumberFormat('en-US', {
150
+ const formatMoneyFromMajor = (amountMajor, currencyCode, locale) => {
151
+ return new Intl.NumberFormat(resolveFormattingLocale(locale), {
57
152
  style: 'currency',
58
153
  currency: currencyCode,
59
154
  minimumFractionDigits: 2,
@@ -91,36 +186,80 @@ const getIntervalDays = (interval, intervalCount) => {
91
186
  }
92
187
  return null;
93
188
  };
94
- const toPlanFromSynced = (rawPlan) => {
95
- const planId = asTrimmedStringOrNull(rawPlan.providerPlanId);
189
+ const reorderPlans = (plans, pricing) => {
190
+ if (!pricing || pricing.offerOrder.length === 0) {
191
+ return plans;
192
+ }
193
+ const orderByPlanId = new Map(pricing.offerOrder.map((planId, index) => [planId, index]));
194
+ return [...plans].sort((left, right) => {
195
+ const leftIndex = orderByPlanId.get(left.id);
196
+ const rightIndex = orderByPlanId.get(right.id);
197
+ if (leftIndex === undefined && rightIndex === undefined) {
198
+ return 0;
199
+ }
200
+ if (leftIndex === undefined) {
201
+ return 1;
202
+ }
203
+ if (rightIndex === undefined) {
204
+ return -1;
205
+ }
206
+ return leftIndex - rightIndex;
207
+ });
208
+ };
209
+ const toRemoteProjectBillingPlan = (rawPlan) => {
210
+ var _a, _b;
211
+ const providerPlanId = asTrimmedStringOrNull(rawPlan.providerPlanId);
212
+ if (!providerPlanId) {
213
+ return null;
214
+ }
215
+ return {
216
+ id: asTrimmedStringOrNull(rawPlan.id) || providerPlanId,
217
+ key: asTrimmedStringOrNull(rawPlan.key) || providerPlanId,
218
+ projectPlanId: asTrimmedStringOrNull(rawPlan.projectPlanId),
219
+ providerPlanId,
220
+ displayName: asTrimmedStringOrNull(rawPlan.displayName),
221
+ description: asTrimmedStringOrNull(rawPlan.description),
222
+ interval: (_b = (_a = asTrimmedStringOrNull(rawPlan.interval)) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : null,
223
+ intervalCount: asPositiveIntOrNull(rawPlan.intervalCount),
224
+ trialPeriodDays: asPositiveIntOrNull(rawPlan.trialPeriodDays),
225
+ currency: asTrimmedStringOrNull(rawPlan.currency),
226
+ amountMinor: toMinorAmount(rawPlan.amountMinor),
227
+ amountDecimal: asTrimmedStringOrNull(rawPlan.amountDecimal),
228
+ metadata: toPlainObject(rawPlan.metadata),
229
+ isActive: rawPlan.isActive !== false,
230
+ };
231
+ };
232
+ const toPlanFromRemoteProjectPlan = (rawPlan, options, planIdOverride) => {
233
+ const planId = (planIdOverride === null || planIdOverride === void 0 ? void 0 : planIdOverride.trim()) || rawPlan.id || rawPlan.key || rawPlan.providerPlanId;
96
234
  if (!planId) {
97
235
  return null;
98
236
  }
99
- if (rawPlan.isActive === false) {
237
+ if (!rawPlan.isActive) {
100
238
  return null;
101
239
  }
102
- const amountMinor = toMinorAmount(rawPlan.amountMinor);
240
+ const amountMinor = rawPlan.amountMinor;
103
241
  if (!amountMinor) {
104
242
  return null;
105
243
  }
106
- const metadata = isRecord(rawPlan.metadata) ? rawPlan.metadata : {};
244
+ const metadata = rawPlan.metadata;
107
245
  const currencyCode = toCurrencyCode(rawPlan.currency);
108
- const interval = asTrimmedStringOrNull(rawPlan.interval);
109
- const intervalCount = asPositiveIntOrNull(rawPlan.intervalCount);
246
+ const interval = rawPlan.interval;
247
+ const intervalCount = rawPlan.intervalCount;
110
248
  const intervalLabel = getIntervalLabel(interval, intervalCount);
111
249
  const intervalDays = getIntervalDays(interval, intervalCount);
112
- const title = asTrimmedStringOrNull(rawPlan.displayName) || planId;
250
+ const locale = options === null || options === void 0 ? void 0 : options.locale;
251
+ const title = rawPlan.displayName || planId;
113
252
  const priceLabel = intervalLabel
114
- ? `${formatMoneyFromMinor(amountMinor, currencyCode)}/${intervalLabel}`
115
- : formatMoneyFromMinor(amountMinor, currencyCode);
253
+ ? `${formatMoneyFromMinor(amountMinor, currencyCode, locale)}/${intervalLabel}`
254
+ : formatMoneyFromMinor(amountMinor, currencyCode, locale);
116
255
  const perDayAmount = intervalDays
117
- ? formatMoneyFromMajor(amountMinor / 100 / intervalDays, currencyCode)
118
- : formatMoneyFromMinor(amountMinor, currencyCode);
256
+ ? formatMoneyFromMajor(amountMinor / 100 / intervalDays, currencyCode, locale)
257
+ : formatMoneyFromMinor(amountMinor, currencyCode, locale);
119
258
  return {
120
259
  id: planId,
121
260
  title,
122
- description: asTrimmedStringOrNull(metadata.description) || undefined,
123
- providerPlanId: planId,
261
+ description: rawPlan.description || asTrimmedStringOrNull(metadata.description) || undefined,
262
+ providerPlanId: rawPlan.providerPlanId,
124
263
  priceLabel,
125
264
  oldPriceLabel: asTrimmedStringOrNull(metadata.oldPriceLabel) || undefined,
126
265
  featuredTag: asTrimmedStringOrNull(metadata.featuredTag) || undefined,
@@ -130,19 +269,21 @@ const toPlanFromSynced = (rawPlan) => {
130
269
  comparison: intervalDays
131
270
  ? {
132
271
  perDay: `${perDayAmount}/day`,
133
- perWeek: `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 7, currencyCode)}/week`,
272
+ perWeek: `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 7, currencyCode, locale)}/week`,
134
273
  perMonth: interval === 'month' || interval === 'year'
135
274
  ? `${formatMoneyFromMajor(interval === 'year'
136
275
  ? amountMinor / 100 / ((intervalCount || 1) * 12)
137
- : amountMinor / 100 / (intervalCount || 1), currencyCode)}/month`
138
- : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 30, currencyCode)}/month`,
276
+ : amountMinor / 100 / (intervalCount || 1), currencyCode, locale)}/month`
277
+ : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 30, currencyCode, locale)}/month`,
139
278
  perYear: interval === 'year'
140
- ? `${formatMoneyFromMinor(amountMinor / (intervalCount || 1), currencyCode)}/year`
141
- : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 365, currencyCode)}/year`,
279
+ ? `${formatMoneyFromMinor(amountMinor / (intervalCount || 1), currencyCode, locale)}/year`
280
+ : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 365, currencyCode, locale)}/year`,
142
281
  }
143
282
  : undefined,
144
283
  checkoutOriginalAmountCents: asPositiveIntOrNull(metadata.checkoutOriginalAmountCents) || undefined,
145
284
  checkoutSummaryLabel: asTrimmedStringOrNull(metadata.checkoutSummaryLabel) || undefined,
285
+ billingInterval: asBillingPlanIntervalOrNull(interval) || undefined,
286
+ billingIntervalCount: intervalCount || undefined,
146
287
  source: 'stripe',
147
288
  };
148
289
  };
@@ -156,19 +297,24 @@ const addFeaturedTagFallback = (plans) => {
156
297
  return plans.map((plan, index) => index === 0
157
298
  ? Object.assign(Object.assign({}, plan), { featuredTag: '🔥 Most Popular Choice' }) : plan);
158
299
  };
300
+ export function buildConfigPaywallPlans(planCatalog, orderedPlanIds = []) {
301
+ const plans = Array.isArray(planCatalog)
302
+ ? planCatalog.map((plan, index) => toConfigPaywallPlan(String(index), plan))
303
+ : Object.entries(planCatalog !== null && planCatalog !== void 0 ? planCatalog : {}).map(([key, plan]) => toConfigPaywallPlan(key, plan));
304
+ return getOrderedPaywallPlans(plans, orderedPlanIds);
305
+ }
159
306
  const getStripePublishableKeyFromEnv = (mode) => {
160
307
  if (mode === 'live') {
161
308
  return STRIPE_LIVE_PUBLISHABLE_KEY || STRIPE_TEST_PUBLISHABLE_KEY;
162
309
  }
163
310
  return STRIPE_TEST_PUBLISHABLE_KEY || STRIPE_LIVE_PUBLISHABLE_KEY;
164
311
  };
165
- const toFallbackPaywallPlans = (planCatalog) => {
166
- const sourcePlans = Array.isArray(planCatalog)
167
- ? planCatalog
168
- : planCatalog
169
- ? Object.values(planCatalog)
170
- : billingPlanList;
171
- return sourcePlans.map((plan) => (Object.assign(Object.assign({}, plan), { source: 'config' })));
312
+ const toFallbackPaywallPlans = (planCatalog, options) => {
313
+ const sourcePlans = buildConfigPaywallPlans(planCatalog);
314
+ return reorderPlans(sourcePlans.length > 0 ? sourcePlans : fallbackPaywallPlans, options === null || options === void 0 ? void 0 : options.pricing);
315
+ };
316
+ const isFallbackBillingPlanCatalog = (planCatalog) => {
317
+ return Array.isArray(planCatalog) || !isBillingPlanMappingCatalog(planCatalog);
172
318
  };
173
319
  const parseErrorMessage = async (response, fallbackMessage) => {
174
320
  try {
@@ -192,17 +338,80 @@ const parseErrorMessage = async (response, fallbackMessage) => {
192
338
  }
193
339
  return fallbackMessage;
194
340
  };
195
- export function getFallbackPlans(planCatalog) {
196
- return planCatalog ? toFallbackPaywallPlans(planCatalog) : fallbackPaywallPlans;
341
+ export function getFallbackPlans(planCatalog, options) {
342
+ if (!planCatalog) {
343
+ return reorderPlans(fallbackPaywallPlans, options === null || options === void 0 ? void 0 : options.pricing);
344
+ }
345
+ if (!isFallbackBillingPlanCatalog(planCatalog)) {
346
+ return [];
347
+ }
348
+ return toFallbackPaywallPlans(planCatalog, options);
197
349
  }
198
- export function getDefaultPlanId(plans) {
199
- var _a;
350
+ export function getOrderedPaywallPlans(plans, orderedPlanIds) {
351
+ if (orderedPlanIds.length === 0) {
352
+ return plans;
353
+ }
354
+ const planByKey = new Map();
355
+ for (const plan of plans) {
356
+ for (const key of [plan.id, plan.providerPlanId]) {
357
+ const normalizedKey = key === null || key === void 0 ? void 0 : key.trim();
358
+ if (normalizedKey && !planByKey.has(normalizedKey)) {
359
+ planByKey.set(normalizedKey, plan);
360
+ }
361
+ }
362
+ }
363
+ const selectedPlans = [];
364
+ const selectedPlanIds = new Set();
365
+ for (const planId of orderedPlanIds) {
366
+ const plan = planByKey.get(planId.trim()) || null;
367
+ if (!plan || selectedPlanIds.has(plan.id)) {
368
+ continue;
369
+ }
370
+ selectedPlans.push(plan);
371
+ selectedPlanIds.add(plan.id);
372
+ }
373
+ return selectedPlans;
374
+ }
375
+ const shouldLoadStripePlans = (planCatalog) => {
376
+ if (!planCatalog) {
377
+ return true;
378
+ }
379
+ return !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog);
380
+ };
381
+ export function getDefaultPlanId(plans, pricing) {
382
+ var _a, _b;
200
383
  if (plans.length === 0) {
201
384
  return null;
202
385
  }
203
- return ((_a = plans[0]) === null || _a === void 0 ? void 0 : _a.id) || null;
386
+ const countryDefaultPlanId = (_a = pricing === null || pricing === void 0 ? void 0 : pricing.defaultOfferKey) === null || _a === void 0 ? void 0 : _a.trim();
387
+ if (countryDefaultPlanId && plans.some((plan) => plan.id === countryDefaultPlanId)) {
388
+ return countryDefaultPlanId;
389
+ }
390
+ return ((_b = plans[0]) === null || _b === void 0 ? void 0 : _b.id) || null;
391
+ }
392
+ export function getPaywallPlanSelectionValue(plan) {
393
+ var _a;
394
+ return ((_a = plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || plan.id;
395
+ }
396
+ export function findPaywallPlan(plans, selectionValue) {
397
+ const normalizedSelectionValue = (selectionValue === null || selectionValue === void 0 ? void 0 : selectionValue.trim()) || '';
398
+ if (!normalizedSelectionValue) {
399
+ return null;
400
+ }
401
+ return (plans.find((plan) => {
402
+ var _a;
403
+ const providerPlanId = ((_a = plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
404
+ return plan.id === normalizedSelectionValue || providerPlanId === normalizedSelectionValue;
405
+ }) || null);
204
406
  }
205
- export async function getStripeSyncedPlans(mode) {
407
+ export function getDefaultPlanSelectionValue(plans, pricing) {
408
+ const defaultPlanId = getDefaultPlanId(plans, pricing);
409
+ if (!defaultPlanId) {
410
+ return null;
411
+ }
412
+ return getPaywallPlanSelectionValue(plans.find((plan) => plan.id === defaultPlanId) || plans[0]);
413
+ }
414
+ const getStripeSyncedPlanRecords = async (mode) => {
206
415
  const cacheKey = `${mode}:${FUNNEL_ID || 'no-funnel'}:${FUNNEL_SDK_PUBLISHABLE_KEY}`;
207
416
  const cachedPromise = syncedPlansPromiseByMode.get(cacheKey);
208
417
  if (cachedPromise) {
@@ -224,18 +433,37 @@ export async function getStripeSyncedPlans(mode) {
224
433
  }
225
434
  const payload = (await response.json());
226
435
  const rawPlans = Array.isArray(payload.plans) ? payload.plans : [];
227
- const mappedPlans = rawPlans
228
- .map((raw) => (isRecord(raw) ? toPlanFromSynced(raw) : null))
229
- .filter((plan) => Boolean(plan))
230
- .sort((a, b) => a.amountCents - b.amountCents);
231
- return addFeaturedTagFallback(mappedPlans);
436
+ return rawPlans
437
+ .filter((rawPlan) => isRecord(rawPlan))
438
+ .map((rawPlan) => toRemoteProjectBillingPlan(rawPlan))
439
+ .filter((rawPlan) => Boolean(rawPlan));
232
440
  })();
233
441
  syncedPlansPromiseByMode.set(cacheKey, nextPromise);
234
442
  return nextPromise;
443
+ };
444
+ export async function getStripeSyncedPlans(mode, planCatalog, options) {
445
+ const rawPlans = await getStripeSyncedPlanRecords(mode);
446
+ const mappedPlans = (planCatalog && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog)
447
+ ? resolveMappedProjectPlans(planCatalog, rawPlans).map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options, rawPlan.funnelKey))
448
+ : rawPlans.map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options)))
449
+ .filter((plan) => Boolean(plan))
450
+ .sort((left, right) => left.amountCents - right.amountCents);
451
+ return reorderPlans(addFeaturedTagFallback(mappedPlans), options === null || options === void 0 ? void 0 : options.pricing);
235
452
  }
236
- export async function getPaywallPlans(mode, planCatalog) {
453
+ export function resolvePaywallPlansFromProjectPlans(projectPlans, planCatalog, options) {
454
+ const mappedPlans = (planCatalog && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog)
455
+ ? resolveMappedProjectPlans(planCatalog, projectPlans).map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options, rawPlan.funnelKey))
456
+ : projectPlans.map((rawPlan) => toPlanFromRemoteProjectPlan(rawPlan, options)))
457
+ .filter((plan) => Boolean(plan))
458
+ .sort((left, right) => left.amountCents - right.amountCents);
459
+ return reorderPlans(addFeaturedTagFallback(mappedPlans), options === null || options === void 0 ? void 0 : options.pricing);
460
+ }
461
+ export async function getPaywallPlans(mode, planCatalog, options) {
462
+ if (!shouldLoadStripePlans(planCatalog)) {
463
+ return getFallbackPlans(planCatalog, options);
464
+ }
237
465
  try {
238
- const syncedPlans = await getStripeSyncedPlans(mode);
466
+ const syncedPlans = resolvePaywallPlansFromProjectPlans(await getStripeSyncedPlanRecords(mode), planCatalog, options);
239
467
  if (syncedPlans.length > 0) {
240
468
  return syncedPlans;
241
469
  }
@@ -243,7 +471,7 @@ export async function getPaywallPlans(mode, planCatalog) {
243
471
  catch (_a) {
244
472
  // fall through to config fallback plans
245
473
  }
246
- return getFallbackPlans(planCatalog);
474
+ return getFallbackPlans(planCatalog, options);
247
475
  }
248
476
  export function isStripeConfigured(mode) {
249
477
  return getStripePublishableKeyFromEnv(mode).length > 0 || FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
@@ -260,18 +488,61 @@ export function getStripePromise(mode, runtimePublishableKey) {
260
488
  }
261
489
  return stripePromise;
262
490
  }
491
+ const inferCheckoutPlanRecurringFromText = (value) => {
492
+ const normalized = value === null || value === void 0 ? void 0 : value.trim().toLowerCase();
493
+ if (!normalized) {
494
+ return null;
495
+ }
496
+ const countedMatch = normalized.match(/(\d+)\s*[- ]?(day|week|month|year)s?\b/);
497
+ if (countedMatch) {
498
+ return {
499
+ interval: countedMatch[2],
500
+ intervalCount: Number.parseInt(countedMatch[1] || '1', 10) || 1,
501
+ };
502
+ }
503
+ if (normalized.includes('daily') || normalized.includes('day')) {
504
+ return { interval: 'day', intervalCount: 1 };
505
+ }
506
+ if (normalized.includes('weekly') || normalized.includes('week')) {
507
+ return { interval: 'week', intervalCount: 1 };
508
+ }
509
+ if (normalized.includes('monthly') || normalized.includes('month')) {
510
+ return { interval: 'month', intervalCount: 1 };
511
+ }
512
+ if (normalized.includes('yearly') || normalized.includes('annual') || normalized.includes('year')) {
513
+ return { interval: 'year', intervalCount: 1 };
514
+ }
515
+ return null;
516
+ };
517
+ export const resolveCheckoutPlanRecurringConfig = (plan) => {
518
+ const explicitInterval = asBillingPlanIntervalOrNull(plan.billingInterval);
519
+ if (explicitInterval) {
520
+ return {
521
+ interval: explicitInterval,
522
+ intervalCount: asPositiveIntOrNull(plan.billingIntervalCount) || 1,
523
+ };
524
+ }
525
+ return (inferCheckoutPlanRecurringFromText(plan.title) ||
526
+ inferCheckoutPlanRecurringFromText(plan.providerPlanId) ||
527
+ inferCheckoutPlanRecurringFromText(plan.id) ||
528
+ inferCheckoutPlanRecurringFromText(plan.description));
529
+ };
263
530
  export async function createStripePaymentIntent(input) {
264
- const response = await fetch(buildMainApiUrl('/sdk/public/payments/intents'), {
531
+ var _a, _b;
532
+ const runtimeConfig = resolveStripeRuntimeConfig(input.runtimeConfig);
533
+ const response = await fetch(buildStripeRuntimeApiUrl('/sdk/public/payments/intents', runtimeConfig), {
265
534
  method: 'POST',
266
- headers: buildSdkHeaders({
535
+ headers: buildStripeRuntimeHeaders(runtimeConfig, {
267
536
  'Content-Type': 'application/json',
268
537
  }),
269
538
  body: JSON.stringify({
270
- publishableKey: FUNNEL_SDK_PUBLISHABLE_KEY || undefined,
271
- funnelId: FUNNEL_ID || undefined,
539
+ publishableKey: runtimeConfig.funnelSdkPublishableKey || undefined,
540
+ funnelId: runtimeConfig.funnelId || undefined,
541
+ funnelVersionId: runtimeConfig.funnelVersionId || undefined,
272
542
  planId: input.planId,
273
543
  amountCents: input.amountCents,
274
- externalUserId: input.externalUserId || undefined,
544
+ couponId: ((_a = input.couponId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
545
+ user_id: ((_b = input.user_id) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
275
546
  environment: input.environment || undefined,
276
547
  }),
277
548
  });
@@ -281,3 +552,87 @@ export async function createStripePaymentIntent(input) {
281
552
  }
282
553
  return (await response.json());
283
554
  }
555
+ export async function createStripeSubscriptionCheckout(input) {
556
+ var _a, _b, _c, _d, _e;
557
+ const recurring = resolveCheckoutPlanRecurringConfig(input.plan);
558
+ if (!recurring) {
559
+ throw new Error('Unable to resolve the recurring interval for the selected plan');
560
+ }
561
+ const runtimeConfig = resolveStripeRuntimeConfig(input.runtimeConfig);
562
+ const response = await fetch(buildStripeRuntimeApiUrl('/sdk/public/payments/subscriptions', runtimeConfig), {
563
+ method: 'POST',
564
+ headers: buildStripeRuntimeHeaders(runtimeConfig, {
565
+ 'Content-Type': 'application/json',
566
+ }),
567
+ body: JSON.stringify({
568
+ publishableKey: runtimeConfig.funnelSdkPublishableKey || undefined,
569
+ funnelId: runtimeConfig.funnelId || undefined,
570
+ funnelVersionId: runtimeConfig.funnelVersionId || undefined,
571
+ planId: getPaywallPlanSelectionValue(input.plan),
572
+ providerPlanId: ((_a = input.plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
573
+ title: input.plan.title,
574
+ description: ((_b = input.plan.description) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
575
+ amountCents: input.plan.amountCents,
576
+ billingInterval: recurring.interval,
577
+ billingIntervalCount: recurring.intervalCount,
578
+ couponId: ((_c = input.couponId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
579
+ customerEmail: ((_d = input.customerEmail) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
580
+ user_id: ((_e = input.user_id) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
581
+ environment: input.environment || undefined,
582
+ }),
583
+ });
584
+ if (!response.ok) {
585
+ const errorMessage = await parseErrorMessage(response, 'Unable to create subscription checkout');
586
+ throw new Error(errorMessage);
587
+ }
588
+ return (await response.json());
589
+ }
590
+ export async function createStripeCheckoutSession(input) {
591
+ var _a, _b, _c, _d, _e;
592
+ const recurring = resolveCheckoutPlanRecurringConfig(input.plan);
593
+ if (!recurring) {
594
+ throw new Error('Unable to resolve the recurring interval for the selected plan');
595
+ }
596
+ const runtimeConfig = resolveStripeRuntimeConfig(input.runtimeConfig);
597
+ const response = await fetch(buildStripeRuntimeApiUrl('/sdk/public/payments/checkout-session', runtimeConfig), {
598
+ method: 'POST',
599
+ headers: buildStripeRuntimeHeaders(runtimeConfig, {
600
+ 'Content-Type': 'application/json',
601
+ }),
602
+ body: JSON.stringify({
603
+ publishableKey: runtimeConfig.funnelSdkPublishableKey || undefined,
604
+ funnelId: runtimeConfig.funnelId || undefined,
605
+ funnelVersionId: runtimeConfig.funnelVersionId || undefined,
606
+ planId: getPaywallPlanSelectionValue(input.plan),
607
+ providerPlanId: ((_a = input.plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
608
+ title: input.plan.title,
609
+ description: ((_b = input.plan.description) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
610
+ amountCents: input.plan.amountCents,
611
+ billingInterval: recurring.interval,
612
+ billingIntervalCount: recurring.intervalCount,
613
+ couponId: ((_c = input.couponId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
614
+ successUrl: input.successUrl,
615
+ cancelUrl: input.cancelUrl,
616
+ customerEmail: ((_d = input.customerEmail) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
617
+ user_id: ((_e = input.user_id) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
618
+ environment: input.environment || undefined,
619
+ }),
620
+ });
621
+ if (!response.ok) {
622
+ const errorMessage = await parseErrorMessage(response, 'Unable to create checkout session');
623
+ throw new Error(errorMessage);
624
+ }
625
+ return (await response.json());
626
+ }
627
+ export async function redirectToStripeCheckout(input) {
628
+ var _a;
629
+ const response = await createStripeCheckoutSession(input);
630
+ const checkoutUrl = (_a = response.url) === null || _a === void 0 ? void 0 : _a.trim();
631
+ if (!checkoutUrl) {
632
+ throw new Error('Stripe did not return a checkout url');
633
+ }
634
+ if (typeof window !== 'undefined') {
635
+ window.location.assign(checkoutUrl);
636
+ }
637
+ return checkoutUrl;
638
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -20,13 +20,13 @@
20
20
  "access": "public"
21
21
  },
22
22
  "scripts": {
23
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
23
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-esm-relative-imports.mjs dist",
24
24
  "lint": "eslint .",
25
25
  "prepublishOnly": "npm run build",
26
26
  "test:run": "vitest run --passWithNoTests"
27
27
  },
28
28
  "dependencies": {
29
- "@funnelsgrove/runtime": "0.1.0",
29
+ "@funnelsgrove/runtime": "0.1.2",
30
30
  "@stripe/react-stripe-js": "^5.6.0",
31
31
  "@stripe/stripe-js": "^8.7.0",
32
32
  "react": "19.2.3",