@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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AvailablePaymentMethods, StripeExpressCheckoutElementOptions } from '@stripe/stripe-js';
|
|
2
|
+
export type StripeExpressCheckoutButtonProps = {
|
|
3
|
+
amountCents: number;
|
|
4
|
+
beforeConfirm?: () => Promise<string | null>;
|
|
5
|
+
summaryLabel: string;
|
|
6
|
+
returnUrl: string;
|
|
7
|
+
customerEmail?: string | null;
|
|
8
|
+
customerName?: string | null;
|
|
9
|
+
className?: string;
|
|
10
|
+
options?: StripeExpressCheckoutElementOptions;
|
|
11
|
+
availabilityPaymentMethods?: readonly (keyof AvailablePaymentMethods)[];
|
|
12
|
+
initialAvailable?: boolean | null;
|
|
13
|
+
keepInitialAvailableOnReady?: boolean;
|
|
14
|
+
onAvailabilityChange?: (available: boolean) => void;
|
|
15
|
+
onError?: (message: string | null) => void;
|
|
16
|
+
onSuccess?: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, onAvailabilityChange, onError, onSuccess, }: StripeExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { ExpressCheckoutElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
5
|
+
const normalizeBillingName = (input) => {
|
|
6
|
+
var _a, _b, _c;
|
|
7
|
+
const normalizedName = (_a = input.name) === null || _a === void 0 ? void 0 : _a.trim();
|
|
8
|
+
if (normalizedName) {
|
|
9
|
+
return normalizedName;
|
|
10
|
+
}
|
|
11
|
+
const normalizedEmail = (_b = input.email) === null || _b === void 0 ? void 0 : _b.trim();
|
|
12
|
+
if (normalizedEmail) {
|
|
13
|
+
const localPart = (_c = normalizedEmail
|
|
14
|
+
.split('@')[0]) === null || _c === void 0 ? void 0 : _c.replace(/[._-]+/g, ' ').trim();
|
|
15
|
+
if (localPart) {
|
|
16
|
+
return localPart;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return 'Customer';
|
|
20
|
+
};
|
|
21
|
+
const buildDefaultOptions = (input) => {
|
|
22
|
+
return {
|
|
23
|
+
buttonHeight: 55,
|
|
24
|
+
buttonTheme: {
|
|
25
|
+
applePay: 'black',
|
|
26
|
+
},
|
|
27
|
+
buttonType: {
|
|
28
|
+
applePay: 'subscribe',
|
|
29
|
+
},
|
|
30
|
+
emailRequired: true,
|
|
31
|
+
layout: {
|
|
32
|
+
maxColumns: 1,
|
|
33
|
+
maxRows: 1,
|
|
34
|
+
overflow: 'auto',
|
|
35
|
+
},
|
|
36
|
+
lineItems: [
|
|
37
|
+
{
|
|
38
|
+
name: input.summaryLabel.trim() || 'Selected plan',
|
|
39
|
+
amount: input.amountCents,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
paymentMethodOrder: ['apple_pay'],
|
|
43
|
+
paymentMethods: {
|
|
44
|
+
amazonPay: 'never',
|
|
45
|
+
applePay: 'always',
|
|
46
|
+
googlePay: 'never',
|
|
47
|
+
klarna: 'never',
|
|
48
|
+
link: 'never',
|
|
49
|
+
paypal: 'never',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
const isSuccessfulIntentStatus = (status) => {
|
|
54
|
+
return status === 'succeeded' || status === 'processing' || status === 'requires_capture';
|
|
55
|
+
};
|
|
56
|
+
export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, onAvailabilityChange, onError, onSuccess, }) {
|
|
57
|
+
const stripe = useStripe();
|
|
58
|
+
const elements = useElements();
|
|
59
|
+
const [walletAvailable, setWalletAvailable] = useState(initialAvailable !== null && initialAvailable !== void 0 ? initialAvailable : null);
|
|
60
|
+
const effectiveWalletAvailable = initialAvailable === true && walletAvailable !== false
|
|
61
|
+
? true
|
|
62
|
+
: walletAvailable;
|
|
63
|
+
const baseOptions = useMemo(() => buildDefaultOptions({
|
|
64
|
+
amountCents,
|
|
65
|
+
summaryLabel,
|
|
66
|
+
}), [amountCents, summaryLabel]);
|
|
67
|
+
const resolvedOptions = useMemo(() => {
|
|
68
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
69
|
+
return Object.assign(Object.assign(Object.assign({}, baseOptions), (options !== null && options !== void 0 ? options : {})), { buttonTheme: Object.assign(Object.assign({}, ((_a = baseOptions.buttonTheme) !== null && _a !== void 0 ? _a : {})), ((_b = options === null || options === void 0 ? void 0 : options.buttonTheme) !== null && _b !== void 0 ? _b : {})), buttonType: Object.assign(Object.assign({}, ((_c = baseOptions.buttonType) !== null && _c !== void 0 ? _c : {})), ((_d = options === null || options === void 0 ? void 0 : options.buttonType) !== null && _d !== void 0 ? _d : {})), layout: Object.assign(Object.assign({}, ((_e = baseOptions.layout) !== null && _e !== void 0 ? _e : {})), ((_f = options === null || options === void 0 ? void 0 : options.layout) !== null && _f !== void 0 ? _f : {})), paymentMethods: Object.assign(Object.assign({}, ((_g = baseOptions.paymentMethods) !== null && _g !== void 0 ? _g : {})), ((_h = options === null || options === void 0 ? void 0 : options.paymentMethods) !== null && _h !== void 0 ? _h : {})), lineItems: (_j = options === null || options === void 0 ? void 0 : options.lineItems) !== null && _j !== void 0 ? _j : baseOptions.lineItems, paymentMethodOrder: (_k = options === null || options === void 0 ? void 0 : options.paymentMethodOrder) !== null && _k !== void 0 ? _k : baseOptions.paymentMethodOrder });
|
|
70
|
+
}, [baseOptions, options]);
|
|
71
|
+
const handleConfirm = async (event) => {
|
|
72
|
+
var _a;
|
|
73
|
+
if (!stripe || !elements) {
|
|
74
|
+
const message = 'Wallet checkout is not ready yet.';
|
|
75
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
76
|
+
event.paymentFailed({ reason: 'fail', message });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
onError === null || onError === void 0 ? void 0 : onError(null);
|
|
80
|
+
let preparedCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || null;
|
|
81
|
+
if (beforeConfirm) {
|
|
82
|
+
try {
|
|
83
|
+
preparedCustomerEmail = await beforeConfirm();
|
|
84
|
+
}
|
|
85
|
+
catch (confirmError) {
|
|
86
|
+
const message = confirmError instanceof Error && confirmError.message
|
|
87
|
+
? confirmError.message
|
|
88
|
+
: 'Unable to prepare wallet checkout.';
|
|
89
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
90
|
+
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (beforeConfirm && !preparedCustomerEmail) {
|
|
95
|
+
const message = 'Email is required to continue.';
|
|
96
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
97
|
+
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const submitResult = await elements.submit();
|
|
101
|
+
if (submitResult.error) {
|
|
102
|
+
const message = submitResult.error.message || 'Unable to start wallet checkout.';
|
|
103
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
104
|
+
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const result = await stripe.confirmPayment({
|
|
108
|
+
elements,
|
|
109
|
+
confirmParams: {
|
|
110
|
+
payment_method_data: {
|
|
111
|
+
billing_details: {
|
|
112
|
+
email: preparedCustomerEmail || (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || undefined,
|
|
113
|
+
name: normalizeBillingName({
|
|
114
|
+
email: preparedCustomerEmail || customerEmail,
|
|
115
|
+
name: customerName,
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
return_url: returnUrl,
|
|
120
|
+
},
|
|
121
|
+
redirect: 'if_required',
|
|
122
|
+
});
|
|
123
|
+
if (result.error) {
|
|
124
|
+
const message = result.error.message || 'Wallet checkout failed.';
|
|
125
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
126
|
+
event.paymentFailed({ reason: 'fail', message });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
onError === null || onError === void 0 ? void 0 : onError(null);
|
|
130
|
+
if (typeof window !== 'undefined' &&
|
|
131
|
+
isSuccessfulIntentStatus((_a = result.paymentIntent) === null || _a === void 0 ? void 0 : _a.status)) {
|
|
132
|
+
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess();
|
|
133
|
+
window.location.assign(returnUrl);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
|
|
137
|
+
'stripe-express-checkout-button',
|
|
138
|
+
className !== null && className !== void 0 ? className : '',
|
|
139
|
+
effectiveWalletAvailable === false ? 'is-hidden' : '',
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
|
|
143
|
+
setWalletAvailable(false);
|
|
144
|
+
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
|
|
145
|
+
onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
|
|
146
|
+
}, onReady: ({ availablePaymentMethods: availablePaymentMethodsMap }) => {
|
|
147
|
+
const available = Boolean(availabilityPaymentMethods.some((paymentMethod) => availablePaymentMethodsMap === null || availablePaymentMethodsMap === void 0 ? void 0 : availablePaymentMethodsMap[paymentMethod]));
|
|
148
|
+
const resolvedAvailable = available || (keepInitialAvailableOnReady && initialAvailable === true);
|
|
149
|
+
setWalletAvailable(resolvedAvailable);
|
|
150
|
+
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(resolvedAvailable);
|
|
151
|
+
if (resolvedAvailable) {
|
|
152
|
+
onError === null || onError === void 0 ? void 0 : onError(null);
|
|
153
|
+
}
|
|
154
|
+
} }) }), _jsx("style", { children: stripeExpressCheckoutButtonStyles })] }));
|
|
155
|
+
}
|
|
156
|
+
const stripeExpressCheckoutButtonStyles = `
|
|
157
|
+
.stripe-express-checkout-button {
|
|
158
|
+
width: 100%;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.stripe-express-checkout-button.is-hidden {
|
|
162
|
+
display: none;
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Appearance, Stripe } from '@stripe/stripe-js';
|
|
2
|
+
import { type StripeExpressCheckoutButtonProps } from './StripeExpressCheckoutButton.js';
|
|
3
|
+
export type StripeExpressCheckoutElementProps = StripeExpressCheckoutButtonProps & {
|
|
4
|
+
appearance?: Appearance;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
stripePromise: Promise<Stripe | null>;
|
|
7
|
+
};
|
|
8
|
+
export declare function StripeExpressCheckoutElement({ appearance, clientSecret, stripePromise, ...buttonProps }: StripeExpressCheckoutElementProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
14
|
+
import { Elements } from '@stripe/react-stripe-js';
|
|
15
|
+
import { StripeExpressCheckoutButton, } from './StripeExpressCheckoutButton.js';
|
|
16
|
+
export function StripeExpressCheckoutElement(_a) {
|
|
17
|
+
var { appearance, clientSecret, stripePromise } = _a, buttonProps = __rest(_a, ["appearance", "clientSecret", "stripePromise"]);
|
|
18
|
+
return (_jsx(Elements, { stripe: stripePromise, options: Object.assign({ clientSecret }, (appearance ? { appearance } : {})), children: _jsx(StripeExpressCheckoutButton, Object.assign({}, buttonProps)) }));
|
|
19
|
+
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import
|
|
3
|
-
import { type PaywallPlan } from '../../services/stripe.service';
|
|
4
|
-
import type { RuntimeMode } from '@funnelsgrove/runtime';
|
|
2
|
+
import { type PaywallPlan } from '../../services/stripe.service.js';
|
|
5
3
|
type StripePlanSelectorProps = {
|
|
6
|
-
|
|
7
|
-
runtimeMode: RuntimeMode;
|
|
4
|
+
plans?: readonly PaywallPlan[] | null;
|
|
8
5
|
selectedPlanId: string | null;
|
|
9
6
|
onSelectPlan: (planId: string) => void;
|
|
10
|
-
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
error?: string | null;
|
|
11
9
|
className?: string;
|
|
12
10
|
loadingClassName?: string;
|
|
13
11
|
errorClassName?: string;
|
|
@@ -18,5 +16,5 @@ type StripePlanSelectorProps = {
|
|
|
18
16
|
onSelect: () => void;
|
|
19
17
|
}) => ReactNode;
|
|
20
18
|
};
|
|
21
|
-
export declare function StripePlanSelector({
|
|
19
|
+
export declare function StripePlanSelector({ plans, selectedPlanId, onSelectPlan, loading, error, className, loadingClassName, errorClassName, renderPlan, }: StripePlanSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
22
20
|
export {};
|
|
@@ -1,74 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useMemo
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const [error, setError] = useState(null);
|
|
3
|
+
import { useEffect, useMemo } from 'react';
|
|
4
|
+
import { findPaywallPlan, getDefaultPlanSelectionValue, } from '../../services/stripe.service.js';
|
|
5
|
+
export function StripePlanSelector({ plans, selectedPlanId, onSelectPlan, loading = false, error = null, className, loadingClassName, errorClassName, renderPlan, }) {
|
|
6
|
+
const safePlans = useMemo(() => {
|
|
7
|
+
return Array.isArray(plans) ? plans : [];
|
|
8
|
+
}, [plans]);
|
|
10
9
|
useEffect(() => {
|
|
11
|
-
|
|
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) {
|
|
10
|
+
if (safePlans.length === 0) {
|
|
52
11
|
return;
|
|
53
12
|
}
|
|
54
|
-
if (
|
|
13
|
+
if (findPaywallPlan(safePlans, selectedPlanId)) {
|
|
55
14
|
return;
|
|
56
15
|
}
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
onSelectPlan(
|
|
16
|
+
const defaultPlanSelectionValue = getDefaultPlanSelectionValue(safePlans);
|
|
17
|
+
if (defaultPlanSelectionValue) {
|
|
18
|
+
onSelectPlan(defaultPlanSelectionValue);
|
|
60
19
|
}
|
|
61
|
-
}, [onSelectPlan,
|
|
20
|
+
}, [onSelectPlan, safePlans, selectedPlanId]);
|
|
62
21
|
const renderedPlans = useMemo(() => {
|
|
63
|
-
return
|
|
64
|
-
const isSelected = selectedPlanId
|
|
22
|
+
return safePlans.map((plan, index) => {
|
|
23
|
+
const isSelected = findPaywallPlan([plan], selectedPlanId) !== null;
|
|
65
24
|
return renderPlan({
|
|
66
25
|
plan,
|
|
67
26
|
index,
|
|
68
27
|
isSelected,
|
|
69
|
-
onSelect: () => onSelectPlan(plan.id),
|
|
28
|
+
onSelect: () => { var _a; return onSelectPlan(((_a = plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || plan.id); },
|
|
70
29
|
});
|
|
71
30
|
});
|
|
72
|
-
}, [onSelectPlan,
|
|
31
|
+
}, [onSelectPlan, renderPlan, safePlans, selectedPlanId]);
|
|
73
32
|
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
33
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type WalletPlatform = 'android' | 'ios' | 'web';
|
|
2
|
+
export type PlatformWalletPaymentMethod = 'applePay' | 'googlePay';
|
|
3
|
+
type WalletPlatformInput = {
|
|
4
|
+
maxTouchPoints?: number;
|
|
5
|
+
userAgent?: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveWalletPlatform({ maxTouchPoints, userAgent, }: WalletPlatformInput): WalletPlatform;
|
|
8
|
+
export declare function getPlatformWalletPaymentMethods(platform: WalletPlatform): readonly PlatformWalletPaymentMethod[];
|
|
9
|
+
export declare function getStripeWalletPaymentMethodOrder(walletPaymentMethods: readonly PlatformWalletPaymentMethod[]): string[];
|
|
10
|
+
export declare function usePlatformWalletPaymentMethods(): readonly PlatformWalletPaymentMethod[];
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useSyncExternalStore } from 'react';
|
|
3
|
+
export function resolveWalletPlatform({ maxTouchPoints = 0, userAgent, }) {
|
|
4
|
+
var _a;
|
|
5
|
+
const normalizedUserAgent = (_a = userAgent === null || userAgent === void 0 ? void 0 : userAgent.toLowerCase()) !== null && _a !== void 0 ? _a : '';
|
|
6
|
+
if (normalizedUserAgent.includes('android')) {
|
|
7
|
+
return 'android';
|
|
8
|
+
}
|
|
9
|
+
if (/iphone|ipad|ipod/.test(normalizedUserAgent) ||
|
|
10
|
+
(normalizedUserAgent.includes('macintosh') && maxTouchPoints > 1)) {
|
|
11
|
+
return 'ios';
|
|
12
|
+
}
|
|
13
|
+
return 'web';
|
|
14
|
+
}
|
|
15
|
+
export function getPlatformWalletPaymentMethods(platform) {
|
|
16
|
+
if (platform === 'android') {
|
|
17
|
+
return ['googlePay'];
|
|
18
|
+
}
|
|
19
|
+
if (platform === 'ios') {
|
|
20
|
+
return ['applePay'];
|
|
21
|
+
}
|
|
22
|
+
return ['applePay', 'googlePay'];
|
|
23
|
+
}
|
|
24
|
+
export function getStripeWalletPaymentMethodOrder(walletPaymentMethods) {
|
|
25
|
+
return walletPaymentMethods.map((method) => method === 'applePay' ? 'apple_pay' : 'google_pay');
|
|
26
|
+
}
|
|
27
|
+
function getCurrentWalletPlatform() {
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return 'web';
|
|
30
|
+
}
|
|
31
|
+
return resolveWalletPlatform({
|
|
32
|
+
maxTouchPoints: window.navigator.maxTouchPoints,
|
|
33
|
+
userAgent: window.navigator.userAgent,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function usePlatformWalletPaymentMethods() {
|
|
37
|
+
const walletPlatform = useSyncExternalStore(() => () => undefined, getCurrentWalletPlatform, () => 'web');
|
|
38
|
+
return getPlatformWalletPaymentMethods(walletPlatform);
|
|
39
|
+
}
|
|
@@ -4,25 +4,62 @@ export type BillingPlanComparison = {
|
|
|
4
4
|
perMonth: string;
|
|
5
5
|
perYear: string;
|
|
6
6
|
};
|
|
7
|
-
export type
|
|
8
|
-
|
|
7
|
+
export type BillingPlanInterval = 'day' | 'week' | 'month' | 'year';
|
|
8
|
+
export type BillingPlanConfig = {
|
|
9
|
+
projectPlanId: string;
|
|
9
10
|
title: string;
|
|
10
11
|
description?: string;
|
|
11
12
|
providerPlanId?: string;
|
|
12
13
|
priceLabel: string;
|
|
13
|
-
oldPriceLabel?: string;
|
|
14
14
|
featuredTag?: string;
|
|
15
15
|
perDayAmount: string;
|
|
16
|
-
perDayLabel: string;
|
|
17
16
|
amountCents: number;
|
|
17
|
+
checkoutSummaryLabel?: string;
|
|
18
|
+
};
|
|
19
|
+
export type BillingFallbackPlanConfig = BillingPlanConfig & {
|
|
20
|
+
id?: string;
|
|
21
|
+
oldPriceLabel?: string;
|
|
22
|
+
perDayLabel?: string;
|
|
18
23
|
comparison?: BillingPlanComparison;
|
|
19
24
|
checkoutOriginalAmountCents?: number;
|
|
20
|
-
|
|
25
|
+
billingInterval?: BillingPlanInterval;
|
|
26
|
+
billingIntervalCount?: number;
|
|
27
|
+
isDefault?: boolean;
|
|
21
28
|
};
|
|
22
29
|
export type BillingPlanCatalog = Record<string, BillingFallbackPlanConfig>;
|
|
30
|
+
export type BillingPlanMappingConfig = {
|
|
31
|
+
projectPlanId: string;
|
|
32
|
+
};
|
|
33
|
+
export type BillingPlanMappingCatalog = Record<string, BillingPlanMappingConfig>;
|
|
34
|
+
export type BillingPlanInputCatalog = BillingPlanCatalog | BillingPlanMappingCatalog;
|
|
35
|
+
export type BillingFirstDiscountConfig = {
|
|
36
|
+
stage: 'first';
|
|
37
|
+
couponId: string;
|
|
38
|
+
discountPercent: number;
|
|
39
|
+
durationSeconds: number;
|
|
40
|
+
previousDiscountPercent?: null;
|
|
41
|
+
};
|
|
42
|
+
export type BillingSecondDiscountConfig = {
|
|
43
|
+
stage: 'second';
|
|
44
|
+
couponId: string;
|
|
45
|
+
discountPercent: number;
|
|
46
|
+
durationSeconds: number;
|
|
47
|
+
previousDiscountPercent: number;
|
|
48
|
+
};
|
|
49
|
+
export type BillingDiscountCatalog = {
|
|
50
|
+
first: BillingFirstDiscountConfig;
|
|
51
|
+
second: BillingSecondDiscountConfig;
|
|
52
|
+
};
|
|
53
|
+
export type BillingDiscountConfig = BillingFirstDiscountConfig | BillingSecondDiscountConfig;
|
|
54
|
+
export type BillingDiscountList = readonly BillingDiscountConfig[];
|
|
55
|
+
export type BillingConfig = {
|
|
56
|
+
plans: BillingPlanCatalog;
|
|
57
|
+
discounts?: BillingDiscountCatalog | BillingDiscountList;
|
|
58
|
+
};
|
|
23
59
|
export declare const billing: {
|
|
24
60
|
readonly plans: {
|
|
25
|
-
|
|
61
|
+
primary: {
|
|
62
|
+
projectPlanId: string;
|
|
26
63
|
id: string;
|
|
27
64
|
title: string;
|
|
28
65
|
providerPlanId: string;
|
|
@@ -31,14 +68,17 @@ export declare const billing: {
|
|
|
31
68
|
perDayAmount: string;
|
|
32
69
|
perDayLabel: string;
|
|
33
70
|
amountCents: number;
|
|
71
|
+
billingInterval: "month";
|
|
34
72
|
comparison: {
|
|
35
73
|
perDay: string;
|
|
36
74
|
perWeek: string;
|
|
37
75
|
perMonth: string;
|
|
38
76
|
perYear: string;
|
|
39
77
|
};
|
|
78
|
+
isDefault: true;
|
|
40
79
|
};
|
|
41
|
-
|
|
80
|
+
secondary: {
|
|
81
|
+
projectPlanId: string;
|
|
42
82
|
id: string;
|
|
43
83
|
title: string;
|
|
44
84
|
providerPlanId: string;
|
|
@@ -47,6 +87,7 @@ export declare const billing: {
|
|
|
47
87
|
perDayAmount: string;
|
|
48
88
|
perDayLabel: string;
|
|
49
89
|
amountCents: number;
|
|
90
|
+
billingInterval: "year";
|
|
50
91
|
comparison: {
|
|
51
92
|
perDay: string;
|
|
52
93
|
perWeek: string;
|
|
@@ -57,7 +98,8 @@ export declare const billing: {
|
|
|
57
98
|
};
|
|
58
99
|
};
|
|
59
100
|
export declare const billingPlans: {
|
|
60
|
-
|
|
101
|
+
primary: {
|
|
102
|
+
projectPlanId: string;
|
|
61
103
|
id: string;
|
|
62
104
|
title: string;
|
|
63
105
|
providerPlanId: string;
|
|
@@ -66,14 +108,17 @@ export declare const billingPlans: {
|
|
|
66
108
|
perDayAmount: string;
|
|
67
109
|
perDayLabel: string;
|
|
68
110
|
amountCents: number;
|
|
111
|
+
billingInterval: "month";
|
|
69
112
|
comparison: {
|
|
70
113
|
perDay: string;
|
|
71
114
|
perWeek: string;
|
|
72
115
|
perMonth: string;
|
|
73
116
|
perYear: string;
|
|
74
117
|
};
|
|
118
|
+
isDefault: true;
|
|
75
119
|
};
|
|
76
|
-
|
|
120
|
+
secondary: {
|
|
121
|
+
projectPlanId: string;
|
|
77
122
|
id: string;
|
|
78
123
|
title: string;
|
|
79
124
|
providerPlanId: string;
|
|
@@ -82,6 +127,7 @@ export declare const billingPlans: {
|
|
|
82
127
|
perDayAmount: string;
|
|
83
128
|
perDayLabel: string;
|
|
84
129
|
amountCents: number;
|
|
130
|
+
billingInterval: "year";
|
|
85
131
|
comparison: {
|
|
86
132
|
perDay: string;
|
|
87
133
|
perWeek: string;
|
|
@@ -91,6 +137,7 @@ export declare const billingPlans: {
|
|
|
91
137
|
};
|
|
92
138
|
};
|
|
93
139
|
export declare const billingPlanList: ({
|
|
140
|
+
projectPlanId: string;
|
|
94
141
|
id: string;
|
|
95
142
|
title: string;
|
|
96
143
|
providerPlanId: string;
|
|
@@ -99,13 +146,16 @@ export declare const billingPlanList: ({
|
|
|
99
146
|
perDayAmount: string;
|
|
100
147
|
perDayLabel: string;
|
|
101
148
|
amountCents: number;
|
|
149
|
+
billingInterval: "month";
|
|
102
150
|
comparison: {
|
|
103
151
|
perDay: string;
|
|
104
152
|
perWeek: string;
|
|
105
153
|
perMonth: string;
|
|
106
154
|
perYear: string;
|
|
107
155
|
};
|
|
156
|
+
isDefault: true;
|
|
108
157
|
} | {
|
|
158
|
+
projectPlanId: string;
|
|
109
159
|
id: string;
|
|
110
160
|
title: string;
|
|
111
161
|
providerPlanId: string;
|
|
@@ -114,6 +164,7 @@ export declare const billingPlanList: ({
|
|
|
114
164
|
perDayAmount: string;
|
|
115
165
|
perDayLabel: string;
|
|
116
166
|
amountCents: number;
|
|
167
|
+
billingInterval: "year";
|
|
117
168
|
comparison: {
|
|
118
169
|
perDay: string;
|
|
119
170
|
perWeek: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const billing = {
|
|
2
2
|
plans: {
|
|
3
|
-
|
|
3
|
+
primary: {
|
|
4
|
+
projectPlanId: 'price_monthly',
|
|
4
5
|
id: 'monthly',
|
|
5
6
|
title: '1-Month Plan',
|
|
6
7
|
providerPlanId: 'price_monthly',
|
|
@@ -9,14 +10,17 @@ export const billing = {
|
|
|
9
10
|
perDayAmount: '$0.99',
|
|
10
11
|
perDayLabel: 'per day',
|
|
11
12
|
amountCents: 2995,
|
|
13
|
+
billingInterval: 'month',
|
|
12
14
|
comparison: {
|
|
13
15
|
perDay: '$0.99/day',
|
|
14
16
|
perWeek: '$6.99/week',
|
|
15
17
|
perMonth: '$29.95/month',
|
|
16
18
|
perYear: '$363.34/year',
|
|
17
19
|
},
|
|
20
|
+
isDefault: true,
|
|
18
21
|
},
|
|
19
|
-
|
|
22
|
+
secondary: {
|
|
23
|
+
projectPlanId: 'price_yearly',
|
|
20
24
|
id: 'yearly',
|
|
21
25
|
title: '1-Year Plan',
|
|
22
26
|
providerPlanId: 'price_yearly',
|
|
@@ -25,6 +29,7 @@ export const billing = {
|
|
|
25
29
|
perDayAmount: '$0.53',
|
|
26
30
|
perDayLabel: 'per day',
|
|
27
31
|
amountCents: 19140,
|
|
32
|
+
billingInterval: 'year',
|
|
28
33
|
comparison: {
|
|
29
34
|
perDay: '$0.53/day',
|
|
30
35
|
perWeek: '$3.67/week',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RuntimeMode } from '@funnelsgrove/runtime';
|
|
2
|
+
import type { BillingFallbackPlanConfig, BillingPlanInputCatalog } from '../config/billing.config.js';
|
|
3
|
+
import { type PaywallPlan, type PaywallPlanResolutionOptions } from '../services/stripe.service.js';
|
|
4
|
+
type UseResolvedPaywallPlansInput = {
|
|
5
|
+
runtimeMode: RuntimeMode;
|
|
6
|
+
planCatalog?: BillingPlanInputCatalog | readonly BillingFallbackPlanConfig[];
|
|
7
|
+
resolutionOptions?: PaywallPlanResolutionOptions;
|
|
8
|
+
previewPlans?: readonly PaywallPlan[] | null;
|
|
9
|
+
};
|
|
10
|
+
export declare const useResolvedPaywallPlans: ({ runtimeMode, planCatalog, resolutionOptions, previewPlans, }: UseResolvedPaywallPlansInput) => {
|
|
11
|
+
plans: readonly PaywallPlan[];
|
|
12
|
+
loading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { isPreviewFrameRuntime } from '@funnelsgrove/runtime';
|
|
4
|
+
import { getFallbackPlans, getPaywallPlans, } from '../services/stripe.service.js';
|
|
5
|
+
import { isBillingPlanMappingCatalog } from '../services/planCatalog.service.js';
|
|
6
|
+
export const useResolvedPaywallPlans = ({ runtimeMode, planCatalog, resolutionOptions, previewPlans, }) => {
|
|
7
|
+
const fallbackPlans = useMemo(() => getFallbackPlans(planCatalog, resolutionOptions), [planCatalog, resolutionOptions]);
|
|
8
|
+
const isMappingCatalog = Boolean(planCatalog) && !Array.isArray(planCatalog) && isBillingPlanMappingCatalog(planCatalog);
|
|
9
|
+
const shouldLoadRemotePlans = !planCatalog || isMappingCatalog;
|
|
10
|
+
const [plans, setPlans] = useState(() => (previewPlans === null || previewPlans === void 0 ? void 0 : previewPlans.length) ? previewPlans : fallbackPlans);
|
|
11
|
+
const [loading, setLoading] = useState(shouldLoadRemotePlans);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (previewPlans === null || previewPlans === void 0 ? void 0 : previewPlans.length) {
|
|
15
|
+
setPlans(previewPlans);
|
|
16
|
+
setLoading(false);
|
|
17
|
+
setError(null);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!shouldLoadRemotePlans || (isPreviewFrameRuntime() && !isMappingCatalog)) {
|
|
21
|
+
setPlans(fallbackPlans);
|
|
22
|
+
setLoading(false);
|
|
23
|
+
setError(null);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let isActive = true;
|
|
27
|
+
const load = async () => {
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const nextPlans = await getPaywallPlans(runtimeMode, planCatalog, resolutionOptions);
|
|
32
|
+
if (!isActive) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setPlans(nextPlans);
|
|
36
|
+
}
|
|
37
|
+
catch (nextError) {
|
|
38
|
+
if (!isActive) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setPlans(fallbackPlans);
|
|
42
|
+
setError(nextError instanceof Error ? nextError.message : 'Unable to load plans');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (isActive) {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
void load();
|
|
51
|
+
return () => {
|
|
52
|
+
isActive = false;
|
|
53
|
+
};
|
|
54
|
+
}, [
|
|
55
|
+
fallbackPlans,
|
|
56
|
+
isMappingCatalog,
|
|
57
|
+
planCatalog,
|
|
58
|
+
previewPlans,
|
|
59
|
+
resolutionOptions,
|
|
60
|
+
runtimeMode,
|
|
61
|
+
shouldLoadRemotePlans,
|
|
62
|
+
]);
|
|
63
|
+
return {
|
|
64
|
+
plans,
|
|
65
|
+
loading,
|
|
66
|
+
error,
|
|
67
|
+
};
|
|
68
|
+
};
|