@funnelsgrove/payments 0.1.34 → 0.1.36
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/dist/components/shared/PaywallSubscriptionCheckout.d.ts +146 -0
- package/dist/components/shared/PaywallSubscriptionCheckout.js +340 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.d.ts +2 -1
- package/dist/providers/stripe/WalletSubscriptionCheckoutSlot.js +22 -14
- package/dist/providers/stripe/useStripeSubscriptionCheckoutSession.js +25 -6
- package/dist/testing/walletCheckoutSmoke.d.ts +92 -0
- package/dist/testing/walletCheckoutSmoke.js +111 -0
- package/package.json +2 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type FormEvent } from 'react';
|
|
2
|
+
import type { RuntimeMode } from '@funnelsgrove/runtime';
|
|
3
|
+
import type { StripeRuntimeConfigOverrides, PaywallPlan } from '../../services/stripe.service.js';
|
|
4
|
+
import type { PaywallDisplayPlan } from '../../services/paywallOffer.service.js';
|
|
5
|
+
import { type PlatformWalletPaymentMethod } from './walletPlatform.js';
|
|
6
|
+
import { type StripeSubscriptionCheckoutSession } from '../../providers/stripe/useStripeSubscriptionCheckoutSession.js';
|
|
7
|
+
export type PaywallSubscriptionCheckoutContent = {
|
|
8
|
+
title: string;
|
|
9
|
+
closeAriaLabel: string;
|
|
10
|
+
walletButtonLabel: string;
|
|
11
|
+
customerEmailLabel: string;
|
|
12
|
+
customerEmailPlaceholder: string;
|
|
13
|
+
customerEmailInvalidMessage: string;
|
|
14
|
+
walletEmailPromptTitle: string;
|
|
15
|
+
walletEmailPromptDescription: string;
|
|
16
|
+
walletEmailPromptSubmitLabel: string;
|
|
17
|
+
walletEmailPromptCancelLabel: string;
|
|
18
|
+
countdownLabel: string;
|
|
19
|
+
satisfactionPercent: string;
|
|
20
|
+
satisfactionLabel: string;
|
|
21
|
+
originalPriceLabel: string;
|
|
22
|
+
discountRowLabelTemplate: string;
|
|
23
|
+
promoCodeLabel: string;
|
|
24
|
+
savedLabelTemplate: string;
|
|
25
|
+
totalLabel: string;
|
|
26
|
+
cardSubmitLabel: string;
|
|
27
|
+
paypalLabel: string;
|
|
28
|
+
paypalButtonLabel: string;
|
|
29
|
+
processingLabel: string;
|
|
30
|
+
secureLabel: string;
|
|
31
|
+
paymentMethodLabels: readonly string[];
|
|
32
|
+
specialOffer: {
|
|
33
|
+
image?: {
|
|
34
|
+
src?: string;
|
|
35
|
+
alt?: string;
|
|
36
|
+
};
|
|
37
|
+
discountLabel: string;
|
|
38
|
+
title: string;
|
|
39
|
+
description: string;
|
|
40
|
+
buttonLabel: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type PaywallSubscriptionPaymentStackContent = {
|
|
44
|
+
cardButtonLabel: string;
|
|
45
|
+
googlePayPlaceholderLabel: string;
|
|
46
|
+
metaLabels: readonly string[];
|
|
47
|
+
stripeUnavailableNote: string;
|
|
48
|
+
supportText: string;
|
|
49
|
+
walletPlaceholderLabel: string;
|
|
50
|
+
};
|
|
51
|
+
type PaywallCheckoutCloseDiscountState = {
|
|
52
|
+
stage: string;
|
|
53
|
+
status: string;
|
|
54
|
+
};
|
|
55
|
+
export type UsePaywallSubscriptionCheckoutControllerInput = {
|
|
56
|
+
analyticsMetadata?: Record<string, unknown> | null;
|
|
57
|
+
appliedDiscountAmountCents: number;
|
|
58
|
+
checkoutDiscountPercent: number;
|
|
59
|
+
checkoutMode: RuntimeMode;
|
|
60
|
+
checkoutPromoActive: boolean;
|
|
61
|
+
content: PaywallSubscriptionCheckoutContent;
|
|
62
|
+
customerEmailEditable: boolean;
|
|
63
|
+
customerName?: string | null;
|
|
64
|
+
discountState?: PaywallCheckoutCloseDiscountState | null;
|
|
65
|
+
initialCustomerEmail?: string | null;
|
|
66
|
+
onCheckoutCompleted?: () => Promise<void> | void;
|
|
67
|
+
onCheckoutStarted?: () => Promise<void> | void;
|
|
68
|
+
onCustomerEmailCommit: (email: string) => Promise<string>;
|
|
69
|
+
onFirstCheckoutClosed?: () => void;
|
|
70
|
+
plan: PaywallPlan | null;
|
|
71
|
+
returnUrl: string;
|
|
72
|
+
runtimeConfig?: StripeRuntimeConfigOverrides;
|
|
73
|
+
selectedDisplayPlan: PaywallDisplayPlan | null;
|
|
74
|
+
supportEmail: string;
|
|
75
|
+
userId?: string | null;
|
|
76
|
+
getReturnUrl?: (checkoutSessionId?: string | null) => string;
|
|
77
|
+
};
|
|
78
|
+
export type PaywallSubscriptionCheckoutController = {
|
|
79
|
+
checkoutDialogOpen: boolean;
|
|
80
|
+
checkoutDialogReady: boolean;
|
|
81
|
+
checkoutDisabled: boolean;
|
|
82
|
+
checkoutDiscountAmountLabel: string;
|
|
83
|
+
checkoutDiscountLabel: string;
|
|
84
|
+
checkoutInitialWalletAvailable: boolean | null;
|
|
85
|
+
checkoutSavedLabel: string;
|
|
86
|
+
checkoutSession: StripeSubscriptionCheckoutSession;
|
|
87
|
+
checkoutSuccessReturnUrl: string;
|
|
88
|
+
checkoutSummaryLabel: string;
|
|
89
|
+
checkoutTotalValue: string;
|
|
90
|
+
closeCheckoutDialog: () => void;
|
|
91
|
+
commitCheckoutEmail: (draftEmail: string) => Promise<string>;
|
|
92
|
+
customerEmail: string;
|
|
93
|
+
customerEmailEditable: boolean;
|
|
94
|
+
customerName: string;
|
|
95
|
+
error: string | null;
|
|
96
|
+
handleApplePayAvailabilityChange: (available: boolean) => void;
|
|
97
|
+
handleGooglePayAvailabilityChange: (available: boolean) => void;
|
|
98
|
+
openCardCheckoutFromUnavailableWallet: () => Promise<void>;
|
|
99
|
+
preparePaywallWalletCheckout: () => Promise<string | null>;
|
|
100
|
+
setCheckoutEmail: (value: string) => void;
|
|
101
|
+
startCheckout: () => Promise<void>;
|
|
102
|
+
supportEmail: string;
|
|
103
|
+
trackCheckoutCompleted: () => Promise<void>;
|
|
104
|
+
walletEmailPromptCancel: () => void;
|
|
105
|
+
walletEmailPromptDraft: string;
|
|
106
|
+
walletEmailPromptError: string | null;
|
|
107
|
+
walletEmailPromptOpen: boolean;
|
|
108
|
+
walletEmailPromptSubmit: (event: FormEvent<HTMLFormElement>) => Promise<void>;
|
|
109
|
+
walletPaymentMethods: readonly PlatformWalletPaymentMethod[];
|
|
110
|
+
setWalletEmailPromptDraft: (value: string) => void;
|
|
111
|
+
};
|
|
112
|
+
export declare function usePaywallSubscriptionCheckoutController({ analyticsMetadata, appliedDiscountAmountCents, checkoutDiscountPercent, checkoutMode, checkoutPromoActive, content, customerEmailEditable, customerName, discountState, getReturnUrl, initialCustomerEmail, onCheckoutCompleted, onCheckoutStarted, onCustomerEmailCommit, onFirstCheckoutClosed, plan, returnUrl, runtimeConfig, selectedDisplayPlan, supportEmail, userId, }: UsePaywallSubscriptionCheckoutControllerInput): PaywallSubscriptionCheckoutController;
|
|
113
|
+
export type PaywallSubscriptionCheckoutDialogsProps = {
|
|
114
|
+
buttonColor?: string;
|
|
115
|
+
content: PaywallSubscriptionCheckoutContent;
|
|
116
|
+
controller: PaywallSubscriptionCheckoutController;
|
|
117
|
+
discountPercent: number;
|
|
118
|
+
promoActive: boolean;
|
|
119
|
+
promoDisplayName: string;
|
|
120
|
+
remainingSeconds: number;
|
|
121
|
+
selectedDisplayPlan: PaywallDisplayPlan | null;
|
|
122
|
+
specialOfferDialogOpen: boolean;
|
|
123
|
+
themeColor?: string;
|
|
124
|
+
onSpecialOfferAccept: () => void;
|
|
125
|
+
};
|
|
126
|
+
export declare function PaywallSubscriptionCheckoutDialogs({ buttonColor, content, controller, discountPercent, promoActive, promoDisplayName, remainingSeconds, selectedDisplayPlan, specialOfferDialogOpen, themeColor, onSpecialOfferAccept, }: PaywallSubscriptionCheckoutDialogsProps): import("react/jsx-runtime").JSX.Element;
|
|
127
|
+
export type PaywallSubscriptionPaymentStackProps = {
|
|
128
|
+
applePayButtonClassName?: string;
|
|
129
|
+
applePayShellClassName?: string;
|
|
130
|
+
cardButtonClassName?: string;
|
|
131
|
+
className?: string;
|
|
132
|
+
content: PaywallSubscriptionPaymentStackContent;
|
|
133
|
+
controller: PaywallSubscriptionCheckoutController;
|
|
134
|
+
disclaimerClassName?: string;
|
|
135
|
+
errorClassName?: string;
|
|
136
|
+
googlePayButtonClassName?: string;
|
|
137
|
+
googlePayShellClassName?: string;
|
|
138
|
+
metaClassName?: string;
|
|
139
|
+
noteClassName?: string;
|
|
140
|
+
paymentDisclaimer: string;
|
|
141
|
+
selectedDisplayPlan: PaywallDisplayPlan | null;
|
|
142
|
+
supportClassName?: string;
|
|
143
|
+
walletSuspended: boolean;
|
|
144
|
+
};
|
|
145
|
+
export declare function PaywallSubscriptionPaymentStack({ applePayButtonClassName, applePayShellClassName, cardButtonClassName, className, content, controller, disclaimerClassName, errorClassName, googlePayButtonClassName, googlePayShellClassName, metaClassName, noteClassName, paymentDisclaimer, selectedDisplayPlan, supportClassName, walletSuspended, }: PaywallSubscriptionPaymentStackProps): import("react/jsx-runtime").JSX.Element;
|
|
146
|
+
export {};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { ApplePaySubscriptionCheckoutSlot, } from '../../providers/stripe/ApplePaySubscriptionCheckoutSlot.js';
|
|
5
|
+
import { GooglePaySubscriptionCheckoutSlot, } from '../../providers/stripe/GooglePaySubscriptionCheckoutSlot.js';
|
|
6
|
+
import { SharedCheckoutSpecialOfferDialog, SharedStripeCheckoutV2Dialog, } from './SharedStripeCheckoutV2Dialog.js';
|
|
7
|
+
import { usePlatformWalletPaymentMethods } from './walletPlatform.js';
|
|
8
|
+
import { useStripeSubscriptionCheckoutSession, } from '../../providers/stripe/useStripeSubscriptionCheckoutSession.js';
|
|
9
|
+
const checkoutCurrencyFormatter = new Intl.NumberFormat('en-US', {
|
|
10
|
+
currency: 'USD',
|
|
11
|
+
maximumFractionDigits: 2,
|
|
12
|
+
minimumFractionDigits: 2,
|
|
13
|
+
style: 'currency',
|
|
14
|
+
});
|
|
15
|
+
function formatCheckoutAmount(amountCents) {
|
|
16
|
+
return checkoutCurrencyFormatter.format(amountCents / 100);
|
|
17
|
+
}
|
|
18
|
+
function getErrorMessage(error, fallback) {
|
|
19
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
20
|
+
}
|
|
21
|
+
export function usePaywallSubscriptionCheckoutController({ analyticsMetadata, appliedDiscountAmountCents, checkoutDiscountPercent, checkoutMode, checkoutPromoActive, content, customerEmailEditable, customerName, discountState, getReturnUrl, initialCustomerEmail, onCheckoutCompleted, onCheckoutStarted, onCustomerEmailCommit, onFirstCheckoutClosed, plan, returnUrl, runtimeConfig, selectedDisplayPlan, supportEmail, userId, }) {
|
|
22
|
+
var _a, _b, _c, _d;
|
|
23
|
+
const walletPaymentMethods = usePlatformWalletPaymentMethods();
|
|
24
|
+
const [checkoutDialogOpen, setCheckoutDialogOpen] = useState(false);
|
|
25
|
+
const [checkoutEmail, setCheckoutEmail] = useState((_a = initialCustomerEmail === null || initialCustomerEmail === void 0 ? void 0 : initialCustomerEmail.trim()) !== null && _a !== void 0 ? _a : '');
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
const [paywallWalletAvailability, setPaywallWalletAvailability] = useState({
|
|
28
|
+
intentKey: '',
|
|
29
|
+
methods: {},
|
|
30
|
+
});
|
|
31
|
+
const [walletEmailPromptOpen, setWalletEmailPromptOpen] = useState(false);
|
|
32
|
+
const [walletEmailPromptDraft, setWalletEmailPromptDraft] = useState('');
|
|
33
|
+
const [walletEmailPromptError, setWalletEmailPromptError] = useState(null);
|
|
34
|
+
const walletEmailResolverRef = useRef(null);
|
|
35
|
+
const checkoutSummaryLabel = ((_b = selectedDisplayPlan === null || selectedDisplayPlan === void 0 ? void 0 : selectedDisplayPlan.checkoutSummaryLabel) === null || _b === void 0 ? void 0 : _b.trim()) || (selectedDisplayPlan === null || selectedDisplayPlan === void 0 ? void 0 : selectedDisplayPlan.title) || 'Selected plan';
|
|
36
|
+
const checkoutDiscountLabel = checkoutPromoActive
|
|
37
|
+
? content.discountRowLabelTemplate.replace('{percent}', String(checkoutDiscountPercent))
|
|
38
|
+
: '';
|
|
39
|
+
const checkoutDiscountAmountLabel = checkoutPromoActive
|
|
40
|
+
? `-${formatCheckoutAmount(appliedDiscountAmountCents)}`
|
|
41
|
+
: '';
|
|
42
|
+
const checkoutSavedLabel = checkoutPromoActive
|
|
43
|
+
? content.savedLabelTemplate
|
|
44
|
+
.replace('{amount}', formatCheckoutAmount(appliedDiscountAmountCents))
|
|
45
|
+
.replace('{percent}', String(checkoutDiscountPercent))
|
|
46
|
+
: '';
|
|
47
|
+
const checkoutTotalValue = useMemo(() => {
|
|
48
|
+
var _a;
|
|
49
|
+
if (!selectedDisplayPlan) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
return ((_a = selectedDisplayPlan.priceLabel.split('/')[0]) === null || _a === void 0 ? void 0 : _a.trim()) || selectedDisplayPlan.priceLabel;
|
|
53
|
+
}, [selectedDisplayPlan]);
|
|
54
|
+
const successReturnUrl = useMemo(() => (getReturnUrl === null || getReturnUrl === void 0 ? void 0 : getReturnUrl()) || returnUrl, [getReturnUrl, returnUrl]);
|
|
55
|
+
const checkoutSession = useStripeSubscriptionCheckoutSession({
|
|
56
|
+
analyticsMetadata,
|
|
57
|
+
checkoutMode,
|
|
58
|
+
couponId: checkoutPromoActive ? (_c = selectedDisplayPlan === null || selectedDisplayPlan === void 0 ? void 0 : selectedDisplayPlan.couponId) !== null && _c !== void 0 ? _c : null : null,
|
|
59
|
+
customerEmail: checkoutEmail || initialCustomerEmail,
|
|
60
|
+
displayPlan: selectedDisplayPlan,
|
|
61
|
+
onError: setError,
|
|
62
|
+
plan,
|
|
63
|
+
returnUrl: successReturnUrl,
|
|
64
|
+
runtimeConfig,
|
|
65
|
+
userId,
|
|
66
|
+
});
|
|
67
|
+
const checkoutDisabled = !checkoutSession.configured || !plan || !selectedDisplayPlan || !userId;
|
|
68
|
+
const checkoutDialogReady = checkoutDialogOpen &&
|
|
69
|
+
Boolean(checkoutSession.activeClientSecret) &&
|
|
70
|
+
Boolean(checkoutSession.stripePromise) &&
|
|
71
|
+
Boolean(selectedDisplayPlan);
|
|
72
|
+
const checkoutSuccessReturnUrl = useMemo(() => (getReturnUrl === null || getReturnUrl === void 0 ? void 0 : getReturnUrl(checkoutSession.checkoutSessionId)) || returnUrl, [checkoutSession.checkoutSessionId, getReturnUrl, returnUrl]);
|
|
73
|
+
const currentPaywallWalletAvailability = paywallWalletAvailability.intentKey === checkoutSession.intentKey
|
|
74
|
+
? paywallWalletAvailability.methods
|
|
75
|
+
: {};
|
|
76
|
+
const checkoutInitialWalletAvailable = walletPaymentMethods.some((method) => currentPaywallWalletAvailability[method] === true)
|
|
77
|
+
? true
|
|
78
|
+
: null;
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const timeoutId = window.setTimeout(() => {
|
|
81
|
+
var _a;
|
|
82
|
+
setCheckoutEmail((_a = initialCustomerEmail === null || initialCustomerEmail === void 0 ? void 0 : initialCustomerEmail.trim()) !== null && _a !== void 0 ? _a : '');
|
|
83
|
+
}, 0);
|
|
84
|
+
return () => window.clearTimeout(timeoutId);
|
|
85
|
+
}, [initialCustomerEmail]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
return () => {
|
|
88
|
+
var _a;
|
|
89
|
+
(_a = walletEmailResolverRef.current) === null || _a === void 0 ? void 0 : _a.call(walletEmailResolverRef, null);
|
|
90
|
+
walletEmailResolverRef.current = null;
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
const updatePaywallWalletAvailability = (method, available) => {
|
|
94
|
+
setPaywallWalletAvailability((current) => {
|
|
95
|
+
const currentMethods = current.intentKey === checkoutSession.intentKey ? current.methods : {};
|
|
96
|
+
return {
|
|
97
|
+
intentKey: checkoutSession.intentKey,
|
|
98
|
+
methods: Object.assign(Object.assign({}, currentMethods), { [method]: available }),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const handleApplePayAvailabilityChange = (available) => updatePaywallWalletAvailability('applePay', available);
|
|
103
|
+
const handleGooglePayAvailabilityChange = (available) => updatePaywallWalletAvailability('googlePay', available);
|
|
104
|
+
const trackCheckoutStarted = async () => {
|
|
105
|
+
await (onCheckoutStarted === null || onCheckoutStarted === void 0 ? void 0 : onCheckoutStarted());
|
|
106
|
+
};
|
|
107
|
+
const startCheckout = async () => {
|
|
108
|
+
if (checkoutDisabled || checkoutSession.loading.card) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await trackCheckoutStarted();
|
|
112
|
+
if (checkoutSession.activeClientSecret) {
|
|
113
|
+
setCheckoutDialogOpen(true);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const readyClientSecret = await checkoutSession.prepareCardCheckout();
|
|
117
|
+
if (readyClientSecret) {
|
|
118
|
+
setCheckoutDialogOpen(true);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const trackCheckoutCompleted = async () => {
|
|
122
|
+
await (onCheckoutCompleted === null || onCheckoutCompleted === void 0 ? void 0 : onCheckoutCompleted());
|
|
123
|
+
};
|
|
124
|
+
const commitCheckoutEmail = async (draftEmail) => {
|
|
125
|
+
var _a;
|
|
126
|
+
const normalizedEmail = draftEmail.trim();
|
|
127
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
|
|
128
|
+
throw new Error(content.customerEmailInvalidMessage);
|
|
129
|
+
}
|
|
130
|
+
if (normalizedEmail === ((_a = initialCustomerEmail === null || initialCustomerEmail === void 0 ? void 0 : initialCustomerEmail.trim()) !== null && _a !== void 0 ? _a : '')) {
|
|
131
|
+
setCheckoutEmail(normalizedEmail);
|
|
132
|
+
return normalizedEmail;
|
|
133
|
+
}
|
|
134
|
+
const syncedEmail = await onCustomerEmailCommit(normalizedEmail);
|
|
135
|
+
const committedEmail = syncedEmail.trim() || normalizedEmail;
|
|
136
|
+
setCheckoutEmail(committedEmail);
|
|
137
|
+
return committedEmail;
|
|
138
|
+
};
|
|
139
|
+
const resolveWalletEmailPrompt = useCallback((email) => {
|
|
140
|
+
var _a;
|
|
141
|
+
(_a = walletEmailResolverRef.current) === null || _a === void 0 ? void 0 : _a.call(walletEmailResolverRef, email);
|
|
142
|
+
walletEmailResolverRef.current = null;
|
|
143
|
+
}, []);
|
|
144
|
+
const requestWalletEmail = useCallback(async () => {
|
|
145
|
+
setWalletEmailPromptDraft(checkoutEmail.trim() || (initialCustomerEmail === null || initialCustomerEmail === void 0 ? void 0 : initialCustomerEmail.trim()) || '');
|
|
146
|
+
setWalletEmailPromptError(null);
|
|
147
|
+
setWalletEmailPromptOpen(true);
|
|
148
|
+
return await new Promise((resolve) => {
|
|
149
|
+
walletEmailResolverRef.current = resolve;
|
|
150
|
+
});
|
|
151
|
+
}, [checkoutEmail, initialCustomerEmail]);
|
|
152
|
+
const walletEmailPromptSubmit = async (event) => {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
try {
|
|
155
|
+
const committedEmail = await commitCheckoutEmail(walletEmailPromptDraft);
|
|
156
|
+
setWalletEmailPromptOpen(false);
|
|
157
|
+
setWalletEmailPromptError(null);
|
|
158
|
+
resolveWalletEmailPrompt(committedEmail);
|
|
159
|
+
}
|
|
160
|
+
catch (submitError) {
|
|
161
|
+
setWalletEmailPromptError(getErrorMessage(submitError, content.customerEmailInvalidMessage));
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const walletEmailPromptCancel = () => {
|
|
165
|
+
setWalletEmailPromptOpen(false);
|
|
166
|
+
setWalletEmailPromptError(null);
|
|
167
|
+
resolveWalletEmailPrompt(null);
|
|
168
|
+
};
|
|
169
|
+
const preparePaywallWalletCheckout = async () => {
|
|
170
|
+
await trackCheckoutStarted();
|
|
171
|
+
const normalizedEmail = checkoutEmail.trim() || (initialCustomerEmail === null || initialCustomerEmail === void 0 ? void 0 : initialCustomerEmail.trim()) || '';
|
|
172
|
+
if (!normalizedEmail) {
|
|
173
|
+
return await requestWalletEmail();
|
|
174
|
+
}
|
|
175
|
+
return normalizedEmail;
|
|
176
|
+
};
|
|
177
|
+
const openCardCheckoutFromUnavailableWallet = async () => {
|
|
178
|
+
var _a;
|
|
179
|
+
if (checkoutDisabled || checkoutSession.loading.card) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await trackCheckoutStarted();
|
|
183
|
+
const readyClientSecret = (_a = checkoutSession.activeClientSecret) !== null && _a !== void 0 ? _a : await checkoutSession.prepareCardCheckout();
|
|
184
|
+
if (readyClientSecret) {
|
|
185
|
+
setCheckoutDialogOpen(true);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const closeCheckoutDialog = () => {
|
|
189
|
+
setCheckoutDialogOpen(false);
|
|
190
|
+
setError(null);
|
|
191
|
+
if ((discountState === null || discountState === void 0 ? void 0 : discountState.stage) === 'first' && discountState.status === 'active') {
|
|
192
|
+
onFirstCheckoutClosed === null || onFirstCheckoutClosed === void 0 ? void 0 : onFirstCheckoutClosed();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
checkoutDialogOpen,
|
|
197
|
+
checkoutDialogReady,
|
|
198
|
+
checkoutDisabled,
|
|
199
|
+
checkoutDiscountAmountLabel,
|
|
200
|
+
checkoutDiscountLabel,
|
|
201
|
+
checkoutInitialWalletAvailable,
|
|
202
|
+
checkoutSavedLabel,
|
|
203
|
+
checkoutSession,
|
|
204
|
+
checkoutSuccessReturnUrl,
|
|
205
|
+
checkoutSummaryLabel,
|
|
206
|
+
checkoutTotalValue,
|
|
207
|
+
closeCheckoutDialog,
|
|
208
|
+
commitCheckoutEmail,
|
|
209
|
+
customerEmail: checkoutEmail,
|
|
210
|
+
customerEmailEditable,
|
|
211
|
+
customerName: (_d = customerName === null || customerName === void 0 ? void 0 : customerName.trim()) !== null && _d !== void 0 ? _d : '',
|
|
212
|
+
error,
|
|
213
|
+
handleApplePayAvailabilityChange,
|
|
214
|
+
handleGooglePayAvailabilityChange,
|
|
215
|
+
openCardCheckoutFromUnavailableWallet,
|
|
216
|
+
preparePaywallWalletCheckout,
|
|
217
|
+
setCheckoutEmail,
|
|
218
|
+
startCheckout,
|
|
219
|
+
supportEmail,
|
|
220
|
+
trackCheckoutCompleted,
|
|
221
|
+
walletEmailPromptCancel,
|
|
222
|
+
walletEmailPromptDraft,
|
|
223
|
+
walletEmailPromptError,
|
|
224
|
+
walletEmailPromptOpen,
|
|
225
|
+
walletEmailPromptSubmit,
|
|
226
|
+
walletPaymentMethods,
|
|
227
|
+
setWalletEmailPromptDraft,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function PaywallSubscriptionCheckoutDialogs({ buttonColor = 'var(--color-secondary-text)', content, controller, discountPercent, promoActive, promoDisplayName, remainingSeconds, selectedDisplayPlan, specialOfferDialogOpen, themeColor = 'var(--color-secondary-text)', onSpecialOfferAccept, }) {
|
|
231
|
+
var _a, _b;
|
|
232
|
+
return (_jsxs(_Fragment, { children: [controller.checkoutDialogOpen &&
|
|
233
|
+
controller.checkoutSession.activeClientSecret &&
|
|
234
|
+
controller.checkoutSession.stripePromise &&
|
|
235
|
+
selectedDisplayPlan ? (_jsx(SharedStripeCheckoutV2Dialog, { amountCents: selectedDisplayPlan.amountCents, buttonColor: buttonColor, cardSubmitLabel: content.cardSubmitLabel, clientSecret: controller.checkoutSession.activeClientSecret, closeAriaLabel: content.closeAriaLabel, countdownLabel: content.countdownLabel, customerEmail: controller.customerEmail, customerEmailEditable: controller.customerEmailEditable, customerEmailLabel: content.customerEmailLabel, customerEmailPlaceholder: content.customerEmailPlaceholder, customerEmailInvalidMessage: content.customerEmailInvalidMessage, customerName: controller.customerName, discountAmountLabel: controller.checkoutDiscountAmountLabel, discountLabel: controller.checkoutDiscountLabel, discountPercent: discountPercent, initialWalletAvailable: controller.checkoutInitialWalletAvailable, onClose: controller.closeCheckoutDialog, onCustomerEmailChange: controller.setCheckoutEmail, onCustomerEmailCommit: controller.commitCheckoutEmail, onError: controller.checkoutSession.setError, onSuccess: controller.trackCheckoutCompleted, paymentMethodLabels: content.paymentMethodLabels, paypalButtonLabel: content.paypalButtonLabel, paypalLabel: content.paypalLabel, promoActive: promoActive, promoCode: promoDisplayName, promoCodeLabel: content.promoCodeLabel, processingLabel: content.processingLabel, remainingSeconds: remainingSeconds, returnUrl: controller.checkoutSuccessReturnUrl, savedLabel: controller.checkoutSavedLabel, secureLabel: content.secureLabel, satisfactionLabel: content.satisfactionLabel, satisfactionPercent: content.satisfactionPercent, stripePromise: controller.checkoutSession.stripePromise, summaryLabel: controller.checkoutSummaryLabel, themeColor: themeColor, title: content.title, totalLabel: content.totalLabel, totalValue: controller.checkoutTotalValue, walletButtonLabel: content.walletButtonLabel, originalPriceLabel: content.originalPriceLabel, originalPriceValue: selectedDisplayPlan.basePriceLabel })) : null, specialOfferDialogOpen ? (_jsx(SharedCheckoutSpecialOfferDialog, { buttonColor: buttonColor, buttonLabel: content.specialOffer.buttonLabel, description: content.specialOffer.description, discountLabel: content.specialOffer.discountLabel, imageAlt: (_a = content.specialOffer.image) === null || _a === void 0 ? void 0 : _a.alt, imageSrc: (_b = content.specialOffer.image) === null || _b === void 0 ? void 0 : _b.src, onAccept: onSpecialOfferAccept, themeColor: themeColor, title: content.specialOffer.title })) : null, controller.walletEmailPromptOpen ? (_jsx(PaywallWalletEmailPromptDialog, { content: content, draftEmail: controller.walletEmailPromptDraft, error: controller.walletEmailPromptError, onCancel: controller.walletEmailPromptCancel, onChange: controller.setWalletEmailPromptDraft, onSubmit: controller.walletEmailPromptSubmit })) : null] }));
|
|
236
|
+
}
|
|
237
|
+
export function PaywallSubscriptionPaymentStack({ applePayButtonClassName, applePayShellClassName, cardButtonClassName, className, content, controller, disclaimerClassName, errorClassName, googlePayButtonClassName, googlePayShellClassName, metaClassName, noteClassName, paymentDisclaimer, selectedDisplayPlan, supportClassName, walletSuspended, }) {
|
|
238
|
+
const suspended = controller.checkoutDialogReady || walletSuspended;
|
|
239
|
+
return (_jsxs("section", { className: className, children: [controller.walletPaymentMethods.includes('applePay') ? (_jsx("div", { className: applePayShellClassName, children: selectedDisplayPlan ? (_jsx(ApplePaySubscriptionCheckoutSlot, { amountCents: selectedDisplayPlan.amountCents, beforeConfirm: controller.preparePaywallWalletCheckout, className: applePayButtonClassName, customerEmail: controller.customerEmail, customerName: controller.customerName, disabled: controller.checkoutDisabled, onAvailabilityChange: controller.handleApplePayAvailabilityChange, onSuccess: controller.trackCheckoutCompleted, onUnavailableClick: controller.openCardCheckoutFromUnavailableWallet, placeholderLabel: content.walletPlaceholderLabel, returnUrl: controller.checkoutSuccessReturnUrl, session: controller.checkoutSession, summaryLabel: controller.checkoutSummaryLabel, suspended: suspended })) : null })) : null, controller.walletPaymentMethods.includes('googlePay') ? (_jsx("div", { className: googlePayShellClassName, children: selectedDisplayPlan ? (_jsx(GooglePaySubscriptionCheckoutSlot, { amountCents: selectedDisplayPlan.amountCents, beforeConfirm: controller.preparePaywallWalletCheckout, className: googlePayButtonClassName, customerEmail: controller.customerEmail, customerName: controller.customerName, disabled: controller.checkoutDisabled, onAvailabilityChange: controller.handleGooglePayAvailabilityChange, onSuccess: controller.trackCheckoutCompleted, onUnavailableClick: controller.openCardCheckoutFromUnavailableWallet, placeholderLabel: content.googlePayPlaceholderLabel, returnUrl: controller.checkoutSuccessReturnUrl, session: controller.checkoutSession, summaryLabel: controller.checkoutSummaryLabel, suspended: suspended })) : null })) : null, _jsx("button", { type: 'button', className: cardButtonClassName, onClick: () => void controller.startCheckout(), disabled: controller.checkoutDisabled || controller.checkoutSession.loading.card, children: content.cardButtonLabel }), _jsx("p", { className: metaClassName, children: content.metaLabels.join(' · ') }), _jsx("p", { className: disclaimerClassName, children: paymentDisclaimer }), _jsxs("p", { className: supportClassName, children: [content.supportText, ' ', _jsx("a", { href: `mailto:${controller.supportEmail}`, children: controller.supportEmail })] }), !controller.checkoutSession.configured ? (_jsx("p", { className: noteClassName, children: content.stripeUnavailableNote })) : null, controller.error ? _jsx("p", { className: errorClassName, children: controller.error }) : null] }));
|
|
240
|
+
}
|
|
241
|
+
function PaywallWalletEmailPromptDialog({ content, draftEmail, error, onCancel, onChange, onSubmit, }) {
|
|
242
|
+
return (_jsxs("div", { className: 'paywall-wallet-email-prompt-backdrop', role: 'presentation', children: [_jsxs("form", { className: 'paywall-wallet-email-prompt', role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'paywall-wallet-email-prompt-title', onSubmit: (event) => void onSubmit(event), children: [_jsx("h2", { id: 'paywall-wallet-email-prompt-title', children: content.walletEmailPromptTitle }), _jsx("p", { children: content.walletEmailPromptDescription }), _jsxs("label", { children: [_jsx("span", { children: content.customerEmailLabel }), _jsx("input", { type: 'email', inputMode: 'email', autoComplete: 'email', value: draftEmail, onChange: (event) => onChange(event.currentTarget.value), placeholder: content.customerEmailPlaceholder, autoFocus: true })] }), error ? _jsx("p", { className: 'paywall-wallet-email-prompt-error', children: error }) : null, _jsxs("div", { className: 'paywall-wallet-email-prompt-actions', children: [_jsx("button", { type: 'button', onClick: onCancel, children: content.walletEmailPromptCancelLabel }), _jsx("button", { type: 'submit', children: content.walletEmailPromptSubmitLabel })] })] }), _jsx("style", { children: paywallWalletEmailPromptStyles })] }));
|
|
243
|
+
}
|
|
244
|
+
const paywallWalletEmailPromptStyles = `
|
|
245
|
+
.paywall-wallet-email-prompt-backdrop {
|
|
246
|
+
position: fixed;
|
|
247
|
+
inset: 0;
|
|
248
|
+
z-index: 1000;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: center;
|
|
252
|
+
padding: 20px;
|
|
253
|
+
background: rgb(13 17 32 / 42%);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.paywall-wallet-email-prompt {
|
|
257
|
+
box-sizing: border-box;
|
|
258
|
+
width: min(100%, 360px);
|
|
259
|
+
border-radius: 20px;
|
|
260
|
+
background: #fff;
|
|
261
|
+
padding: 22px;
|
|
262
|
+
box-shadow: 0 18px 48px rgb(19 27 47 / 26%);
|
|
263
|
+
color: #111827;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.paywall-wallet-email-prompt h2 {
|
|
267
|
+
margin: 0;
|
|
268
|
+
color: #111827;
|
|
269
|
+
font-size: 22px;
|
|
270
|
+
font-weight: 800;
|
|
271
|
+
line-height: 1.15;
|
|
272
|
+
letter-spacing: 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.paywall-wallet-email-prompt p {
|
|
276
|
+
margin: 10px 0 0;
|
|
277
|
+
color: #4b5563;
|
|
278
|
+
font-size: 15px;
|
|
279
|
+
font-weight: 500;
|
|
280
|
+
line-height: 1.45;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.paywall-wallet-email-prompt label {
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
gap: 8px;
|
|
287
|
+
margin-top: 18px;
|
|
288
|
+
color: #374151;
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
font-weight: 700;
|
|
291
|
+
line-height: 1.2;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.paywall-wallet-email-prompt input {
|
|
295
|
+
min-height: 48px;
|
|
296
|
+
border: 1px solid #d8deea;
|
|
297
|
+
border-radius: 12px;
|
|
298
|
+
padding: 0 14px;
|
|
299
|
+
color: #111827;
|
|
300
|
+
font: inherit;
|
|
301
|
+
font-size: 16px;
|
|
302
|
+
font-weight: 500;
|
|
303
|
+
outline: none;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.paywall-wallet-email-prompt input:focus {
|
|
307
|
+
border-color: var(--color-secondary-text);
|
|
308
|
+
box-shadow: 0 0 0 3px rgb(63 81 181 / 12%);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.paywall-wallet-email-prompt-error {
|
|
312
|
+
color: #c03232 !important;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.paywall-wallet-email-prompt-actions {
|
|
316
|
+
display: grid;
|
|
317
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
318
|
+
gap: 10px;
|
|
319
|
+
margin-top: 18px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.paywall-wallet-email-prompt-actions button {
|
|
323
|
+
min-height: 48px;
|
|
324
|
+
border-radius: 14px;
|
|
325
|
+
border: 0;
|
|
326
|
+
font-size: 15px;
|
|
327
|
+
font-weight: 800;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.paywall-wallet-email-prompt-actions button[type='button'] {
|
|
332
|
+
background: #f2f5fb;
|
|
333
|
+
color: #4b5563;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.paywall-wallet-email-prompt-actions button[type='submit'] {
|
|
337
|
+
background: var(--color-secondary-text);
|
|
338
|
+
color: #fff;
|
|
339
|
+
}
|
|
340
|
+
`;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './providers/stripe/GooglePaySubscriptionCheckoutSlot.js';
|
|
|
14
14
|
export { ManageSubscriptionScreen, type ManageSubscriptionContent, type ManageSubscriptionScreenProps, } from './components/ManageSubscriptionScreen.js';
|
|
15
15
|
export * from './components/shared/SharedStripeCheckoutDialog.js';
|
|
16
16
|
export * from './components/shared/SharedStripeCheckoutV2Dialog.js';
|
|
17
|
+
export * from './components/shared/PaywallSubscriptionCheckout.js';
|
|
17
18
|
export * from './components/shared/ApplePaySubscribeButton.js';
|
|
18
19
|
export * from './components/shared/GooglePaySubscribeButton.js';
|
|
19
20
|
export * from './components/shared/StripeCheckoutExpressCheckoutButton.js';
|
|
@@ -21,3 +22,4 @@ export * from './components/shared/StripeExpressCheckoutButton.js';
|
|
|
21
22
|
export * from './components/shared/StripeExpressCheckoutElement.js';
|
|
22
23
|
export * from './components/shared/StripePlanSelector.js';
|
|
23
24
|
export * from './components/shared/walletPlatform.js';
|
|
25
|
+
export * from './testing/walletCheckoutSmoke.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './providers/stripe/GooglePaySubscriptionCheckoutSlot.js';
|
|
|
14
14
|
export { ManageSubscriptionScreen, } from './components/ManageSubscriptionScreen.js';
|
|
15
15
|
export * from './components/shared/SharedStripeCheckoutDialog.js';
|
|
16
16
|
export * from './components/shared/SharedStripeCheckoutV2Dialog.js';
|
|
17
|
+
export * from './components/shared/PaywallSubscriptionCheckout.js';
|
|
17
18
|
export * from './components/shared/ApplePaySubscribeButton.js';
|
|
18
19
|
export * from './components/shared/GooglePaySubscribeButton.js';
|
|
19
20
|
export * from './components/shared/StripeCheckoutExpressCheckoutButton.js';
|
|
@@ -21,3 +22,4 @@ export * from './components/shared/StripeExpressCheckoutButton.js';
|
|
|
21
22
|
export * from './components/shared/StripeExpressCheckoutElement.js';
|
|
22
23
|
export * from './components/shared/StripePlanSelector.js';
|
|
23
24
|
export * from './components/shared/walletPlatform.js';
|
|
25
|
+
export * from './testing/walletCheckoutSmoke.js';
|
|
@@ -7,13 +7,14 @@ export type WalletSubscriptionCheckoutRenderStateInput = {
|
|
|
7
7
|
hasStripePromise: boolean;
|
|
8
8
|
suspended: boolean;
|
|
9
9
|
walletAvailable: boolean | null;
|
|
10
|
+
walletPreparing: boolean;
|
|
10
11
|
};
|
|
11
12
|
export type WalletSubscriptionCheckoutRenderState = {
|
|
12
13
|
placeholderIsPreparing: boolean;
|
|
13
14
|
shouldRenderExpressCheckout: boolean;
|
|
14
15
|
shouldRenderPlaceholder: boolean;
|
|
15
16
|
};
|
|
16
|
-
export declare function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, }: WalletSubscriptionCheckoutRenderStateInput): WalletSubscriptionCheckoutRenderState;
|
|
17
|
+
export declare function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, walletPreparing, }: WalletSubscriptionCheckoutRenderStateInput): WalletSubscriptionCheckoutRenderState;
|
|
17
18
|
type WalletPlaceholderButtonProps = {
|
|
18
19
|
className?: string;
|
|
19
20
|
disabled: boolean;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { StripeExpressCheckoutElement } from '../../components/shared/StripeExpressCheckoutElement.js';
|
|
5
5
|
import { isInactiveCheckoutSessionError, } from './useStripeSubscriptionCheckoutSession.js';
|
|
6
|
-
export function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, }) {
|
|
6
|
+
export function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, walletPreparing, }) {
|
|
7
7
|
const shouldRenderExpressCheckout = !suspended &&
|
|
8
8
|
expressCheckoutAllowed &&
|
|
9
9
|
walletAvailable !== false &&
|
|
@@ -14,7 +14,7 @@ export function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowe
|
|
|
14
14
|
expressCheckoutAllowed &&
|
|
15
15
|
walletAvailable === null;
|
|
16
16
|
return {
|
|
17
|
-
placeholderIsPreparing: shouldRenderPlaceholder,
|
|
17
|
+
placeholderIsPreparing: shouldRenderPlaceholder && walletPreparing,
|
|
18
18
|
shouldRenderExpressCheckout,
|
|
19
19
|
shouldRenderPlaceholder,
|
|
20
20
|
};
|
|
@@ -43,19 +43,28 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
|
|
|
43
43
|
hasStripePromise: Boolean(session.stripePromise),
|
|
44
44
|
suspended,
|
|
45
45
|
walletAvailable,
|
|
46
|
+
walletPreparing: session.loading.wallet,
|
|
46
47
|
});
|
|
47
48
|
const serverUpdateKey = session.activeClientSecret &&
|
|
48
49
|
session.checkoutSessionId &&
|
|
49
50
|
session.activePlanKey !== session.planKey
|
|
50
51
|
? session.planKey
|
|
51
52
|
: null;
|
|
52
|
-
const
|
|
53
|
+
const reportWalletAvailability = useCallback((intentKey, available) => {
|
|
53
54
|
setWalletAvailability({
|
|
54
|
-
available:
|
|
55
|
+
available: available,
|
|
55
56
|
intentKey,
|
|
56
57
|
});
|
|
57
|
-
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(
|
|
58
|
+
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(available);
|
|
58
59
|
}, [onAvailabilityChange]);
|
|
60
|
+
const reportWalletPreparationFailure = useCallback((intentKey) => {
|
|
61
|
+
setWalletAvailability((current) => current.intentKey === intentKey
|
|
62
|
+
? current
|
|
63
|
+
: {
|
|
64
|
+
available: null,
|
|
65
|
+
intentKey,
|
|
66
|
+
});
|
|
67
|
+
}, []);
|
|
59
68
|
useEffect(() => {
|
|
60
69
|
if (slotDisabled ||
|
|
61
70
|
suspended ||
|
|
@@ -72,12 +81,12 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
|
|
|
72
81
|
void (async () => {
|
|
73
82
|
const readyClientSecret = await session.prepareWalletCheckout();
|
|
74
83
|
if (!readyClientSecret) {
|
|
75
|
-
|
|
84
|
+
reportWalletPreparationFailure(requestKey);
|
|
76
85
|
}
|
|
77
86
|
})();
|
|
78
87
|
}, [
|
|
79
88
|
expressCheckoutAllowed,
|
|
80
|
-
|
|
89
|
+
reportWalletPreparationFailure,
|
|
81
90
|
session,
|
|
82
91
|
slotDisabled,
|
|
83
92
|
suspended,
|
|
@@ -96,11 +105,7 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
|
|
|
96
105
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
97
106
|
};
|
|
98
107
|
const handleAvailabilityChange = (available) => {
|
|
99
|
-
|
|
100
|
-
available,
|
|
101
|
-
intentKey: session.intentKey,
|
|
102
|
-
});
|
|
103
|
-
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(available);
|
|
108
|
+
reportWalletAvailability(session.intentKey, available);
|
|
104
109
|
};
|
|
105
110
|
const handlePlaceholderClick = async () => {
|
|
106
111
|
if (slotDisabled || session.loading.wallet) {
|
|
@@ -112,9 +117,12 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
|
|
|
112
117
|
}
|
|
113
118
|
const readyClientSecret = await session.prepareWalletCheckout();
|
|
114
119
|
if (!readyClientSecret) {
|
|
115
|
-
|
|
120
|
+
reportWalletPreparationFailure(session.intentKey);
|
|
116
121
|
}
|
|
117
122
|
};
|
|
123
|
+
const placeholderRenderer = placeholderIsPreparing
|
|
124
|
+
? renderPreparingPlaceholder !== null && renderPreparingPlaceholder !== void 0 ? renderPreparingPlaceholder : renderPlaceholder
|
|
125
|
+
: renderPlaceholder;
|
|
118
126
|
return (_jsxs(_Fragment, { children: [shouldRenderPlaceholder || shouldRenderExpressCheckout ? (_jsxs("div", { className: [
|
|
119
127
|
'wallet-subscription-checkout-slot',
|
|
120
128
|
placeholderIsPreparing ? 'is-loading' : '',
|
|
@@ -123,7 +131,7 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
|
|
|
123
131
|
.filter(Boolean)
|
|
124
132
|
.join(' '), "aria-busy": placeholderIsPreparing || undefined, children: [shouldRenderExpressCheckout &&
|
|
125
133
|
session.activeClientSecret &&
|
|
126
|
-
session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onError: handleError, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: (
|
|
134
|
+
session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onError: handleError, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: placeholderRenderer({
|
|
127
135
|
className,
|
|
128
136
|
disabled: slotDisabled,
|
|
129
137
|
label: placeholderLabel,
|
|
@@ -70,6 +70,10 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
70
70
|
const activePlanKey = clientSecretIntentKey === intentKey ? clientSecretPlanKey : null;
|
|
71
71
|
const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey), [checkoutMode, runtimeStripePublishableKey]);
|
|
72
72
|
const previousIntentKeyRef = useRef(intentKey);
|
|
73
|
+
const analyticsMetadataRef = useRef(analyticsMetadata);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
analyticsMetadataRef.current = analyticsMetadata;
|
|
76
|
+
}, [analyticsMetadata]);
|
|
73
77
|
const clearActiveCheckoutSession = useCallback(() => {
|
|
74
78
|
requestIdRef.current += 1;
|
|
75
79
|
creatingIntentRef.current = null;
|
|
@@ -129,7 +133,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
129
133
|
try {
|
|
130
134
|
const response = await createStripeSubscriptionCheckout({
|
|
131
135
|
plan,
|
|
132
|
-
analyticsMetadata,
|
|
136
|
+
analyticsMetadata: analyticsMetadataRef.current,
|
|
133
137
|
couponId,
|
|
134
138
|
customerEmail,
|
|
135
139
|
returnUrl,
|
|
@@ -173,7 +177,6 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
173
177
|
return subscriptionRequest;
|
|
174
178
|
}, [
|
|
175
179
|
activeClientSecret,
|
|
176
|
-
analyticsMetadata,
|
|
177
180
|
checkoutMode,
|
|
178
181
|
clearActiveCheckoutSession,
|
|
179
182
|
configured,
|
|
@@ -228,7 +231,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
228
231
|
await updateStripeSubscriptionCheckoutPlan({
|
|
229
232
|
checkoutSessionId: activeCheckoutSessionId,
|
|
230
233
|
plan,
|
|
231
|
-
analyticsMetadata,
|
|
234
|
+
analyticsMetadata: analyticsMetadataRef.current,
|
|
232
235
|
couponId,
|
|
233
236
|
customerEmail,
|
|
234
237
|
user_id: userId,
|
|
@@ -245,7 +248,6 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
245
248
|
}, [
|
|
246
249
|
activeCheckoutSessionId,
|
|
247
250
|
activePlanKey,
|
|
248
|
-
analyticsMetadata,
|
|
249
251
|
checkoutMode,
|
|
250
252
|
configured,
|
|
251
253
|
couponId,
|
|
@@ -257,7 +259,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
257
259
|
setError,
|
|
258
260
|
userId,
|
|
259
261
|
]);
|
|
260
|
-
return {
|
|
262
|
+
return useMemo(() => ({
|
|
261
263
|
activeClientSecret,
|
|
262
264
|
activePlanKey,
|
|
263
265
|
checkoutSessionId: activeCheckoutSessionId,
|
|
@@ -275,5 +277,22 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
275
277
|
setError,
|
|
276
278
|
stripePromise,
|
|
277
279
|
updateCheckoutSessionPlan,
|
|
278
|
-
}
|
|
280
|
+
}), [
|
|
281
|
+
activeCheckoutSessionId,
|
|
282
|
+
activeClientSecret,
|
|
283
|
+
activePlanKey,
|
|
284
|
+
configured,
|
|
285
|
+
error,
|
|
286
|
+
intentKey,
|
|
287
|
+
loading,
|
|
288
|
+
planKey,
|
|
289
|
+
prepareCardCheckout,
|
|
290
|
+
prepareWalletCheckout,
|
|
291
|
+
reset,
|
|
292
|
+
restartCardCheckout,
|
|
293
|
+
restartWalletCheckout,
|
|
294
|
+
setError,
|
|
295
|
+
stripePromise,
|
|
296
|
+
updateCheckoutSessionPlan,
|
|
297
|
+
]);
|
|
279
298
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type WalletCheckoutSmokeProfileId = 'mobile-safari' | 'mobile-chrome';
|
|
2
|
+
export type WalletCheckoutSmokeProfile = {
|
|
3
|
+
id: WalletCheckoutSmokeProfileId;
|
|
4
|
+
name: string;
|
|
5
|
+
userAgent: string;
|
|
6
|
+
viewport: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
isMobile: boolean;
|
|
10
|
+
hasTouch: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type WalletCheckoutSmokeSnapshot = {
|
|
14
|
+
blockingOverlayVisible: boolean;
|
|
15
|
+
cardFallbackVisible: boolean;
|
|
16
|
+
subscriptionRequestSucceeded: boolean;
|
|
17
|
+
walletSlotState: 'ready' | 'unavailable' | 'unknown';
|
|
18
|
+
};
|
|
19
|
+
export type WalletCheckoutSmokeEvaluation = {
|
|
20
|
+
failures: string[];
|
|
21
|
+
ok: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type WalletCheckoutSmokeSelectors = {
|
|
24
|
+
blockingOverlay?: string;
|
|
25
|
+
cardFallback: string;
|
|
26
|
+
walletSlot: string;
|
|
27
|
+
};
|
|
28
|
+
export type WalletCheckoutSmokeOptions = {
|
|
29
|
+
profiles?: WalletCheckoutSmokeProfile[];
|
|
30
|
+
selectors: WalletCheckoutSmokeSelectors;
|
|
31
|
+
subscriptionRequestPath?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
export type WalletCheckoutSmokeProfileResult = WalletCheckoutSmokeEvaluation & {
|
|
36
|
+
profileId: WalletCheckoutSmokeProfileId;
|
|
37
|
+
snapshot: WalletCheckoutSmokeSnapshot;
|
|
38
|
+
};
|
|
39
|
+
export type WalletCheckoutSmokeResult = {
|
|
40
|
+
ok: boolean;
|
|
41
|
+
results: WalletCheckoutSmokeProfileResult[];
|
|
42
|
+
};
|
|
43
|
+
type PlaywrightPage = {
|
|
44
|
+
click: (selector: string, options?: {
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}) => Promise<void>;
|
|
47
|
+
close?: () => Promise<void>;
|
|
48
|
+
goto: (url: string, options?: {
|
|
49
|
+
waitUntil?: 'domcontentloaded' | 'load' | 'networkidle';
|
|
50
|
+
}) => Promise<unknown>;
|
|
51
|
+
isVisible: (selector: string, options?: {
|
|
52
|
+
timeout?: number;
|
|
53
|
+
}) => Promise<boolean>;
|
|
54
|
+
waitForLoadState: (state?: 'domcontentloaded' | 'load' | 'networkidle', options?: {
|
|
55
|
+
timeout?: number;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
waitForResponse: (predicate: (response: {
|
|
58
|
+
status: () => number;
|
|
59
|
+
url: () => string;
|
|
60
|
+
}) => boolean, options?: {
|
|
61
|
+
timeout?: number;
|
|
62
|
+
}) => Promise<unknown>;
|
|
63
|
+
waitForSelector: (selector: string, options?: {
|
|
64
|
+
state?: 'attached' | 'detached' | 'hidden' | 'visible';
|
|
65
|
+
timeout?: number;
|
|
66
|
+
}) => Promise<unknown>;
|
|
67
|
+
};
|
|
68
|
+
type PlaywrightContext = {
|
|
69
|
+
close: () => Promise<void>;
|
|
70
|
+
newPage: () => Promise<PlaywrightPage>;
|
|
71
|
+
};
|
|
72
|
+
type PlaywrightBrowser = {
|
|
73
|
+
close: () => Promise<void>;
|
|
74
|
+
newContext: (options: {
|
|
75
|
+
hasTouch: boolean;
|
|
76
|
+
isMobile: boolean;
|
|
77
|
+
userAgent: string;
|
|
78
|
+
viewport: {
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
};
|
|
82
|
+
}) => Promise<PlaywrightContext>;
|
|
83
|
+
};
|
|
84
|
+
type PlaywrightChromium = {
|
|
85
|
+
launch: (options?: {
|
|
86
|
+
headless?: boolean;
|
|
87
|
+
}) => Promise<PlaywrightBrowser>;
|
|
88
|
+
};
|
|
89
|
+
export declare function buildWalletCheckoutSmokeProfiles(): WalletCheckoutSmokeProfile[];
|
|
90
|
+
export declare function evaluateWalletCheckoutSmokeSnapshot(snapshot: WalletCheckoutSmokeSnapshot): WalletCheckoutSmokeEvaluation;
|
|
91
|
+
export declare function runWalletCheckoutSmokeTest(chromium: PlaywrightChromium, options: WalletCheckoutSmokeOptions): Promise<WalletCheckoutSmokeResult>;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function buildWalletCheckoutSmokeProfiles() {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
id: 'mobile-safari',
|
|
5
|
+
name: 'Mobile Safari',
|
|
6
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1',
|
|
7
|
+
viewport: {
|
|
8
|
+
width: 390,
|
|
9
|
+
height: 844,
|
|
10
|
+
hasTouch: true,
|
|
11
|
+
isMobile: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'mobile-chrome',
|
|
16
|
+
name: 'Mobile Chrome',
|
|
17
|
+
userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36',
|
|
18
|
+
viewport: {
|
|
19
|
+
width: 412,
|
|
20
|
+
height: 915,
|
|
21
|
+
hasTouch: true,
|
|
22
|
+
isMobile: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
export function evaluateWalletCheckoutSmokeSnapshot(snapshot) {
|
|
28
|
+
const failures = [];
|
|
29
|
+
if (!snapshot.subscriptionRequestSucceeded) {
|
|
30
|
+
failures.push('subscription checkout session request did not succeed');
|
|
31
|
+
}
|
|
32
|
+
if (snapshot.walletSlotState === 'unknown') {
|
|
33
|
+
failures.push('wallet slot never reached Stripe readiness or known unavailable state');
|
|
34
|
+
}
|
|
35
|
+
if (!snapshot.cardFallbackVisible) {
|
|
36
|
+
failures.push('card fallback checkout is not visible');
|
|
37
|
+
}
|
|
38
|
+
if (snapshot.blockingOverlayVisible) {
|
|
39
|
+
failures.push('a blocking loader or modal is still visible');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: failures.length === 0,
|
|
43
|
+
failures,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function runWalletCheckoutSmokeTest(chromium, options) {
|
|
47
|
+
var _a, _b, _c;
|
|
48
|
+
const profiles = (_a = options.profiles) !== null && _a !== void 0 ? _a : buildWalletCheckoutSmokeProfiles();
|
|
49
|
+
const timeoutMs = (_b = options.timeoutMs) !== null && _b !== void 0 ? _b : 15000;
|
|
50
|
+
const subscriptionRequestPath = (_c = options.subscriptionRequestPath) !== null && _c !== void 0 ? _c : '/sdk/public/payments/subscriptions';
|
|
51
|
+
const browser = await chromium.launch({ headless: true });
|
|
52
|
+
try {
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const profile of profiles) {
|
|
55
|
+
const context = await browser.newContext({
|
|
56
|
+
hasTouch: profile.viewport.hasTouch,
|
|
57
|
+
isMobile: profile.viewport.isMobile,
|
|
58
|
+
userAgent: profile.userAgent,
|
|
59
|
+
viewport: {
|
|
60
|
+
width: profile.viewport.width,
|
|
61
|
+
height: profile.viewport.height,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
const page = await context.newPage();
|
|
66
|
+
const subscriptionResponse = page
|
|
67
|
+
.waitForResponse((response) => response.url().includes(subscriptionRequestPath) &&
|
|
68
|
+
response.status() >= 200 &&
|
|
69
|
+
response.status() < 300, { timeout: timeoutMs })
|
|
70
|
+
.then(() => true)
|
|
71
|
+
.catch(() => false);
|
|
72
|
+
await page.goto(options.url, { waitUntil: 'domcontentloaded' });
|
|
73
|
+
await page.waitForLoadState('networkidle', { timeout: timeoutMs }).catch(() => undefined);
|
|
74
|
+
const walletSlotResolved = await Promise.race([
|
|
75
|
+
page.waitForSelector(`${options.selectors.walletSlot}.has-express-checkout`, {
|
|
76
|
+
timeout: timeoutMs,
|
|
77
|
+
}).then(() => 'ready'),
|
|
78
|
+
page.waitForSelector(options.selectors.walletSlot, {
|
|
79
|
+
state: 'detached',
|
|
80
|
+
timeout: timeoutMs,
|
|
81
|
+
}).then(() => 'unavailable'),
|
|
82
|
+
]).catch(() => 'unknown');
|
|
83
|
+
const snapshot = {
|
|
84
|
+
blockingOverlayVisible: options.selectors.blockingOverlay
|
|
85
|
+
? await page.isVisible(options.selectors.blockingOverlay, { timeout: 500 }).catch(() => false)
|
|
86
|
+
: false,
|
|
87
|
+
cardFallbackVisible: await page
|
|
88
|
+
.isVisible(options.selectors.cardFallback, { timeout: timeoutMs })
|
|
89
|
+
.catch(() => false),
|
|
90
|
+
subscriptionRequestSucceeded: await subscriptionResponse,
|
|
91
|
+
walletSlotState: walletSlotResolved,
|
|
92
|
+
};
|
|
93
|
+
const evaluation = evaluateWalletCheckoutSmokeSnapshot(snapshot);
|
|
94
|
+
if (snapshot.cardFallbackVisible) {
|
|
95
|
+
await page.click(options.selectors.cardFallback, { timeout: timeoutMs }).catch(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
results.push(Object.assign(Object.assign({}, evaluation), { profileId: profile.id, snapshot }));
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await context.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: results.every((result) => result.ok),
|
|
105
|
+
results,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await browser.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/payments",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
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
|
+
"smoke:wallet": "npm run build && node scripts/wallet-checkout-smoke.mjs",
|
|
26
27
|
"test:run": "vitest run --passWithNoTests"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|