@funnelsgrove/payments 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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @funnelsgrove/payments
2
+
3
+ Shared billing and checkout helpers for funnels.
4
+
5
+ ## Build And Publish
6
+
7
+ - Build distributable output with `npm run build --workspace @funnelsgrove/payments`.
8
+ - Publish from the repo root with `npm publish --workspace @funnelsgrove/payments --access public`.
9
+ - The package pins `@funnelsgrove/runtime` to the same exact version and the build resolves against the runtime package's emitted `dist` contract.
10
+
11
+ ## Use This Package For
12
+
13
+ - Stripe plan loading and checkout integration
14
+ - global funnel billing catalog contracts
15
+ - reusable plan-selector and checkout UI
16
+ - billing catalog types and runtime helpers
17
+
18
+ ## Responsibilities
19
+
20
+ - keep paywall behavior consistent across funnels
21
+ - define the shared billing types used by funnel-local catalogs
22
+ - expose one ordered funnel-local billing catalog contract
23
+
24
+ ## What Belongs Here
25
+
26
+ - billing catalog types and normalization helpers
27
+ - Stripe SDK wiring
28
+ - checkout/plan-selector components
29
+
30
+ ## What Does Not Belong Here
31
+
32
+ - funnel routing
33
+ - funnel analytics
34
+ - step-specific copy or layout
35
+ - funnel-owned billing catalog entries
36
+ - manifest validation
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { BillingFallbackPlanConfig, BillingPlanCatalog } from '../../config/billing.config';
3
+ import { type PaywallPlan } from '../../services/stripe.service';
4
+ import type { RuntimeMode } from '@funnelsgrove/runtime';
5
+ type StripePlanSelectorProps = {
6
+ planCatalog?: BillingPlanCatalog | readonly BillingFallbackPlanConfig[];
7
+ runtimeMode: RuntimeMode;
8
+ selectedPlanId: string | null;
9
+ onSelectPlan: (planId: string) => void;
10
+ onPlansLoaded?: (plans: readonly PaywallPlan[]) => void;
11
+ className?: string;
12
+ loadingClassName?: string;
13
+ errorClassName?: string;
14
+ renderPlan: (input: {
15
+ plan: PaywallPlan;
16
+ index: number;
17
+ isSelected: boolean;
18
+ onSelect: () => void;
19
+ }) => ReactNode;
20
+ };
21
+ export declare function StripePlanSelector({ planCatalog, runtimeMode, selectedPlanId, onSelectPlan, onPlansLoaded, className, loadingClassName, errorClassName, renderPlan, }: StripePlanSelectorProps): import("react/jsx-runtime").JSX.Element;
22
+ export {};
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { getDefaultPlanId, getPaywallPlans, getFallbackPlans, } from '../../services/stripe.service';
5
+ import { isPreviewFrameRuntime } from '@funnelsgrove/runtime';
6
+ export function StripePlanSelector({ planCatalog, runtimeMode, selectedPlanId, onSelectPlan, onPlansLoaded, className, loadingClassName, errorClassName, renderPlan, }) {
7
+ const [plans, setPlans] = useState(() => getFallbackPlans(planCatalog));
8
+ const [loading, setLoading] = useState(true);
9
+ const [error, setError] = useState(null);
10
+ useEffect(() => {
11
+ let isActive = true;
12
+ const load = async () => {
13
+ setLoading(true);
14
+ setError(null);
15
+ if (isPreviewFrameRuntime()) {
16
+ const fallbackPlans = getFallbackPlans(planCatalog);
17
+ setPlans(fallbackPlans);
18
+ onPlansLoaded === null || onPlansLoaded === void 0 ? void 0 : onPlansLoaded(fallbackPlans);
19
+ setLoading(false);
20
+ return;
21
+ }
22
+ try {
23
+ const nextPlans = await getPaywallPlans(runtimeMode, planCatalog);
24
+ if (!isActive) {
25
+ return;
26
+ }
27
+ setPlans(nextPlans);
28
+ onPlansLoaded === null || onPlansLoaded === void 0 ? void 0 : onPlansLoaded(nextPlans);
29
+ }
30
+ catch (nextError) {
31
+ if (!isActive) {
32
+ return;
33
+ }
34
+ const fallbackPlans = getFallbackPlans(planCatalog);
35
+ setPlans(fallbackPlans);
36
+ onPlansLoaded === null || onPlansLoaded === void 0 ? void 0 : onPlansLoaded(fallbackPlans);
37
+ setError(nextError instanceof Error ? nextError.message : 'Unable to load plans');
38
+ }
39
+ finally {
40
+ if (isActive) {
41
+ setLoading(false);
42
+ }
43
+ }
44
+ };
45
+ void load();
46
+ return () => {
47
+ isActive = false;
48
+ };
49
+ }, [onPlansLoaded, planCatalog, runtimeMode]);
50
+ useEffect(() => {
51
+ if (plans.length === 0) {
52
+ return;
53
+ }
54
+ if (selectedPlanId && plans.some((plan) => plan.id === selectedPlanId)) {
55
+ return;
56
+ }
57
+ const defaultPlanId = getDefaultPlanId(plans);
58
+ if (defaultPlanId) {
59
+ onSelectPlan(defaultPlanId);
60
+ }
61
+ }, [onSelectPlan, plans, selectedPlanId]);
62
+ const renderedPlans = useMemo(() => {
63
+ return plans.map((plan, index) => {
64
+ const isSelected = selectedPlanId === plan.id;
65
+ return renderPlan({
66
+ plan,
67
+ index,
68
+ isSelected,
69
+ onSelect: () => onSelectPlan(plan.id),
70
+ });
71
+ });
72
+ }, [onSelectPlan, plans, renderPlan, selectedPlanId]);
73
+ return (_jsxs(_Fragment, { children: [loading ? _jsx("p", { className: loadingClassName, children: "Loading plans..." }) : null, error ? _jsx("p", { className: errorClassName, children: error }) : null, _jsx("div", { className: className, children: renderedPlans })] }));
74
+ }
@@ -0,0 +1,123 @@
1
+ export type BillingPlanComparison = {
2
+ perDay: string;
3
+ perWeek: string;
4
+ perMonth: string;
5
+ perYear: string;
6
+ };
7
+ export type BillingFallbackPlanConfig = {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ providerPlanId?: string;
12
+ priceLabel: string;
13
+ oldPriceLabel?: string;
14
+ featuredTag?: string;
15
+ perDayAmount: string;
16
+ perDayLabel: string;
17
+ amountCents: number;
18
+ comparison?: BillingPlanComparison;
19
+ checkoutOriginalAmountCents?: number;
20
+ checkoutSummaryLabel?: string;
21
+ };
22
+ export type BillingPlanCatalog = Record<string, BillingFallbackPlanConfig>;
23
+ export declare const billing: {
24
+ readonly plans: {
25
+ firstPlan: {
26
+ id: string;
27
+ title: string;
28
+ providerPlanId: string;
29
+ priceLabel: string;
30
+ featuredTag: string;
31
+ perDayAmount: string;
32
+ perDayLabel: string;
33
+ amountCents: number;
34
+ comparison: {
35
+ perDay: string;
36
+ perWeek: string;
37
+ perMonth: string;
38
+ perYear: string;
39
+ };
40
+ };
41
+ secondPlan: {
42
+ id: string;
43
+ title: string;
44
+ providerPlanId: string;
45
+ priceLabel: string;
46
+ oldPriceLabel: string;
47
+ perDayAmount: string;
48
+ perDayLabel: string;
49
+ amountCents: number;
50
+ comparison: {
51
+ perDay: string;
52
+ perWeek: string;
53
+ perMonth: string;
54
+ perYear: string;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ export declare const billingPlans: {
60
+ firstPlan: {
61
+ id: string;
62
+ title: string;
63
+ providerPlanId: string;
64
+ priceLabel: string;
65
+ featuredTag: string;
66
+ perDayAmount: string;
67
+ perDayLabel: string;
68
+ amountCents: number;
69
+ comparison: {
70
+ perDay: string;
71
+ perWeek: string;
72
+ perMonth: string;
73
+ perYear: string;
74
+ };
75
+ };
76
+ secondPlan: {
77
+ id: string;
78
+ title: string;
79
+ providerPlanId: string;
80
+ priceLabel: string;
81
+ oldPriceLabel: string;
82
+ perDayAmount: string;
83
+ perDayLabel: string;
84
+ amountCents: number;
85
+ comparison: {
86
+ perDay: string;
87
+ perWeek: string;
88
+ perMonth: string;
89
+ perYear: string;
90
+ };
91
+ };
92
+ };
93
+ export declare const billingPlanList: ({
94
+ id: string;
95
+ title: string;
96
+ providerPlanId: string;
97
+ priceLabel: string;
98
+ featuredTag: string;
99
+ perDayAmount: string;
100
+ perDayLabel: string;
101
+ amountCents: number;
102
+ comparison: {
103
+ perDay: string;
104
+ perWeek: string;
105
+ perMonth: string;
106
+ perYear: string;
107
+ };
108
+ } | {
109
+ id: string;
110
+ title: string;
111
+ providerPlanId: string;
112
+ priceLabel: string;
113
+ oldPriceLabel: string;
114
+ perDayAmount: string;
115
+ perDayLabel: string;
116
+ amountCents: number;
117
+ comparison: {
118
+ perDay: string;
119
+ perWeek: string;
120
+ perMonth: string;
121
+ perYear: string;
122
+ };
123
+ })[];
@@ -0,0 +1,38 @@
1
+ export const billing = {
2
+ plans: {
3
+ firstPlan: {
4
+ id: 'monthly',
5
+ title: '1-Month Plan',
6
+ providerPlanId: 'price_monthly',
7
+ priceLabel: '$29.95/month',
8
+ featuredTag: '🔥 Most Popular Choice',
9
+ perDayAmount: '$0.99',
10
+ perDayLabel: 'per day',
11
+ amountCents: 2995,
12
+ comparison: {
13
+ perDay: '$0.99/day',
14
+ perWeek: '$6.99/week',
15
+ perMonth: '$29.95/month',
16
+ perYear: '$363.34/year',
17
+ },
18
+ },
19
+ secondPlan: {
20
+ id: 'yearly',
21
+ title: '1-Year Plan',
22
+ providerPlanId: 'price_yearly',
23
+ priceLabel: '$191.40/year',
24
+ oldPriceLabel: '$359.40',
25
+ perDayAmount: '$0.53',
26
+ perDayLabel: 'per day',
27
+ amountCents: 19140,
28
+ comparison: {
29
+ perDay: '$0.53/day',
30
+ perWeek: '$3.67/week',
31
+ perMonth: '$15.95/month',
32
+ perYear: '$191.40/year',
33
+ },
34
+ },
35
+ },
36
+ };
37
+ export const billingPlans = billing.plans;
38
+ export const billingPlanList = Object.values(billing.plans);
@@ -0,0 +1,5 @@
1
+ export * from './config/billing.config';
2
+ export * from './services/preview-frame.service';
3
+ export * from './services/runtime-mode.service';
4
+ export * from './services/stripe.service';
5
+ export * from './components/shared/StripePlanSelector';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from './config/billing.config';
2
+ export * from './services/preview-frame.service';
3
+ export * from './services/runtime-mode.service';
4
+ export * from './services/stripe.service';
5
+ export * from './components/shared/StripePlanSelector';
@@ -0,0 +1 @@
1
+ export * from '@funnelsgrove/runtime';
@@ -0,0 +1 @@
1
+ export * from '@funnelsgrove/runtime';
@@ -0,0 +1 @@
1
+ export * from '@funnelsgrove/runtime';
@@ -0,0 +1 @@
1
+ export * from '@funnelsgrove/runtime';
@@ -0,0 +1,38 @@
1
+ import { type Stripe } from '@stripe/stripe-js';
2
+ import { type BillingFallbackPlanConfig, type BillingPlanCatalog } from '../config/billing.config';
3
+ import type { RuntimeMode } from '@funnelsgrove/runtime';
4
+ export type PaywallPlan = {
5
+ id: string;
6
+ title: string;
7
+ description?: string;
8
+ providerPlanId?: string;
9
+ priceLabel: string;
10
+ oldPriceLabel?: string;
11
+ featuredTag?: string;
12
+ perDayAmount: string;
13
+ perDayLabel: string;
14
+ amountCents: number;
15
+ comparison?: BillingFallbackPlanConfig['comparison'];
16
+ checkoutOriginalAmountCents?: number;
17
+ checkoutSummaryLabel?: string;
18
+ source: 'stripe' | 'config';
19
+ };
20
+ export declare function getFallbackPlans(planCatalog?: BillingPlanCatalog | readonly BillingFallbackPlanConfig[]): readonly PaywallPlan[];
21
+ export declare function getDefaultPlanId(plans: readonly PaywallPlan[]): string | null;
22
+ export declare function getStripeSyncedPlans(mode: RuntimeMode): Promise<readonly PaywallPlan[]>;
23
+ export declare function getPaywallPlans(mode: RuntimeMode, planCatalog?: BillingPlanCatalog | readonly BillingFallbackPlanConfig[]): Promise<readonly PaywallPlan[]>;
24
+ export declare function isStripeConfigured(mode: RuntimeMode): boolean;
25
+ export declare function getStripePromise(mode: RuntimeMode, runtimePublishableKey?: string | null): Promise<Stripe | null> | null;
26
+ type CreatePaymentIntentInput = {
27
+ planId: string;
28
+ amountCents: number;
29
+ externalUserId?: string | null;
30
+ environment?: RuntimeMode;
31
+ };
32
+ type CreatePaymentIntentResponse = {
33
+ clientSecret: string;
34
+ stripePublishableKey?: string;
35
+ environment?: 'test' | 'live';
36
+ };
37
+ export declare function createStripePaymentIntent(input: CreatePaymentIntentInput): Promise<CreatePaymentIntentResponse>;
38
+ export {};
@@ -0,0 +1,283 @@
1
+ var _a, _b, _c, _d;
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();
7
+ const stripePromiseByKey = new Map();
8
+ const syncedPlansPromiseByMode = new Map();
9
+ const fallbackPaywallPlans = billingPlanList.map((plan) => (Object.assign(Object.assign({}, plan), { source: 'config' })));
10
+ const isRecord = (value) => {
11
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
12
+ };
13
+ const asTrimmedStringOrNull = (value) => {
14
+ if (typeof value !== 'string') {
15
+ return null;
16
+ }
17
+ const trimmed = value.trim();
18
+ return trimmed || null;
19
+ };
20
+ const asFiniteNumberOrNull = (value) => {
21
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
22
+ return null;
23
+ }
24
+ return value;
25
+ };
26
+ const asPositiveIntOrNull = (value) => {
27
+ const numeric = asFiniteNumberOrNull(value);
28
+ if (numeric === null || numeric <= 0) {
29
+ return null;
30
+ }
31
+ return Math.round(numeric);
32
+ };
33
+ const toMinorAmount = (value) => {
34
+ const numeric = asFiniteNumberOrNull(value);
35
+ if (numeric === null || numeric <= 0) {
36
+ return null;
37
+ }
38
+ return Math.round(numeric);
39
+ };
40
+ const toCurrencyCode = (value) => {
41
+ const raw = asTrimmedStringOrNull(value);
42
+ if (!raw) {
43
+ return 'USD';
44
+ }
45
+ return raw.toUpperCase();
46
+ };
47
+ const formatMoneyFromMinor = (amountMinor, currencyCode) => {
48
+ return new Intl.NumberFormat('en-US', {
49
+ style: 'currency',
50
+ currency: currencyCode,
51
+ minimumFractionDigits: 2,
52
+ maximumFractionDigits: 2,
53
+ }).format(amountMinor / 100);
54
+ };
55
+ const formatMoneyFromMajor = (amountMajor, currencyCode) => {
56
+ return new Intl.NumberFormat('en-US', {
57
+ style: 'currency',
58
+ currency: currencyCode,
59
+ minimumFractionDigits: 2,
60
+ maximumFractionDigits: 2,
61
+ }).format(amountMajor);
62
+ };
63
+ const getIntervalLabel = (interval, intervalCount) => {
64
+ if (!interval) {
65
+ return '';
66
+ }
67
+ const normalized = interval.toLowerCase();
68
+ const count = intervalCount && intervalCount > 0 ? intervalCount : 1;
69
+ if (count === 1) {
70
+ return normalized;
71
+ }
72
+ return `${count} ${normalized}s`;
73
+ };
74
+ const getIntervalDays = (interval, intervalCount) => {
75
+ if (!interval) {
76
+ return null;
77
+ }
78
+ const normalized = interval.toLowerCase();
79
+ const count = intervalCount && intervalCount > 0 ? intervalCount : 1;
80
+ if (normalized === 'day') {
81
+ return count;
82
+ }
83
+ if (normalized === 'week') {
84
+ return count * 7;
85
+ }
86
+ if (normalized === 'month') {
87
+ return count * 30;
88
+ }
89
+ if (normalized === 'year') {
90
+ return count * 365;
91
+ }
92
+ return null;
93
+ };
94
+ const toPlanFromSynced = (rawPlan) => {
95
+ const planId = asTrimmedStringOrNull(rawPlan.providerPlanId);
96
+ if (!planId) {
97
+ return null;
98
+ }
99
+ if (rawPlan.isActive === false) {
100
+ return null;
101
+ }
102
+ const amountMinor = toMinorAmount(rawPlan.amountMinor);
103
+ if (!amountMinor) {
104
+ return null;
105
+ }
106
+ const metadata = isRecord(rawPlan.metadata) ? rawPlan.metadata : {};
107
+ const currencyCode = toCurrencyCode(rawPlan.currency);
108
+ const interval = asTrimmedStringOrNull(rawPlan.interval);
109
+ const intervalCount = asPositiveIntOrNull(rawPlan.intervalCount);
110
+ const intervalLabel = getIntervalLabel(interval, intervalCount);
111
+ const intervalDays = getIntervalDays(interval, intervalCount);
112
+ const title = asTrimmedStringOrNull(rawPlan.displayName) || planId;
113
+ const priceLabel = intervalLabel
114
+ ? `${formatMoneyFromMinor(amountMinor, currencyCode)}/${intervalLabel}`
115
+ : formatMoneyFromMinor(amountMinor, currencyCode);
116
+ const perDayAmount = intervalDays
117
+ ? formatMoneyFromMajor(amountMinor / 100 / intervalDays, currencyCode)
118
+ : formatMoneyFromMinor(amountMinor, currencyCode);
119
+ return {
120
+ id: planId,
121
+ title,
122
+ description: asTrimmedStringOrNull(metadata.description) || undefined,
123
+ providerPlanId: planId,
124
+ priceLabel,
125
+ oldPriceLabel: asTrimmedStringOrNull(metadata.oldPriceLabel) || undefined,
126
+ featuredTag: asTrimmedStringOrNull(metadata.featuredTag) || undefined,
127
+ perDayAmount,
128
+ perDayLabel: 'per day',
129
+ amountCents: amountMinor,
130
+ comparison: intervalDays
131
+ ? {
132
+ perDay: `${perDayAmount}/day`,
133
+ perWeek: `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 7, currencyCode)}/week`,
134
+ perMonth: interval === 'month' || interval === 'year'
135
+ ? `${formatMoneyFromMajor(interval === 'year'
136
+ ? amountMinor / 100 / ((intervalCount || 1) * 12)
137
+ : amountMinor / 100 / (intervalCount || 1), currencyCode)}/month`
138
+ : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 30, currencyCode)}/month`,
139
+ perYear: interval === 'year'
140
+ ? `${formatMoneyFromMinor(amountMinor / (intervalCount || 1), currencyCode)}/year`
141
+ : `${formatMoneyFromMajor(amountMinor / 100 / intervalDays * 365, currencyCode)}/year`,
142
+ }
143
+ : undefined,
144
+ checkoutOriginalAmountCents: asPositiveIntOrNull(metadata.checkoutOriginalAmountCents) || undefined,
145
+ checkoutSummaryLabel: asTrimmedStringOrNull(metadata.checkoutSummaryLabel) || undefined,
146
+ source: 'stripe',
147
+ };
148
+ };
149
+ const addFeaturedTagFallback = (plans) => {
150
+ if (plans.length === 0) {
151
+ return plans;
152
+ }
153
+ if (plans.some((plan) => Boolean(plan.featuredTag))) {
154
+ return plans;
155
+ }
156
+ return plans.map((plan, index) => index === 0
157
+ ? Object.assign(Object.assign({}, plan), { featuredTag: '🔥 Most Popular Choice' }) : plan);
158
+ };
159
+ const getStripePublishableKeyFromEnv = (mode) => {
160
+ if (mode === 'live') {
161
+ return STRIPE_LIVE_PUBLISHABLE_KEY || STRIPE_TEST_PUBLISHABLE_KEY;
162
+ }
163
+ return STRIPE_TEST_PUBLISHABLE_KEY || STRIPE_LIVE_PUBLISHABLE_KEY;
164
+ };
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' })));
172
+ };
173
+ const parseErrorMessage = async (response, fallbackMessage) => {
174
+ try {
175
+ const payload = (await response.json());
176
+ const apiError = asTrimmedStringOrNull(payload.error);
177
+ if (apiError) {
178
+ return apiError;
179
+ }
180
+ }
181
+ catch (_a) {
182
+ // ignore JSON parsing errors
183
+ }
184
+ try {
185
+ const errorText = await response.text();
186
+ if (errorText.trim()) {
187
+ return errorText;
188
+ }
189
+ }
190
+ catch (_b) {
191
+ // ignore text parsing errors
192
+ }
193
+ return fallbackMessage;
194
+ };
195
+ export function getFallbackPlans(planCatalog) {
196
+ return planCatalog ? toFallbackPaywallPlans(planCatalog) : fallbackPaywallPlans;
197
+ }
198
+ export function getDefaultPlanId(plans) {
199
+ var _a;
200
+ if (plans.length === 0) {
201
+ return null;
202
+ }
203
+ return ((_a = plans[0]) === null || _a === void 0 ? void 0 : _a.id) || null;
204
+ }
205
+ export async function getStripeSyncedPlans(mode) {
206
+ const cacheKey = `${mode}:${FUNNEL_ID || 'no-funnel'}:${FUNNEL_SDK_PUBLISHABLE_KEY}`;
207
+ const cachedPromise = syncedPlansPromiseByMode.get(cacheKey);
208
+ if (cachedPromise) {
209
+ return cachedPromise;
210
+ }
211
+ const nextPromise = (async () => {
212
+ const url = new URL(buildMainApiUrl('/sdk/public/payments/plans'));
213
+ url.searchParams.set('environment', mode);
214
+ if (FUNNEL_ID) {
215
+ url.searchParams.set('funnelId', FUNNEL_ID);
216
+ }
217
+ const response = await fetch(url.toString(), {
218
+ method: 'GET',
219
+ headers: buildSdkHeaders(),
220
+ });
221
+ if (!response.ok) {
222
+ const errorMessage = await parseErrorMessage(response, 'Unable to load Stripe plans');
223
+ throw new Error(errorMessage);
224
+ }
225
+ const payload = (await response.json());
226
+ 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);
232
+ })();
233
+ syncedPlansPromiseByMode.set(cacheKey, nextPromise);
234
+ return nextPromise;
235
+ }
236
+ export async function getPaywallPlans(mode, planCatalog) {
237
+ try {
238
+ const syncedPlans = await getStripeSyncedPlans(mode);
239
+ if (syncedPlans.length > 0) {
240
+ return syncedPlans;
241
+ }
242
+ }
243
+ catch (_a) {
244
+ // fall through to config fallback plans
245
+ }
246
+ return getFallbackPlans(planCatalog);
247
+ }
248
+ export function isStripeConfigured(mode) {
249
+ return getStripePublishableKeyFromEnv(mode).length > 0 || FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
250
+ }
251
+ export function getStripePromise(mode, runtimePublishableKey) {
252
+ const resolvedPublishableKey = (runtimePublishableKey !== null && runtimePublishableKey !== void 0 ? runtimePublishableKey : getStripePublishableKeyFromEnv(mode)).trim();
253
+ if (!resolvedPublishableKey) {
254
+ return null;
255
+ }
256
+ let stripePromise = stripePromiseByKey.get(resolvedPublishableKey) || null;
257
+ if (!stripePromise) {
258
+ stripePromise = loadStripe(resolvedPublishableKey);
259
+ stripePromiseByKey.set(resolvedPublishableKey, stripePromise);
260
+ }
261
+ return stripePromise;
262
+ }
263
+ export async function createStripePaymentIntent(input) {
264
+ const response = await fetch(buildMainApiUrl('/sdk/public/payments/intents'), {
265
+ method: 'POST',
266
+ headers: buildSdkHeaders({
267
+ 'Content-Type': 'application/json',
268
+ }),
269
+ body: JSON.stringify({
270
+ publishableKey: FUNNEL_SDK_PUBLISHABLE_KEY || undefined,
271
+ funnelId: FUNNEL_ID || undefined,
272
+ planId: input.planId,
273
+ amountCents: input.amountCents,
274
+ externalUserId: input.externalUserId || undefined,
275
+ environment: input.environment || undefined,
276
+ }),
277
+ });
278
+ if (!response.ok) {
279
+ const errorMessage = await parseErrorMessage(response, 'Unable to create payment intent');
280
+ throw new Error(errorMessage);
281
+ }
282
+ return (await response.json());
283
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@funnelsgrove/payments",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
24
+ "lint": "eslint .",
25
+ "prepublishOnly": "npm run build",
26
+ "test:run": "vitest run --passWithNoTests"
27
+ },
28
+ "dependencies": {
29
+ "@funnelsgrove/runtime": "0.1.0",
30
+ "@stripe/react-stripe-js": "^5.6.0",
31
+ "@stripe/stripe-js": "^8.7.0",
32
+ "react": "19.2.3",
33
+ "react-dom": "19.2.3",
34
+ "stripe": "^20.3.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20",
38
+ "@types/react": "^19",
39
+ "@types/react-dom": "^19",
40
+ "typescript": "^5",
41
+ "vitest": "^3.2.4"
42
+ }
43
+ }