@funnelsgrove/payments 0.15.7 → 0.15.10
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 +19 -0
- package/dist/components/CheckoutEmailDialog.d.ts +15 -0
- package/dist/components/CheckoutEmailDialog.js +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2.styles.d.ts +1 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2.styles.js +781 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.d.ts +5 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.js +8 -1092
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.d.ts +7 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.js +272 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.d.ts +6 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.js +19 -0
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +10 -3
- package/dist/providers/stripe/components/StripeWalletSurface.internal.d.ts +5 -1
- package/dist/providers/stripe/components/StripeWalletSurface.internal.js +53 -4
- package/dist/providers/stripe/components/useCheckoutEmail.internal.d.ts +20 -0
- package/dist/providers/stripe/components/useCheckoutEmail.internal.js +74 -0
- package/dist/services/checkoutEmail.d.ts +3 -0
- package/dist/services/checkoutEmail.js +20 -0
- package/package.json +3 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SharedStripeCheckoutV2DialogProps } from './SharedStripeCheckoutV2Dialog.js';
|
|
2
|
+
export declare const getFixedSupportedBillingCountry: (supportedCountries: readonly string[]) => string | null;
|
|
3
|
+
type SharedStripeCheckoutV2FormProps = Omit<SharedStripeCheckoutV2DialogProps, 'stripePromise' | 'onClose'> & {
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
};
|
|
6
|
+
export declare function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing, customerEmailLabel, customerEmailPlaceholder, customerEmailInvalidMessage, customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant, }: SharedStripeCheckoutV2FormProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { PaymentElement, useCheckout } from '@stripe/react-stripe-js/checkout';
|
|
5
|
+
import { isValidCheckoutEmail as isValidEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
|
|
6
|
+
import { PaymentBrandMark, SecurityLockIcon } from './CheckoutBrandAssets.js';
|
|
7
|
+
import { StripeCheckoutExpressCheckoutButton } from './StripeCheckoutExpressCheckoutButton.js';
|
|
8
|
+
import { getUnsupportedCheckoutCountryMessage, normalizeSupportedCheckoutCountries } from './checkoutCountries.js';
|
|
9
|
+
import { isInactiveCheckoutSessionError } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
10
|
+
import { getStripeWalletPaymentMethodOrder, usePlatformWalletPaymentMethods } from './walletPlatform.js';
|
|
11
|
+
import { trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPaymentInfoSubmitted } from '../services/checkoutCompletionAnalytics.service.js';
|
|
12
|
+
import { trackCheckoutFailureShown } from '../../../services/checkoutObservability.service.js';
|
|
13
|
+
import { SharedStripeCheckoutV2Summary } from './SharedStripeCheckoutV2Summary.internal.js';
|
|
14
|
+
import { useCheckoutEmail } from './useCheckoutEmail.internal.js';
|
|
15
|
+
const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'Maestro', 'Discover'];
|
|
16
|
+
export const getFixedSupportedBillingCountry = (supportedCountries) => supportedCountries.length === 1 ? supportedCountries[0] : null;
|
|
17
|
+
const checkoutCountryLabels = {
|
|
18
|
+
US: 'United States',
|
|
19
|
+
};
|
|
20
|
+
const getCheckoutCountryLabel = (countryCode) => { var _a; return (_a = checkoutCountryLabels[countryCode]) !== null && _a !== void 0 ? _a : countryCode; };
|
|
21
|
+
function getFriendlyCardErrorMessage(error) {
|
|
22
|
+
return error instanceof Error && error.message
|
|
23
|
+
? error.message
|
|
24
|
+
: 'Payment failed. Please review your card details and try again.';
|
|
25
|
+
}
|
|
26
|
+
export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant = 'standard', }) {
|
|
27
|
+
const checkoutState = useCheckout();
|
|
28
|
+
const walletPaymentMethods = usePlatformWalletPaymentMethods();
|
|
29
|
+
const [selectedMethod, setSelectedMethod] = useState('wallet');
|
|
30
|
+
const [walletAvailable, setWalletAvailable] = useState(initialWalletAvailable !== null && initialWalletAvailable !== void 0 ? initialWalletAvailable : null);
|
|
31
|
+
const effectiveWalletAvailable = initialWalletAvailable === true && walletAvailable !== false
|
|
32
|
+
? true
|
|
33
|
+
: walletAvailable;
|
|
34
|
+
const [submitting, setSubmitting] = useState(false);
|
|
35
|
+
const submitInFlightRef = useRef(false);
|
|
36
|
+
const [paymentElementReady, setPaymentElementReady] = useState(false);
|
|
37
|
+
const [paymentElementComplete, setPaymentElementComplete] = useState(false);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const [billingCountry, setBillingCountry] = useState(null);
|
|
40
|
+
const { initialCustomerEmail, resolvedCustomerEmail, customerEmailEditable, emailDraft, setEmailDraft, rejectedEmail, setRejectedEmail, emailSaving, emailSessionChanged, commitEmailDraftIfValid, ensureCustomerEmail } = useCheckoutEmail({
|
|
41
|
+
customerEmail, allowEmailEditing, customerEmailInvalidMessage,
|
|
42
|
+
onCustomerEmailCommit, onCustomerEmailChange, onError: (message) => setSharedError(message),
|
|
43
|
+
});
|
|
44
|
+
const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
|
|
45
|
+
const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
|
|
46
|
+
const fixedBillingCountryLabel = fixedBillingCountry
|
|
47
|
+
? getCheckoutCountryLabel(fixedBillingCountry)
|
|
48
|
+
: null;
|
|
49
|
+
const cardBrandLabels = useMemo(() => paymentMethodLabels.length > 0 ? paymentMethodLabels : defaultPaymentMethodLabels, [paymentMethodLabels]);
|
|
50
|
+
const normalizedPromoCode = promoCode.trim();
|
|
51
|
+
const showPromo = promoActive !== null && promoActive !== void 0 ? promoActive : (discountPercent > 0 && normalizedPromoCode.length > 0);
|
|
52
|
+
const emailFieldClassName = [
|
|
53
|
+
'shared-checkout-v2-email-field',
|
|
54
|
+
customerEmailEditable ? '' : 'is-disabled',
|
|
55
|
+
]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(' ');
|
|
58
|
+
const checkoutStyle = {
|
|
59
|
+
'--shared-checkout-v2-theme-color': themeColor,
|
|
60
|
+
'--shared-checkout-v2-button-color': buttonColor,
|
|
61
|
+
};
|
|
62
|
+
const expressCheckoutSummaryLabel = (summaryLabel === null || summaryLabel === void 0 ? void 0 : summaryLabel.trim()) || discountLabel || title;
|
|
63
|
+
const walletAriaLabel = walletPaymentMethods
|
|
64
|
+
.map((method) => method === 'applePay' ? walletButtonLabel : 'Google Pay')
|
|
65
|
+
.join(' / ');
|
|
66
|
+
const showWalletMethod = effectiveWalletAvailable !== false && walletPaymentMethods.length > 0;
|
|
67
|
+
const effectiveSelectedMethod = showWalletMethod && selectedMethod === 'wallet'
|
|
68
|
+
&& !emailSessionChanged && isValidEmail(initialCustomerEmail) && rejectedEmail === null
|
|
69
|
+
&& resolvedCustomerEmail === initialCustomerEmail ? 'wallet' : 'card';
|
|
70
|
+
const expressCheckoutOptions = useMemo(() => {
|
|
71
|
+
return {
|
|
72
|
+
buttonHeight: 55,
|
|
73
|
+
buttonTheme: {
|
|
74
|
+
applePay: 'black',
|
|
75
|
+
googlePay: 'black',
|
|
76
|
+
},
|
|
77
|
+
buttonType: {
|
|
78
|
+
applePay: 'subscribe',
|
|
79
|
+
googlePay: 'subscribe',
|
|
80
|
+
},
|
|
81
|
+
layout: {
|
|
82
|
+
maxColumns: 1,
|
|
83
|
+
maxRows: 2,
|
|
84
|
+
overflow: 'auto',
|
|
85
|
+
},
|
|
86
|
+
paymentMethodOrder: getStripeWalletPaymentMethodOrder(walletPaymentMethods),
|
|
87
|
+
paymentMethods: {
|
|
88
|
+
amazonPay: 'never',
|
|
89
|
+
applePay: walletPaymentMethods.includes('applePay') ? 'always' : 'never',
|
|
90
|
+
googlePay: walletPaymentMethods.includes('googlePay') ? 'always' : 'never',
|
|
91
|
+
klarna: 'never',
|
|
92
|
+
link: 'never',
|
|
93
|
+
paypal: 'never',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}, [walletPaymentMethods]);
|
|
97
|
+
const paymentElementOptions = useMemo(() => {
|
|
98
|
+
return {
|
|
99
|
+
fields: {
|
|
100
|
+
billingDetails: {
|
|
101
|
+
name: 'never',
|
|
102
|
+
email: 'never',
|
|
103
|
+
address: {
|
|
104
|
+
country: fixedBillingCountry ? 'never' : 'auto',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
layout: 'tabs',
|
|
109
|
+
wallets: {
|
|
110
|
+
applePay: 'never',
|
|
111
|
+
googlePay: 'never',
|
|
112
|
+
link: 'never',
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}, [fixedBillingCountry]);
|
|
116
|
+
const handleInactiveCheckoutSession = () => {
|
|
117
|
+
setError(null);
|
|
118
|
+
onInactiveCheckoutSession === null || onInactiveCheckoutSession === void 0 ? void 0 : onInactiveCheckoutSession();
|
|
119
|
+
};
|
|
120
|
+
const setSharedError = (message, alreadyTracked = false, providerError) => {
|
|
121
|
+
if (isCheckoutEmailError(providerError) || isCheckoutEmailError(message)) {
|
|
122
|
+
setRejectedEmail(initialCustomerEmail);
|
|
123
|
+
setSelectedMethod('card');
|
|
124
|
+
}
|
|
125
|
+
if (isInactiveCheckoutSessionError(message)) {
|
|
126
|
+
handleInactiveCheckoutSession();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (message && checkoutAnalytics && !alreadyTracked) {
|
|
130
|
+
trackCheckoutFailureShown({
|
|
131
|
+
analytics: checkoutAnalytics,
|
|
132
|
+
checkoutSessionId,
|
|
133
|
+
errorMessage: message,
|
|
134
|
+
failureStage: 'card_form',
|
|
135
|
+
paymentMethod: 'card',
|
|
136
|
+
provider: 'stripe',
|
|
137
|
+
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
138
|
+
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
139
|
+
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
setError(message);
|
|
143
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
144
|
+
};
|
|
145
|
+
const selectWalletMethod = () => {
|
|
146
|
+
if (showWalletMethod) {
|
|
147
|
+
setSelectedMethod('wallet');
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const selectCardMethod = () => setSelectedMethod('card');
|
|
151
|
+
const handleWalletAvailabilityChange = (available) => {
|
|
152
|
+
setWalletAvailable(available);
|
|
153
|
+
if (!available && selectedMethod === 'wallet') {
|
|
154
|
+
setSelectedMethod('card');
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const getBillingCountryError = (country) => getUnsupportedCheckoutCountryMessage({
|
|
158
|
+
billingCountry: country,
|
|
159
|
+
message: unsupportedCountryMessage,
|
|
160
|
+
supportedCountries: normalizedSupportedCountries,
|
|
161
|
+
});
|
|
162
|
+
const handlePaymentElementChange = (event) => {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
setPaymentElementComplete(event.complete);
|
|
165
|
+
setBillingCountry((_b = (_a = event.value.billingDetails) === null || _a === void 0 ? void 0 : _a.address.country) !== null && _b !== void 0 ? _b : fixedBillingCountry);
|
|
166
|
+
};
|
|
167
|
+
const trackCheckoutSuccess = async () => {
|
|
168
|
+
if (checkoutAnalytics) {
|
|
169
|
+
await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
|
|
170
|
+
}
|
|
171
|
+
await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
|
|
172
|
+
};
|
|
173
|
+
const trackPaymentInfoSubmitted = async () => {
|
|
174
|
+
if (checkoutAnalytics) {
|
|
175
|
+
await trackStripeSubscriptionPaymentInfoSubmitted(Object.assign({ checkoutSessionId }, checkoutAnalytics)).catch(() => 'analytics_rejected');
|
|
176
|
+
}
|
|
177
|
+
await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
|
|
178
|
+
};
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (effectiveSelectedMethod !== 'card' || typeof window === 'undefined') {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const resizeTimer = window.setTimeout(() => {
|
|
184
|
+
window.dispatchEvent(new Event('resize'));
|
|
185
|
+
}, 0);
|
|
186
|
+
return () => window.clearTimeout(resizeTimer);
|
|
187
|
+
}, [effectiveSelectedMethod]);
|
|
188
|
+
const handleSubmit = async (event) => {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
if (submitInFlightRef.current || emailSessionChanged) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
|
|
194
|
+
if (billingCountryError) {
|
|
195
|
+
setSharedError(billingCountryError);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (checkoutState.type !== 'success') {
|
|
199
|
+
setSharedError(checkoutState.type === 'error'
|
|
200
|
+
? checkoutState.error.message
|
|
201
|
+
: 'Payment form is not ready yet.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!paymentElementReady) {
|
|
205
|
+
setSharedError('Payment form is not ready yet.', true);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
submitInFlightRef.current = true;
|
|
209
|
+
setSubmitting(true);
|
|
210
|
+
setSharedError(null);
|
|
211
|
+
const finishSubmitting = () => {
|
|
212
|
+
submitInFlightRef.current = false;
|
|
213
|
+
setSubmitting(false);
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
const syncedEmail = await ensureCustomerEmail();
|
|
217
|
+
if (!syncedEmail) {
|
|
218
|
+
finishSubmitting();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (paymentElementComplete) {
|
|
222
|
+
await trackPaymentInfoSubmitted();
|
|
223
|
+
}
|
|
224
|
+
const result = await checkoutState.checkout.confirm({
|
|
225
|
+
redirect: 'if_required',
|
|
226
|
+
});
|
|
227
|
+
if (result.type === 'error') {
|
|
228
|
+
const message = result.error.message || 'Payment failed.';
|
|
229
|
+
setSharedError(message, false, result.error);
|
|
230
|
+
finishSubmitting();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (typeof window !== 'undefined') {
|
|
234
|
+
await trackCheckoutSuccess();
|
|
235
|
+
window.location.assign(returnUrl);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
finishSubmitting();
|
|
239
|
+
}
|
|
240
|
+
catch (submitError) {
|
|
241
|
+
setSharedError(getFriendlyCardErrorMessage(submitError));
|
|
242
|
+
finishSubmitting();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const visibleError = error;
|
|
246
|
+
return (_jsxs("div", { className: 'shared-checkout-v2-shell', "data-variant": variant, style: checkoutStyle, children: [_jsxs("header", { className: 'shared-checkout-v2-header', children: [_jsx("button", { type: 'button', className: 'shared-checkout-v2-close', onClick: onClose, "aria-label": closeAriaLabel, children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'shared-checkout-v2-title', children: title })] }), _jsx(SharedStripeCheckoutV2Summary, { discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }), _jsxs("form", { className: 'shared-checkout-v2-payment', onSubmit: (event) => void handleSubmit(event), children: [showWalletMethod ? (_jsxs("div", { className: 'shared-checkout-v2-methods', role: 'tablist', "aria-label": 'Payment method', children: [_jsxs("button", { type: 'button', className: [
|
|
247
|
+
'shared-checkout-v2-method',
|
|
248
|
+
effectiveSelectedMethod === 'wallet' ? 'is-selected' : '',
|
|
249
|
+
]
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join(' '), onClick: selectWalletMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'wallet', children: [_jsxs("span", { className: 'shared-checkout-v2-tab-wallets', "aria-hidden": 'true', children: [walletPaymentMethods.includes('applePay') ? (_jsx(PaymentBrandMark, { label: 'Apple Pay', className: 'shared-checkout-v2-tab-wallet' })) : null, walletPaymentMethods.includes('googlePay') ? (_jsx(PaymentBrandMark, { label: 'Google Pay', className: 'shared-checkout-v2-tab-wallet' })) : null] }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] }), _jsxs("button", { type: 'button', className: [
|
|
252
|
+
'shared-checkout-v2-method',
|
|
253
|
+
effectiveSelectedMethod === 'card' ? 'is-selected' : '',
|
|
254
|
+
]
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.join(' '), onClick: selectCardMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'card', children: [_jsx("strong", { children: "Credit card" }), _jsx("span", { className: 'shared-checkout-v2-tab-brands', "aria-hidden": 'true', children: cardBrandLabels.slice(0, 4).map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-tab-brand' }, label))) })] })] })) : null, _jsxs("div", { hidden: effectiveSelectedMethod !== 'wallet', className: [
|
|
257
|
+
'shared-checkout-v2-wallet-panel',
|
|
258
|
+
effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join(' '), children: [_jsxs("p", { className: 'shared-checkout-v2-secure-pill', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] }), _jsxs("div", { className: 'shared-checkout-v2-wallet-shell', "aria-label": walletAriaLabel, children: [_jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: (message) => setSharedError(message, true), onPaymentInfoSubmitted: onPaymentInfoSubmitted, onPaymentRejected: () => setSelectedMethod('card'), onSuccess: trackCheckoutSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel, supportedCountries: normalizedSupportedCountries, unsupportedCountryMessage: unsupportedCountryMessage }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
|
|
262
|
+
'shared-checkout-v2-card-panel',
|
|
263
|
+
effectiveSelectedMethod === 'card' ? 'is-visible' : '',
|
|
264
|
+
]
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.join(' '), children: [_jsxs("div", { className: 'shared-checkout-v2-card-box', children: [_jsxs("label", { className: emailFieldClassName, children: [_jsx("span", { className: 'shared-checkout-v2-email-label', children: customerEmailLabel }), _jsx("span", { className: 'shared-checkout-v2-email-control', children: _jsx("input", { type: 'email', autoComplete: 'email', className: 'shared-checkout-v2-email-input', disabled: !customerEmailEditable || emailSaving, inputMode: 'email', readOnly: !customerEmailEditable, value: customerEmailEditable ? emailDraft : initialCustomerEmail, placeholder: customerEmailPlaceholder, onBlur: (event) => commitEmailDraftIfValid(event.currentTarget.value), onChange: (event) => {
|
|
267
|
+
if (!customerEmailEditable) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
setEmailDraft(event.target.value);
|
|
271
|
+
} }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange, onReady: () => setPaymentElementReady(true) }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting || emailSaving || emailSessionChanged || !paymentElementReady, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting || emailSaving || emailSessionChanged ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
|
|
272
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SharedStripeCheckoutV2DialogProps } from './SharedStripeCheckoutV2Dialog.js';
|
|
2
|
+
type SummaryProps = Pick<SharedStripeCheckoutV2DialogProps, 'discountPercent' | 'countdownLabel' | 'remainingSeconds' | 'variant' | 'satisfactionPercent' | 'satisfactionLabel' | 'originalPriceLabel' | 'originalPriceValue' | 'discountLabel' | 'discountAmountLabel' | 'promoCodeLabel' | 'promoCode' | 'totalLabel' | 'totalValue' | 'savedLabel'> & {
|
|
3
|
+
showPromo: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }: SummaryProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
const formatCountdown = (remainingSeconds) => {
|
|
3
|
+
const normalizedSeconds = Number.isFinite(remainingSeconds)
|
|
4
|
+
? Math.max(0, Math.floor(remainingSeconds))
|
|
5
|
+
: 0;
|
|
6
|
+
const minutes = Math.floor(normalizedSeconds / 60);
|
|
7
|
+
const seconds = normalizedSeconds % 60;
|
|
8
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
9
|
+
};
|
|
10
|
+
export function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }) {
|
|
11
|
+
const countdownValue = formatCountdown(remainingSeconds);
|
|
12
|
+
const normalizedPromoCode = promoCode.trim();
|
|
13
|
+
return (_jsxs("section", { className: 'shared-checkout-v2-summary', children: [showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-countdown', children: [discountPercent, "% ", countdownLabel, " ", countdownValue, " min"] })) : null, variant === 'standard' ? (_jsxs("p", { className: 'shared-checkout-v2-satisfaction', children: [_jsx("strong", { children: satisfactionPercent }), " ", satisfactionLabel] })) : null, showPromo ? (_jsxs("div", { className: 'shared-checkout-v2-price-stack', children: [_jsxs("div", { className: 'shared-checkout-v2-price-row is-muted', children: [_jsx("span", { children: originalPriceLabel }), _jsx("span", { className: 'shared-checkout-v2-strike', children: originalPriceValue })] }), _jsxs("div", { className: 'shared-checkout-v2-price-row is-discount', children: [_jsx("strong", { children: discountLabel }), _jsx("strong", { children: discountAmountLabel })] }), _jsxs("div", { className: 'shared-checkout-v2-promo', children: [_jsx("span", { children: promoCodeLabel }), _jsx("strong", { children: normalizedPromoCode })] })] })) : null, _jsxs("div", { className: [
|
|
14
|
+
'shared-checkout-v2-total',
|
|
15
|
+
showPromo ? '' : 'is-plain',
|
|
16
|
+
]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' '), children: [_jsx("span", { children: totalLabel }), _jsxs("strong", { children: [_jsx("svg", { viewBox: '0 0 24 24', "aria-hidden": 'true', children: _jsx("path", { d: 'M20.6 13.4 12.4 21.6a2.2 2.2 0 0 1-3.1 0L2.4 14.7a2.2 2.2 0 0 1-.6-1.6V5.4a2 2 0 0 1 2-2h7.7a2.2 2.2 0 0 1 1.6.6l7.5 7.5a1.4 1.4 0 0 1 0 1.9ZM7.2 8.1a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4Z' }) }), totalValue] })] }), showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-saved', children: [_jsx("span", { "aria-hidden": 'true', children: "\uD83D\uDD25" }), savedLabel] })) : null] }));
|
|
19
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidCheckoutEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
|
|
3
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
|
|
5
6
|
import { queueStripeSubscriptionCheckoutStarted, trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
@@ -188,7 +189,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
188
189
|
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
|
-
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
192
|
+
onError === null || onError === void 0 ? void 0 : onError(isCheckoutEmailError(providerError) ? 'Enter a valid email address to continue.' : message);
|
|
192
193
|
};
|
|
193
194
|
const handleConfirm = async (event) => {
|
|
194
195
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -235,8 +236,8 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
235
236
|
return;
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
|
-
if (
|
|
239
|
-
const message = '
|
|
239
|
+
if (!isValidCheckoutEmail(preparedCustomerEmail)) {
|
|
240
|
+
const message = 'Enter a valid email address to continue.';
|
|
240
241
|
if (checkoutAnalytics) {
|
|
241
242
|
trackCheckoutCanceled({
|
|
242
243
|
analytics: checkoutAnalytics,
|
|
@@ -250,6 +251,12 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
250
251
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
251
252
|
return;
|
|
252
253
|
}
|
|
254
|
+
if ((preparedCustomerEmail === null || preparedCustomerEmail === void 0 ? void 0 : preparedCustomerEmail.trim()) !== (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim())) {
|
|
255
|
+
const message = 'Your email was updated. Please select your payment method again.';
|
|
256
|
+
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
257
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
253
260
|
if (checkoutAnalytics) {
|
|
254
261
|
await trackStripeSubscriptionPaymentInfoSubmitted(Object.assign({ checkoutSessionId }, checkoutAnalytics)).catch(() => 'analytics_rejected');
|
|
255
262
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CheckoutEmailDialogProps } from '../../../components/CheckoutEmailDialog.js';
|
|
1
2
|
import type { StripeWalletCheckoutSuccess } from './StripeCheckoutExpressCheckoutButton.js';
|
|
2
3
|
import { type ReactNode } from 'react';
|
|
3
4
|
import type { Appearance, StripeCheckoutExpressCheckoutElementOptions } from '@stripe/stripe-js';
|
|
@@ -57,6 +58,9 @@ export type StripeWalletSurfaceProps = {
|
|
|
57
58
|
className?: string;
|
|
58
59
|
containerClassName?: string;
|
|
59
60
|
customerEmail?: string | null;
|
|
61
|
+
onCustomerEmailCommit?: CheckoutEmailDialogProps['onSave'];
|
|
62
|
+
onResolveCustomerEmail?: () => Promise<string | null>;
|
|
63
|
+
emailDialogContent?: Pick<CheckoutEmailDialogProps, 'title' | 'description' | 'emailLabel' | 'placeholder' | 'invalidMessage' | 'continueLabel' | 'cancelLabel'>;
|
|
60
64
|
customerName?: string | null;
|
|
61
65
|
disabled?: boolean;
|
|
62
66
|
googlePayOptions?: Partial<StripeCheckoutExpressCheckoutElementOptions>;
|
|
@@ -80,4 +84,4 @@ export type StripeWalletSurfaceProps = {
|
|
|
80
84
|
surfaceId: string;
|
|
81
85
|
suspended?: boolean;
|
|
82
86
|
};
|
|
83
|
-
export declare function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended, }: StripeWalletSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
|
|
87
|
+
export declare function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, onResolveCustomerEmail, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended, }: StripeWalletSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { CheckoutEmailDialog } from '../../../components/CheckoutEmailDialog.js';
|
|
4
|
+
import { isCheckoutEmailError, isValidCheckoutEmail, normalizeCheckoutEmailResult } from '../../../services/checkoutEmail.js';
|
|
3
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
6
|
import { usePlatformWalletPaymentMethods, } from './walletPlatform.js';
|
|
5
7
|
import { ApplePaySubscriptionCheckoutSlot } from './ApplePaySubscriptionCheckoutSlot.js';
|
|
@@ -74,8 +76,41 @@ function buildAvailabilitySignature(availability) {
|
|
|
74
76
|
.join(','),
|
|
75
77
|
].join('|');
|
|
76
78
|
}
|
|
77
|
-
export function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended = false, }) {
|
|
78
|
-
var _a;
|
|
79
|
+
export function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, onResolveCustomerEmail, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended = false, }) {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
|
|
82
|
+
const [rejectedEmail, setRejectedEmail] = useState(null);
|
|
83
|
+
const [savedEmail, setSavedEmail] = useState(null);
|
|
84
|
+
const normalizedEmail = (_a = customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) !== null && _a !== void 0 ? _a : '';
|
|
85
|
+
const needsEmail = !isValidCheckoutEmail(normalizedEmail)
|
|
86
|
+
|| rejectedEmail === normalizedEmail
|
|
87
|
+
|| (savedEmail !== null && savedEmail !== normalizedEmail);
|
|
88
|
+
const [resolvingEmail, setResolvingEmail] = useState(false);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (savedEmail === normalizedEmail)
|
|
91
|
+
setSavedEmail(null);
|
|
92
|
+
}, [normalizedEmail, savedEmail]);
|
|
93
|
+
const openEmailDialog = async () => {
|
|
94
|
+
if (resolvingEmail)
|
|
95
|
+
return;
|
|
96
|
+
if (!onCustomerEmailCommit) {
|
|
97
|
+
await (onUnavailableClick === null || onUnavailableClick === void 0 ? void 0 : onUnavailableClick());
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setResolvingEmail(true);
|
|
101
|
+
try {
|
|
102
|
+
const resolved = await (onResolveCustomerEmail === null || onResolveCustomerEmail === void 0 ? void 0 : onResolveCustomerEmail());
|
|
103
|
+
if (isValidCheckoutEmail(resolved) && (resolved === null || resolved === void 0 ? void 0 : resolved.trim()) !== rejectedEmail)
|
|
104
|
+
return;
|
|
105
|
+
setEmailDialogOpen(true);
|
|
106
|
+
}
|
|
107
|
+
catch (_a) {
|
|
108
|
+
setEmailDialogOpen(true);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
setResolvingEmail(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
79
114
|
const platformWalletPaymentMethods = usePlatformWalletPaymentMethods();
|
|
80
115
|
const walletPaymentMethods = normalizeStripeWalletSurfaceMethods(paymentMethods !== null && paymentMethods !== void 0 ? paymentMethods : platformWalletPaymentMethods);
|
|
81
116
|
const normalizedSurfaceId = surfaceId.trim();
|
|
@@ -92,6 +127,10 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
|
|
|
92
127
|
surfaceMessage: null,
|
|
93
128
|
});
|
|
94
129
|
const emitError = useCallback((message, paymentMethod) => {
|
|
130
|
+
if (isCheckoutEmailError(message)) {
|
|
131
|
+
setRejectedEmail(normalizedEmail);
|
|
132
|
+
setEmailDialogOpen(true);
|
|
133
|
+
}
|
|
95
134
|
const emission = resolveStripeWalletSurfaceError({
|
|
96
135
|
errorState: errorStateRef.current,
|
|
97
136
|
message,
|
|
@@ -103,7 +142,7 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
|
|
|
103
142
|
}
|
|
104
143
|
errorStateRef.current = emission.errorState;
|
|
105
144
|
onError === null || onError === void 0 ? void 0 : onError(emission.message, emission.context);
|
|
106
|
-
}, [normalizedSurfaceId, onError]);
|
|
145
|
+
}, [normalizedEmail, normalizedSurfaceId, onError]);
|
|
107
146
|
useEffect(() => {
|
|
108
147
|
if (reportedAvailabilitySignatureRef.current === availabilitySignature) {
|
|
109
148
|
return;
|
|
@@ -152,6 +191,16 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
|
|
|
152
191
|
resolved: availability.resolved,
|
|
153
192
|
suspended,
|
|
154
193
|
});
|
|
194
|
+
if (needsEmail && session.configured && !suspended && walletPaymentMethods.length > 0) {
|
|
195
|
+
return (_jsxs("div", { className: containerClassName, "data-wallet-surface-id": normalizedSurfaceId, children: [_jsx("button", { type: 'button', className: ['bg-card text-foreground border-border', className].filter(Boolean).join(' '), style: { minHeight: 55, width: '100%', borderRadius: 12, font: 'inherit', cursor: 'pointer' }, disabled: disabled || resolvingEmail, onClick: () => void openEmailDialog(), children: (_b = emailDialogContent === null || emailDialogContent === void 0 ? void 0 : emailDialogContent.continueLabel) !== null && _b !== void 0 ? _b : 'Continue' }), emailDialogOpen && onCustomerEmailCommit ? (_jsx(CheckoutEmailDialog, Object.assign({}, emailDialogContent, { customerEmail: normalizedEmail, rejectedEmail: rejectedEmail, onClose: () => setEmailDialogOpen(false), onSave: async (email) => {
|
|
196
|
+
const saved = await onCustomerEmailCommit(email);
|
|
197
|
+
const nextEmail = normalizeCheckoutEmailResult(saved, email, emailDialogContent === null || emailDialogContent === void 0 ? void 0 : emailDialogContent.invalidMessage);
|
|
198
|
+
setSavedEmail(nextEmail);
|
|
199
|
+
setRejectedEmail(null);
|
|
200
|
+
session.setError(null);
|
|
201
|
+
return nextEmail;
|
|
202
|
+
} }))) : null] }));
|
|
203
|
+
}
|
|
155
204
|
if (!shouldRenderSurface) {
|
|
156
205
|
return null;
|
|
157
206
|
}
|
|
@@ -160,7 +209,7 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
|
|
|
160
209
|
containerClassName !== null && containerClassName !== void 0 ? containerClassName : '',
|
|
161
210
|
]
|
|
162
211
|
.filter(Boolean)
|
|
163
|
-
.join(' '), "data-wallet-surface-id": normalizedSurfaceId, children: [(
|
|
212
|
+
.join(' '), "data-wallet-surface-id": normalizedSurfaceId, children: [(_c = renderHeader === null || renderHeader === void 0 ? void 0 : renderHeader({ availability })) !== null && _c !== void 0 ? _c : null, visibleWalletPaymentMethods.map((method) => {
|
|
164
213
|
var _a;
|
|
165
214
|
return (_jsx("div", { className: [
|
|
166
215
|
'stripe-subscription-wallet-surface__method',
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SharedStripeCheckoutV2DialogProps } from './SharedStripeCheckoutV2Dialog.js';
|
|
2
|
+
type EmailInput = Pick<SharedStripeCheckoutV2DialogProps, 'customerEmail' | 'onCustomerEmailCommit' | 'onCustomerEmailChange'> & {
|
|
3
|
+
allowEmailEditing: boolean;
|
|
4
|
+
customerEmailInvalidMessage: string;
|
|
5
|
+
onError: (message: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function useCheckoutEmail({ customerEmail, allowEmailEditing, customerEmailInvalidMessage, onCustomerEmailCommit, onCustomerEmailChange, onError }: EmailInput): {
|
|
8
|
+
initialCustomerEmail: string;
|
|
9
|
+
resolvedCustomerEmail: string;
|
|
10
|
+
customerEmailEditable: boolean;
|
|
11
|
+
emailDraft: string;
|
|
12
|
+
setEmailDraft: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
13
|
+
rejectedEmail: string | null;
|
|
14
|
+
setRejectedEmail: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
15
|
+
emailSaving: boolean;
|
|
16
|
+
emailSessionChanged: boolean;
|
|
17
|
+
commitEmailDraftIfValid: (value: string) => void;
|
|
18
|
+
ensureCustomerEmail: () => Promise<string | null>;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { isValidCheckoutEmail as isValidEmail, normalizeCheckoutEmailResult } from '../../../services/checkoutEmail.js';
|
|
4
|
+
export function useCheckoutEmail({ customerEmail, allowEmailEditing, customerEmailInvalidMessage, onCustomerEmailCommit, onCustomerEmailChange, onError }) {
|
|
5
|
+
const initialCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '';
|
|
6
|
+
const sessionCustomerEmail = useRef(initialCustomerEmail).current;
|
|
7
|
+
const emailSessionChanged = sessionCustomerEmail !== initialCustomerEmail;
|
|
8
|
+
const [emailDraft, setEmailDraft] = useState(initialCustomerEmail);
|
|
9
|
+
const [rejectedEmail, setRejectedEmail] = useState(null);
|
|
10
|
+
const customerEmailEditable = allowEmailEditing || !isValidEmail(initialCustomerEmail) || rejectedEmail !== null;
|
|
11
|
+
const resolvedCustomerEmail = customerEmailEditable
|
|
12
|
+
? emailDraft.trim()
|
|
13
|
+
: initialCustomerEmail;
|
|
14
|
+
const emailCommitRef = useRef(null);
|
|
15
|
+
const [emailSaving, setEmailSaving] = useState(false);
|
|
16
|
+
const commitCustomerEmail = async (normalizedEmail) => {
|
|
17
|
+
if (emailCommitRef.current)
|
|
18
|
+
return emailCommitRef.current;
|
|
19
|
+
if ((rejectedEmail === null || rejectedEmail === void 0 ? void 0 : rejectedEmail.toLowerCase()) === normalizedEmail.toLowerCase()) {
|
|
20
|
+
throw new Error(customerEmailInvalidMessage);
|
|
21
|
+
}
|
|
22
|
+
if (normalizedEmail === initialCustomerEmail)
|
|
23
|
+
return normalizedEmail;
|
|
24
|
+
if (!onCustomerEmailCommit)
|
|
25
|
+
throw new Error('Unable to save your email. Please try again.');
|
|
26
|
+
setEmailSaving(true);
|
|
27
|
+
const pending = (async () => {
|
|
28
|
+
const committedEmail = await onCustomerEmailCommit(normalizedEmail);
|
|
29
|
+
const nextEmail = normalizeCheckoutEmailResult(committedEmail, normalizedEmail, customerEmailInvalidMessage);
|
|
30
|
+
setEmailDraft(nextEmail);
|
|
31
|
+
setRejectedEmail(null);
|
|
32
|
+
onCustomerEmailChange === null || onCustomerEmailChange === void 0 ? void 0 : onCustomerEmailChange(nextEmail);
|
|
33
|
+
return nextEmail;
|
|
34
|
+
})();
|
|
35
|
+
emailCommitRef.current = pending;
|
|
36
|
+
try {
|
|
37
|
+
return await pending;
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
emailCommitRef.current = null;
|
|
41
|
+
setEmailSaving(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const commitEmailDraftIfValid = (value) => {
|
|
45
|
+
const normalizedEmail = value.trim();
|
|
46
|
+
if (!customerEmailEditable || !onCustomerEmailCommit) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!normalizedEmail || !isValidEmail(normalizedEmail)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
void commitCustomerEmail(normalizedEmail).catch((commitError) => {
|
|
53
|
+
onError(commitError instanceof Error ? commitError.message : 'Unable to save your email. Please try again.');
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const ensureCustomerEmail = async () => {
|
|
57
|
+
if (emailSessionChanged)
|
|
58
|
+
return null;
|
|
59
|
+
const normalizedEmail = resolvedCustomerEmail.trim();
|
|
60
|
+
if (!normalizedEmail || !isValidEmail(normalizedEmail)) {
|
|
61
|
+
onError(customerEmailInvalidMessage);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (emailCommitRef.current || normalizedEmail !== initialCustomerEmail || rejectedEmail !== null) {
|
|
65
|
+
await commitCustomerEmail(normalizedEmail);
|
|
66
|
+
// Saving can switch Funnel User and replace this Checkout Session. Require a fresh action.
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return normalizedEmail;
|
|
70
|
+
};
|
|
71
|
+
return { initialCustomerEmail, resolvedCustomerEmail, customerEmailEditable,
|
|
72
|
+
emailDraft, setEmailDraft, rejectedEmail, setRejectedEmail, emailSaving,
|
|
73
|
+
emailSessionChanged, commitEmailDraftIfValid, ensureCustomerEmail };
|
|
74
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const isValidCheckoutEmail: (value?: string | null) => boolean;
|
|
2
|
+
export declare const isCheckoutEmailError: (error: unknown) => boolean;
|
|
3
|
+
export declare const normalizeCheckoutEmailResult: (result: string | null | void, submittedEmail: string, invalidMessage?: string) => string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const isValidCheckoutEmail = (value) => { var _a; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : ''); };
|
|
2
|
+
export const isCheckoutEmailError = (error) => {
|
|
3
|
+
const record = error && typeof error === 'object'
|
|
4
|
+
? error
|
|
5
|
+
: null;
|
|
6
|
+
if ((record === null || record === void 0 ? void 0 : record.code) === 'email_invalid' || (record === null || record === void 0 ? void 0 : record.code) === 'stripe_email_invalid') {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const message = typeof error === 'string' ? error : record === null || record === void 0 ? void 0 : record.message;
|
|
10
|
+
return typeof message === 'string'
|
|
11
|
+
&& /(?:invalid email|valid email address|email (?:address )?is (?:invalid|required))/i.test(message);
|
|
12
|
+
};
|
|
13
|
+
export const normalizeCheckoutEmailResult = (result, submittedEmail, invalidMessage = 'Enter a valid email address.') => {
|
|
14
|
+
if (result === null)
|
|
15
|
+
throw new Error('Unable to save your email. Please try again.');
|
|
16
|
+
const email = typeof result === 'string' ? result.trim() : submittedEmail.trim();
|
|
17
|
+
if (!isValidCheckoutEmail(email))
|
|
18
|
+
throw new Error(invalidMessage);
|
|
19
|
+
return email;
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/payments",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.10",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"test:run": "vitest run --passWithNoTests"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@funnelsgrove/analytics": "0.1.
|
|
36
|
-
"@funnelsgrove/runtime": "0.14.
|
|
35
|
+
"@funnelsgrove/analytics": "0.1.105",
|
|
36
|
+
"@funnelsgrove/runtime": "0.14.3",
|
|
37
37
|
"@solidgate/react-sdk": "1.34.0",
|
|
38
38
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
39
39
|
"@stripe/stripe-js": "^8.7.0",
|