@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.
- package/README.md +32 -3
- package/dist/components/shared/ApplePaySubscribeButton.d.ts +15 -0
- package/dist/components/shared/ApplePaySubscribeButton.js +76 -0
- package/dist/components/shared/CheckoutBrandAssets.d.ts +12 -0
- package/dist/components/shared/CheckoutBrandAssets.js +56 -0
- package/dist/components/shared/GooglePaySubscribeButton.d.ts +14 -0
- package/dist/components/shared/GooglePaySubscribeButton.js +94 -0
- package/dist/components/shared/SharedStripeCheckoutDialog.d.ts +44 -0
- package/dist/components/shared/SharedStripeCheckoutDialog.js +717 -0
- package/dist/components/shared/SharedStripeCheckoutV2Dialog.d.ts +60 -0
- package/dist/components/shared/SharedStripeCheckoutV2Dialog.js +988 -0
- package/dist/components/shared/StripeExpressCheckoutButton.d.ts +18 -0
- package/dist/components/shared/StripeExpressCheckoutButton.js +164 -0
- package/dist/components/shared/StripeExpressCheckoutElement.d.ts +8 -0
- package/dist/components/shared/StripeExpressCheckoutElement.js +19 -0
- package/dist/components/shared/StripePlanSelector.d.ts +5 -7
- package/dist/components/shared/StripePlanSelector.js +16 -57
- package/dist/components/shared/walletPlatform.d.ts +11 -0
- package/dist/components/shared/walletPlatform.js +39 -0
- package/dist/config/billing.config.d.ts +60 -9
- package/dist/config/billing.config.js +7 -2
- package/dist/hooks/useResolvedPaywallPlans.d.ts +15 -0
- package/dist/hooks/useResolvedPaywallPlans.js +68 -0
- package/dist/index.d.ts +21 -5
- package/dist/index.js +21 -5
- package/dist/providers/paymentProvider.types.d.ts +4 -0
- package/dist/providers/paymentProvider.types.js +4 -0
- package/dist/providers/stripe/ApplePaySubscriptionCheckoutSlot.d.ts +5 -0
- package/dist/providers/stripe/ApplePaySubscriptionCheckoutSlot.js +21 -0
- package/dist/providers/stripe/GooglePaySubscriptionCheckoutSlot.d.ts +7 -0
- package/dist/providers/stripe/GooglePaySubscriptionCheckoutSlot.js +44 -0
- package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.d.ts +32 -0
- package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.js +69 -0
- package/dist/providers/stripe/useStripeSubscriptionCheckoutSession.d.ts +30 -0
- package/dist/providers/stripe/useStripeSubscriptionCheckoutSession.js +164 -0
- package/dist/services/paywallOffer.service.d.ts +65 -0
- package/dist/services/paywallOffer.service.js +312 -0
- package/dist/services/planCatalog.service.d.ts +28 -0
- package/dist/services/planCatalog.service.js +60 -0
- package/dist/services/runtimeBillingPlanCatalog.service.d.ts +6 -0
- package/dist/services/runtimeBillingPlanCatalog.service.js +24 -0
- package/dist/services/stripe.service.d.ts +66 -6
- package/dist/services/stripe.service.js +409 -54
- 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
|
-
|
|
6
|
-
const
|
|
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
|
|
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
|
|
48
|
-
|
|
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(
|
|
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
|
|
95
|
-
|
|
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
|
|
237
|
+
if (!rawPlan.isActive) {
|
|
100
238
|
return null;
|
|
101
239
|
}
|
|
102
|
-
const amountMinor =
|
|
240
|
+
const amountMinor = rawPlan.amountMinor;
|
|
103
241
|
if (!amountMinor) {
|
|
104
242
|
return null;
|
|
105
243
|
}
|
|
106
|
-
const metadata =
|
|
244
|
+
const metadata = rawPlan.metadata;
|
|
107
245
|
const currencyCode = toCurrencyCode(rawPlan.currency);
|
|
108
|
-
const interval =
|
|
109
|
-
const 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
|
|
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:
|
|
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 =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
228
|
-
.
|
|
229
|
-
.
|
|
230
|
-
.
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
535
|
+
headers: buildStripeRuntimeHeaders(runtimeConfig, {
|
|
267
536
|
'Content-Type': 'application/json',
|
|
268
537
|
}),
|
|
269
538
|
body: JSON.stringify({
|
|
270
|
-
publishableKey:
|
|
271
|
-
funnelId:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|