@funnelsgrove/payments 0.11.15 → 0.11.17
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/providers/stripe/components/SharedStripeCheckoutV2Dialog.js +1 -1
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.d.ts +6 -1
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +43 -5
- package/dist/providers/stripe/components/WalletSubscriptionCheckoutSlot.js +69 -0
- package/dist/providers/stripe/hooks/checkoutAttempt.d.ts +3 -0
- package/dist/providers/stripe/hooks/checkoutAttempt.js +7 -3
- package/dist/providers/stripe/hooks/checkoutIdentity.d.ts +7 -0
- package/dist/providers/stripe/hooks/checkoutIdentity.js +6 -0
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.d.ts +1 -1
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +7 -0
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +2 -0
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +39 -5
- package/dist/providers/stripe/services/checkout/requests.d.ts +53 -0
- package/dist/providers/stripe/services/checkout/requests.js +8 -51
- package/dist/services/checkoutObservability.service.d.ts +14 -0
- package/dist/services/checkoutObservability.service.js +87 -5
- package/package.json +2 -2
|
@@ -319,7 +319,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
319
319
|
effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
|
|
320
320
|
]
|
|
321
321
|
.filter(Boolean)
|
|
322
|
-
.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, 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: [
|
|
322
|
+
.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: [
|
|
323
323
|
'shared-checkout-v2-card-panel',
|
|
324
324
|
effectiveSelectedMethod === 'card' ? 'is-visible' : '',
|
|
325
325
|
]
|
|
@@ -22,7 +22,12 @@ export type StripeCheckoutExpressCheckoutButtonProps = {
|
|
|
22
22
|
onCancel?: () => void;
|
|
23
23
|
onError?: (message: string | null) => void;
|
|
24
24
|
onPaymentInfoSubmitted?: () => void | Promise<void>;
|
|
25
|
+
onPaymentRejected?: (input: {
|
|
26
|
+
declineCode?: string;
|
|
27
|
+
message: string;
|
|
28
|
+
paymentMethod?: string;
|
|
29
|
+
}) => void;
|
|
25
30
|
onSuccess?: () => void | Promise<void>;
|
|
26
31
|
unsupportedCountryMessage?: string;
|
|
27
32
|
};
|
|
28
|
-
export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
4
4
|
import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
|
|
5
5
|
import { queueStripeSubscriptionCheckoutStarted, trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
6
6
|
import { getUnsupportedCheckoutCountryMessage, normalizeSupportedCheckoutCountries, } from './checkoutCountries.js';
|
|
7
|
-
import { trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
7
|
+
import { classifyCheckoutFailure, trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
8
|
+
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
8
9
|
const ExpressCheckoutElementWithCancel = ExpressCheckoutElement;
|
|
9
10
|
const buildDefaultOptions = () => ({
|
|
10
11
|
buttonHeight: 55,
|
|
@@ -29,7 +30,28 @@ const buildDefaultOptions = () => ({
|
|
|
29
30
|
paypal: 'never',
|
|
30
31
|
},
|
|
31
32
|
});
|
|
32
|
-
|
|
33
|
+
const normalizeStripeCheckoutProviderError = (error) => {
|
|
34
|
+
if (!error || typeof error !== 'object') {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const record = error;
|
|
38
|
+
const paymentFailed = record.paymentFailed && typeof record.paymentFailed === 'object'
|
|
39
|
+
? record.paymentFailed
|
|
40
|
+
: null;
|
|
41
|
+
return {
|
|
42
|
+
code: typeof record.code === 'string' ? record.code : undefined,
|
|
43
|
+
decline_code: typeof record.decline_code === 'string' ? record.decline_code : undefined,
|
|
44
|
+
paymentFailed: paymentFailed
|
|
45
|
+
? {
|
|
46
|
+
declineCode: typeof paymentFailed.declineCode === 'string'
|
|
47
|
+
? paymentFailed.declineCode
|
|
48
|
+
: undefined,
|
|
49
|
+
}
|
|
50
|
+
: undefined,
|
|
51
|
+
type: typeof record.type === 'string' ? record.type : undefined,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }) {
|
|
33
55
|
const checkoutState = useCheckout();
|
|
34
56
|
const appliedServerUpdateKeyRef = useRef('');
|
|
35
57
|
const [appliedServerUpdateKey, setAppliedServerUpdateKey] = useState('');
|
|
@@ -143,6 +165,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
143
165
|
paymentMethod }));
|
|
144
166
|
};
|
|
145
167
|
const reportFailure = (message, failureStage, paymentMethod, providerError) => {
|
|
168
|
+
var _a, _b;
|
|
146
169
|
if (checkoutAnalytics) {
|
|
147
170
|
trackCheckoutFailureShown({
|
|
148
171
|
analytics: checkoutAnalytics,
|
|
@@ -151,7 +174,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
151
174
|
failureStage,
|
|
152
175
|
paymentMethod,
|
|
153
176
|
provider: 'stripe',
|
|
154
|
-
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
177
|
+
providerDeclineCode: (_a = providerError === null || providerError === void 0 ? void 0 : providerError.decline_code) !== null && _a !== void 0 ? _a : (_b = providerError === null || providerError === void 0 ? void 0 : providerError.paymentFailed) === null || _b === void 0 ? void 0 : _b.declineCode,
|
|
155
178
|
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
156
179
|
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
157
180
|
});
|
|
@@ -159,7 +182,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
159
182
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
160
183
|
};
|
|
161
184
|
const handleConfirm = async (event) => {
|
|
162
|
-
var _a, _b, _c;
|
|
185
|
+
var _a, _b, _c, _d, _e, _f;
|
|
163
186
|
// Checkout Sessions omits the Express Checkout onClick callback. Confirmation
|
|
164
187
|
// and cancellation are the reliable wallet-intent boundaries for this provider.
|
|
165
188
|
// Session-based analytics deduplication keeps this idempotent if onClick fires.
|
|
@@ -227,7 +250,22 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
227
250
|
: {})), { expressCheckoutConfirmEvent: event, redirect: 'if_required' }));
|
|
228
251
|
if (result.type === 'error') {
|
|
229
252
|
const message = result.error.message || 'Wallet checkout failed.';
|
|
230
|
-
|
|
253
|
+
const providerError = normalizeStripeCheckoutProviderError(result.error);
|
|
254
|
+
reportFailure(message, 'wallet_confirmation', event.expressPaymentType, providerError);
|
|
255
|
+
const declineCode = (_f = (_d = providerError.decline_code) !== null && _d !== void 0 ? _d : (_e = providerError.paymentFailed) === null || _e === void 0 ? void 0 : _e.declineCode) !== null && _f !== void 0 ? _f : undefined;
|
|
256
|
+
if (classifyCheckoutFailure({
|
|
257
|
+
errorMessage: message,
|
|
258
|
+
provider: 'stripe',
|
|
259
|
+
providerDeclineCode: declineCode,
|
|
260
|
+
providerErrorCode: providerError.code,
|
|
261
|
+
providerErrorType: providerError.type,
|
|
262
|
+
}) === 'payment_rejection') {
|
|
263
|
+
onPaymentRejected === null || onPaymentRejected === void 0 ? void 0 : onPaymentRejected({
|
|
264
|
+
declineCode,
|
|
265
|
+
message,
|
|
266
|
+
paymentMethod: toAnalyticsPaymentMethod(event.expressPaymentType) || undefined,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
231
269
|
event.paymentFailed({ reason: 'fail', message });
|
|
232
270
|
return;
|
|
233
271
|
}
|
|
@@ -5,6 +5,7 @@ import { StripeExpressCheckoutElement } from './StripeExpressCheckoutElement.js'
|
|
|
5
5
|
import { trackStripeSubscriptionCheckoutStarted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
6
6
|
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
7
7
|
import { isInactiveCheckoutSessionError, } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
8
|
+
import { trackCheckoutWalletReady } from '../../../services/checkoutObservability.service.js';
|
|
8
9
|
export function getWalletSubscriptionCheckoutRenderState({ expressCheckoutAllowed, hasActiveClientSecret, hasStripePromise, suspended, walletAvailable, walletPreparing, }) {
|
|
9
10
|
const shouldRenderExpressCheckout = !suspended &&
|
|
10
11
|
expressCheckoutAllowed &&
|
|
@@ -42,6 +43,23 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
42
43
|
const [walletCheckoutStartedAfterClickIntentKey, setWalletCheckoutStartedAfterClickIntentKey] = useState(null);
|
|
43
44
|
const autoWalletPreparationIntentKeyRef = useRef(null);
|
|
44
45
|
const walletCheckoutStartedAfterClickSessionIdRef = useRef(null);
|
|
46
|
+
const walletReadinessCycleRef = useRef(null);
|
|
47
|
+
if (!walletReadinessCycleRef.current
|
|
48
|
+
|| walletReadinessCycleRef.current.intentKey !== session.intentKey) {
|
|
49
|
+
walletReadinessCycleRef.current = {
|
|
50
|
+
clientSecretReceivedAtMs: null,
|
|
51
|
+
intentKey: session.intentKey,
|
|
52
|
+
mountedAtMs: null,
|
|
53
|
+
reportedCheckoutSessionId: null,
|
|
54
|
+
requestStartedAtMs: null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const cycle = walletReadinessCycleRef.current;
|
|
59
|
+
if ((cycle === null || cycle === void 0 ? void 0 : cycle.intentKey) === session.intentKey && cycle.mountedAtMs === null) {
|
|
60
|
+
cycle.mountedAtMs = performance.now();
|
|
61
|
+
}
|
|
62
|
+
}, [session.intentKey]);
|
|
45
63
|
const slotDisabled = disabled || !session.configured;
|
|
46
64
|
const analyticsPaymentMethod = toAnalyticsPaymentMethod(availabilityPaymentMethod);
|
|
47
65
|
const walletAvailable = walletAvailability.intentKey === session.intentKey
|
|
@@ -76,6 +94,25 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
76
94
|
intentKey,
|
|
77
95
|
});
|
|
78
96
|
}, []);
|
|
97
|
+
const startWalletPreparationTiming = useCallback((intentKey) => {
|
|
98
|
+
var _a;
|
|
99
|
+
const cycle = walletReadinessCycleRef.current;
|
|
100
|
+
if (!cycle || cycle.intentKey !== intentKey) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const requestStartedAtMs = performance.now();
|
|
104
|
+
(_a = cycle.mountedAtMs) !== null && _a !== void 0 ? _a : (cycle.mountedAtMs = requestStartedAtMs);
|
|
105
|
+
cycle.requestStartedAtMs = requestStartedAtMs;
|
|
106
|
+
cycle.clientSecretReceivedAtMs = null;
|
|
107
|
+
cycle.reportedCheckoutSessionId = null;
|
|
108
|
+
}, []);
|
|
109
|
+
const finishWalletPreparationTiming = useCallback((intentKey, readyClientSecret) => {
|
|
110
|
+
const cycle = walletReadinessCycleRef.current;
|
|
111
|
+
if (!readyClientSecret || !cycle || cycle.intentKey !== intentKey) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
cycle.clientSecretReceivedAtMs = performance.now();
|
|
115
|
+
}, []);
|
|
79
116
|
const autoWalletClientSecret = session.activeClientSecret;
|
|
80
117
|
const autoWalletIntentKey = session.intentKey;
|
|
81
118
|
const autoWalletLoading = session.loading.wallet;
|
|
@@ -93,11 +130,13 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
93
130
|
return;
|
|
94
131
|
}
|
|
95
132
|
autoWalletPreparationIntentKeyRef.current = autoWalletIntentKey;
|
|
133
|
+
startWalletPreparationTiming(autoWalletIntentKey);
|
|
96
134
|
void prepareWalletCheckout({
|
|
97
135
|
checkoutStartSource: 'wallet_render',
|
|
98
136
|
paymentMethod: analyticsPaymentMethod,
|
|
99
137
|
})
|
|
100
138
|
.then((readyClientSecret) => {
|
|
139
|
+
finishWalletPreparationTiming(autoWalletIntentKey, readyClientSecret);
|
|
101
140
|
if (!readyClientSecret) {
|
|
102
141
|
reportWalletPreparationFailure(autoWalletIntentKey);
|
|
103
142
|
}
|
|
@@ -108,9 +147,11 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
108
147
|
autoWalletIntentKey,
|
|
109
148
|
autoWalletLoading,
|
|
110
149
|
expressCheckoutAllowed,
|
|
150
|
+
finishWalletPreparationTiming,
|
|
111
151
|
prepareWalletCheckout,
|
|
112
152
|
reportWalletPreparationFailure,
|
|
113
153
|
slotDisabled,
|
|
154
|
+
startWalletPreparationTiming,
|
|
114
155
|
suspended,
|
|
115
156
|
walletAvailable,
|
|
116
157
|
]);
|
|
@@ -149,7 +190,33 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
149
190
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
150
191
|
};
|
|
151
192
|
const handleAvailabilityChange = (available) => {
|
|
193
|
+
var _a;
|
|
152
194
|
reportWalletAvailability(session.intentKey, available);
|
|
195
|
+
const cycle = walletReadinessCycleRef.current;
|
|
196
|
+
const checkoutSessionId = ((_a = session.checkoutSessionId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
|
|
197
|
+
if (!available
|
|
198
|
+
|| !checkoutAnalytics
|
|
199
|
+
|| !checkoutSessionId
|
|
200
|
+
|| !cycle
|
|
201
|
+
|| cycle.intentKey !== session.intentKey
|
|
202
|
+
|| cycle.mountedAtMs === null
|
|
203
|
+
|| cycle.requestStartedAtMs === null
|
|
204
|
+
|| cycle.clientSecretReceivedAtMs === null
|
|
205
|
+
|| cycle.reportedCheckoutSessionId === checkoutSessionId) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const readyAtMs = performance.now();
|
|
209
|
+
cycle.reportedCheckoutSessionId = checkoutSessionId;
|
|
210
|
+
trackCheckoutWalletReady({
|
|
211
|
+
analytics: checkoutAnalytics,
|
|
212
|
+
checkoutSessionId,
|
|
213
|
+
paymentMethod: availabilityPaymentMethod,
|
|
214
|
+
provider: 'stripe',
|
|
215
|
+
walletClientSecretToReadyMs: Math.max(0, readyAtMs - cycle.clientSecretReceivedAtMs),
|
|
216
|
+
walletMountToReadyMs: Math.max(0, readyAtMs - cycle.mountedAtMs),
|
|
217
|
+
walletMountToRequestMs: Math.max(0, cycle.requestStartedAtMs - cycle.mountedAtMs),
|
|
218
|
+
walletRequestMs: Math.max(0, cycle.clientSecretReceivedAtMs - cycle.requestStartedAtMs),
|
|
219
|
+
});
|
|
153
220
|
};
|
|
154
221
|
const handlePlaceholderClick = async () => {
|
|
155
222
|
if (slotDisabled || session.loading.wallet) {
|
|
@@ -160,10 +227,12 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, appearance, availa
|
|
|
160
227
|
return;
|
|
161
228
|
}
|
|
162
229
|
setWalletCheckoutStartedAfterClickIntentKey(session.intentKey);
|
|
230
|
+
startWalletPreparationTiming(session.intentKey);
|
|
163
231
|
const readyClientSecret = await session.prepareWalletCheckout({
|
|
164
232
|
checkoutStartSource: 'wallet_click',
|
|
165
233
|
paymentMethod: analyticsPaymentMethod,
|
|
166
234
|
});
|
|
235
|
+
finishWalletPreparationTiming(session.intentKey, readyClientSecret);
|
|
167
236
|
if (!readyClientSecret) {
|
|
168
237
|
setWalletCheckoutStartedAfterClickIntentKey(null);
|
|
169
238
|
reportWalletPreparationFailure(session.intentKey);
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export type StripeCheckoutAttempt = {
|
|
2
2
|
key: string;
|
|
3
3
|
id: string;
|
|
4
|
+
createdAtMs: number;
|
|
4
5
|
};
|
|
6
|
+
export declare const STRIPE_CHECKOUT_ATTEMPT_RETRY_WINDOW_MS: number;
|
|
5
7
|
export declare const resolveStripeCheckoutAttempt: (input: {
|
|
6
8
|
current: StripeCheckoutAttempt | null;
|
|
7
9
|
forceNew: boolean;
|
|
8
10
|
intentKey: string;
|
|
9
11
|
paymentsApiVersion?: "v1" | "v2" | null;
|
|
10
12
|
createId: () => string;
|
|
13
|
+
nowMs?: number;
|
|
11
14
|
}) => StripeCheckoutAttempt | null;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
export const STRIPE_CHECKOUT_ATTEMPT_RETRY_WINDOW_MS = 20 * 60 * 1000;
|
|
1
2
|
export const resolveStripeCheckoutAttempt = (input) => {
|
|
2
|
-
var _a;
|
|
3
|
+
var _a, _b;
|
|
3
4
|
if (input.paymentsApiVersion !== 'v2') {
|
|
4
5
|
return null;
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
+
const nowMs = (_a = input.nowMs) !== null && _a !== void 0 ? _a : Date.now();
|
|
8
|
+
if (!input.forceNew
|
|
9
|
+
&& ((_b = input.current) === null || _b === void 0 ? void 0 : _b.key) === input.intentKey
|
|
10
|
+
&& nowMs - input.current.createdAtMs <= STRIPE_CHECKOUT_ATTEMPT_RETRY_WINDOW_MS) {
|
|
7
11
|
return input.current;
|
|
8
12
|
}
|
|
9
|
-
return { key: input.intentKey, id: input.createId() };
|
|
13
|
+
return { key: input.intentKey, id: input.createId(), createdAtMs: nowMs };
|
|
10
14
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CheckoutIdentityStatus } from '@funnelsgrove/runtime';
|
|
2
|
+
export declare const isStripeCheckoutIdentityReady: (input: {
|
|
3
|
+
runtimeConfig?: {
|
|
4
|
+
paymentsApiVersion?: "v1" | "v2" | null;
|
|
5
|
+
} | null;
|
|
6
|
+
userId?: string | null;
|
|
7
|
+
}, getIdentityStatus?: (userId: string | null | undefined) => CheckoutIdentityStatus) => boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getCheckoutIdentityStatus, } from '@funnelsgrove/runtime';
|
|
2
|
+
export const isStripeCheckoutIdentityReady = (input, getIdentityStatus = getCheckoutIdentityStatus) => {
|
|
3
|
+
var _a;
|
|
4
|
+
return ((_a = input.runtimeConfig) === null || _a === void 0 ? void 0 : _a.paymentsApiVersion) !== 'v2'
|
|
5
|
+
|| getIdentityStatus(input.userId) !== 'pending';
|
|
6
|
+
};
|
|
@@ -2,6 +2,7 @@ import type { Stripe } from '@stripe/stripe-js';
|
|
|
2
2
|
import type { RuntimeMode } from '@funnelsgrove/runtime';
|
|
3
3
|
import { type CheckoutPreparationLoading, type CheckoutPreparationSource } from '../../paymentProvider.types.js';
|
|
4
4
|
import { type PaywallPlan, type StripeRuntimeConfigOverrides } from '../services/stripe.service.js';
|
|
5
|
+
export { isStripeCheckoutIdentityReady as isStripeOneTimeCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
5
6
|
type StripeOneTimeCheckoutPlanInput = Pick<PaywallPlan, 'amountCents' | 'analyticsPurchaseValue' | 'description' | 'id' | 'providerPlanId' | 'title' | 'funnelPlanKey' | 'offerSetId' | 'offerSetKey'>;
|
|
6
7
|
export type StripeOneTimeCheckoutSessionInput = {
|
|
7
8
|
runtimeConfigRevisionId?: string | null;
|
|
@@ -49,4 +50,3 @@ export type StripeOneTimeCheckoutIntentKeyInput = Pick<StripeOneTimeCheckoutSess
|
|
|
49
50
|
export declare function buildStripeOneTimeCheckoutIntentKey({ runtimeConfigRevisionId, paymentProfileId, provider, checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutIntentKeyInput): string;
|
|
50
51
|
export declare function shouldResetStripeOneTimeCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
|
|
51
52
|
export declare function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, paymentProfileId, provider, analyticsMetadata, checkoutMode, couponId, customerEmail, enabled, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutSessionInput): StripeOneTimeCheckoutSession;
|
|
52
|
-
export {};
|
|
@@ -16,6 +16,8 @@ import { createStripeOneTimeCheckout, getStripePromise, isStripeConfigured, } fr
|
|
|
16
16
|
import { isInactiveCheckoutSessionError } from './useStripeSubscriptionCheckoutSession.js';
|
|
17
17
|
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
18
18
|
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
19
|
+
import { isStripeCheckoutIdentityReady } from './checkoutIdentity.js';
|
|
20
|
+
export { isStripeCheckoutIdentityReady as isStripeOneTimeCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
19
21
|
export function resolveStripeOneTimeCheckoutPreparationOptions(source, options = {}) {
|
|
20
22
|
var _a, _b;
|
|
21
23
|
const checkoutStartSource = (_a = options.checkoutStartSource) !== null && _a !== void 0 ? _a : (source === 'wallet' ? 'wallet_render' : 'card_click');
|
|
@@ -85,7 +87,12 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, payme
|
|
|
85
87
|
const requestIdRef = useRef(0);
|
|
86
88
|
const attemptRef = useRef(null);
|
|
87
89
|
const anonymousUserIdRef = useRef(null);
|
|
90
|
+
const checkoutIdentityReady = isStripeCheckoutIdentityReady({
|
|
91
|
+
runtimeConfig,
|
|
92
|
+
userId,
|
|
93
|
+
});
|
|
88
94
|
const configured = enabled &&
|
|
95
|
+
checkoutIdentityReady &&
|
|
89
96
|
(isStripeConfigured(checkoutMode) ||
|
|
90
97
|
Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)) ||
|
|
91
98
|
Boolean(normalizeRuntimeConfigValue(stripePublishableKey)));
|
|
@@ -4,6 +4,7 @@ import { type CheckoutPreparationLoading, type CheckoutPreparationSource } from
|
|
|
4
4
|
import type { PaywallDisplayPlan } from '../../../services/paywallOffer.service.js';
|
|
5
5
|
import { type PaywallPlan, type StripeRuntimeConfigOverrides } from '../services/stripe.service.js';
|
|
6
6
|
import { type CheckoutObservabilityAnalyticsContext } from '../../../services/checkoutObservability.service.js';
|
|
7
|
+
export { isStripeCheckoutIdentityReady as isStripeSubscriptionCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
7
8
|
export type StripeSubscriptionCheckoutSessionInput = {
|
|
8
9
|
analyticsMetadata?: Record<string, unknown> | null;
|
|
9
10
|
checkoutAnalytics?: CheckoutObservabilityAnalyticsContext | null;
|
|
@@ -55,5 +56,6 @@ export type StripeSubscriptionCheckoutIntentKeyInput = Pick<StripeSubscriptionCh
|
|
|
55
56
|
export declare function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, planKey, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutIntentKeyInput): string;
|
|
56
57
|
export declare function shouldResetStripeSubscriptionCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
|
|
57
58
|
export declare function isInactiveCheckoutSessionError(message?: string | null): boolean;
|
|
59
|
+
export declare function shouldAutoRecoverStripeSubscriptionCheckoutSession(message: string | null | undefined, alreadyRecovered: boolean): boolean;
|
|
58
60
|
export declare const shouldReportStripeCheckoutFailureShown: (message: string) => boolean;
|
|
59
61
|
export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
|
|
@@ -16,6 +16,8 @@ import { createStripeSubscriptionCheckout, getStripePromise, isStripeConfigured,
|
|
|
16
16
|
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
17
17
|
import { resolveCheckoutObservabilityAnalyticsContext, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
18
18
|
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
19
|
+
import { isStripeCheckoutIdentityReady } from './checkoutIdentity.js';
|
|
20
|
+
export { isStripeCheckoutIdentityReady as isStripeSubscriptionCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
19
21
|
export function resolveStripeSubscriptionCheckoutPreparationOptions(source, options = {}) {
|
|
20
22
|
var _a, _b;
|
|
21
23
|
const checkoutStartSource = (_a = options.checkoutStartSource) !== null && _a !== void 0 ? _a : (source === 'wallet' ? 'wallet_render' : 'card_click');
|
|
@@ -70,6 +72,9 @@ export function isInactiveCheckoutSessionError(message) {
|
|
|
70
72
|
var _a;
|
|
71
73
|
return /\bcheckout session\b.*\bno longer active\b/i.test((_a = message === null || message === void 0 ? void 0 : message.trim()) !== null && _a !== void 0 ? _a : '');
|
|
72
74
|
}
|
|
75
|
+
export function shouldAutoRecoverStripeSubscriptionCheckoutSession(message, alreadyRecovered) {
|
|
76
|
+
return !alreadyRecovered && isInactiveCheckoutSessionError(message);
|
|
77
|
+
}
|
|
73
78
|
export const shouldReportStripeCheckoutFailureShown = (message) => !isInactiveCheckoutSessionError(message);
|
|
74
79
|
export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }) {
|
|
75
80
|
var _a, _b, _c, _d, _e;
|
|
@@ -78,13 +83,21 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
78
83
|
const [checkoutSessionId, setCheckoutSessionId] = useState(null);
|
|
79
84
|
const [clientSecretPlanKey, setClientSecretPlanKey] = useState(null);
|
|
80
85
|
const [error, setErrorState] = useState(null);
|
|
86
|
+
const [recoveryRequest, setRecoveryRequest] = useState(0);
|
|
81
87
|
const [loading, setLoading] = useState(createEmptyCheckoutPreparationLoading);
|
|
82
88
|
const [runtimeStripePublishableKey, setRuntimeStripePublishableKey] = useState(null);
|
|
83
89
|
const creatingIntentRef = useRef(null);
|
|
84
90
|
const requestIdRef = useRef(0);
|
|
85
91
|
const attemptRef = useRef(null);
|
|
92
|
+
const recoveredIntentKeyRef = useRef(null);
|
|
93
|
+
const handledRecoveryRequestRef = useRef(0);
|
|
86
94
|
const anonymousUserIdRef = useRef(null);
|
|
95
|
+
const checkoutIdentityReady = isStripeCheckoutIdentityReady({
|
|
96
|
+
runtimeConfig,
|
|
97
|
+
userId,
|
|
98
|
+
});
|
|
87
99
|
const configured = enabled &&
|
|
100
|
+
checkoutIdentityReady &&
|
|
88
101
|
(isStripeConfigured(checkoutMode) ||
|
|
89
102
|
Boolean(normalizeRuntimeConfigValue(runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey)) ||
|
|
90
103
|
Boolean(normalizeRuntimeConfigValue(stripePublishableKey)));
|
|
@@ -127,21 +140,32 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
127
140
|
}, []);
|
|
128
141
|
const setError = useCallback((message) => {
|
|
129
142
|
if (isInactiveCheckoutSessionError(message)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
const alreadyRecovered = recoveredIntentKeyRef.current === intentKey;
|
|
144
|
+
if (shouldAutoRecoverStripeSubscriptionCheckoutSession(message, alreadyRecovered)) {
|
|
145
|
+
clearActiveCheckoutSession();
|
|
146
|
+
setErrorState(null);
|
|
147
|
+
onError === null || onError === void 0 ? void 0 : onError(null);
|
|
148
|
+
recoveredIntentKeyRef.current = intentKey;
|
|
149
|
+
setRecoveryRequest((current) => current + 1);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
setErrorState(message);
|
|
153
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
154
|
+
}
|
|
133
155
|
return;
|
|
134
156
|
}
|
|
135
157
|
setErrorState(message);
|
|
136
158
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
137
|
-
}, [clearActiveCheckoutSession, onError]);
|
|
159
|
+
}, [clearActiveCheckoutSession, intentKey, onError]);
|
|
138
160
|
const reset = useCallback(() => {
|
|
139
161
|
attemptRef.current = null;
|
|
162
|
+
recoveredIntentKeyRef.current = null;
|
|
163
|
+
handledRecoveryRequestRef.current = recoveryRequest;
|
|
140
164
|
clearActiveCheckoutSession();
|
|
141
165
|
setLoading(createEmptyCheckoutPreparationLoading());
|
|
142
166
|
setRuntimeStripePublishableKey(null);
|
|
143
167
|
setError(null);
|
|
144
|
-
}, [clearActiveCheckoutSession, setError]);
|
|
168
|
+
}, [clearActiveCheckoutSession, recoveryRequest, setError]);
|
|
145
169
|
useIsomorphicLayoutEffect(() => {
|
|
146
170
|
const previousIntentKey = previousIntentKeyRef.current;
|
|
147
171
|
previousIntentKeyRef.current = intentKey;
|
|
@@ -304,6 +328,16 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
304
328
|
const prepareWalletCheckout = useCallback((options = {}) => prepareCheckout('wallet', options), [prepareCheckout]);
|
|
305
329
|
const restartCardCheckout = useCallback((options = {}) => prepareCheckout('card', Object.assign(Object.assign({}, options), { forceNew: true })), [prepareCheckout]);
|
|
306
330
|
const restartWalletCheckout = useCallback((options = {}) => prepareCheckout('wallet', Object.assign(Object.assign({}, options), { forceNew: true })), [prepareCheckout]);
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (recoveryRequest === 0
|
|
333
|
+
|| handledRecoveryRequestRef.current === recoveryRequest) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
handledRecoveryRequestRef.current = recoveryRequest;
|
|
337
|
+
void restartCardCheckout({
|
|
338
|
+
checkoutStartSource: 'checkout_render',
|
|
339
|
+
});
|
|
340
|
+
}, [recoveryRequest, restartCardCheckout]);
|
|
307
341
|
const updateCheckoutSessionPlan = useCallback(async () => {
|
|
308
342
|
if (!configured || !plan || !displayPlan || !activeCheckoutSessionId) {
|
|
309
343
|
return false;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { CreateOneTimeCheckoutInput, CreateSubscriptionCheckoutInput, UpdateSubscriptionCheckoutPlanInput, VerifySubscriptionCheckoutPaymentInput } from './types.js';
|
|
2
2
|
export declare const toOneTimeCheckoutRequest: (input: CreateOneTimeCheckoutInput) => {
|
|
3
|
+
environment: import("@funnelsgrove/runtime").RuntimeMode | undefined;
|
|
4
|
+
runtimeConfig: import("@funnelsgrove/runtime").FunnelSdkRuntimeConfigOverrides | undefined;
|
|
5
|
+
checkoutIdentityToken: string;
|
|
3
6
|
planKey: string;
|
|
4
7
|
runtimeConfigRevisionId: string | null | undefined;
|
|
5
8
|
paymentProfileId: string | null | undefined;
|
|
@@ -18,10 +21,33 @@ export declare const toOneTimeCheckoutRequest: (input: CreateOneTimeCheckoutInpu
|
|
|
18
21
|
customerEmail: string | undefined;
|
|
19
22
|
returnUrl: string;
|
|
20
23
|
userId: string | undefined;
|
|
24
|
+
} | {
|
|
21
25
|
environment: import("@funnelsgrove/runtime").RuntimeMode | undefined;
|
|
22
26
|
runtimeConfig: import("@funnelsgrove/runtime").FunnelSdkRuntimeConfigOverrides | undefined;
|
|
27
|
+
checkoutIdentityToken?: undefined;
|
|
28
|
+
planKey: string;
|
|
29
|
+
runtimeConfigRevisionId: string | null | undefined;
|
|
30
|
+
paymentProfileId: string | null | undefined;
|
|
31
|
+
provider: "stripe" | null | undefined;
|
|
32
|
+
anonymousUserId: string | null | undefined;
|
|
33
|
+
idempotencyKey: string | null | undefined;
|
|
34
|
+
offerSetId: string | undefined;
|
|
35
|
+
offerSetKey: string | undefined;
|
|
36
|
+
planId: string;
|
|
37
|
+
providerPlanId: string | undefined;
|
|
38
|
+
title: string;
|
|
39
|
+
description: string | undefined;
|
|
40
|
+
amountCents: number;
|
|
41
|
+
couponId: string | undefined;
|
|
42
|
+
analyticsMetadata: Record<string, unknown> | undefined;
|
|
43
|
+
customerEmail: string | undefined;
|
|
44
|
+
returnUrl: string;
|
|
45
|
+
userId: string | undefined;
|
|
23
46
|
};
|
|
24
47
|
export declare const toSubscriptionCheckoutRequest: (input: CreateSubscriptionCheckoutInput) => {
|
|
48
|
+
environment: import("@funnelsgrove/runtime").RuntimeMode | undefined;
|
|
49
|
+
runtimeConfig: import("@funnelsgrove/runtime").FunnelSdkRuntimeConfigOverrides | undefined;
|
|
50
|
+
checkoutIdentityToken: string;
|
|
25
51
|
offerSetId: string | undefined;
|
|
26
52
|
offerSetKey: string | undefined;
|
|
27
53
|
planId: string;
|
|
@@ -47,8 +73,35 @@ export declare const toSubscriptionCheckoutRequest: (input: CreateSubscriptionCh
|
|
|
47
73
|
customerEmail: string | undefined;
|
|
48
74
|
returnUrl: string;
|
|
49
75
|
userId: string | undefined;
|
|
76
|
+
} | {
|
|
50
77
|
environment: import("@funnelsgrove/runtime").RuntimeMode | undefined;
|
|
51
78
|
runtimeConfig: import("@funnelsgrove/runtime").FunnelSdkRuntimeConfigOverrides | undefined;
|
|
79
|
+
checkoutIdentityToken?: undefined;
|
|
80
|
+
offerSetId: string | undefined;
|
|
81
|
+
offerSetKey: string | undefined;
|
|
82
|
+
planId: string;
|
|
83
|
+
planKey: string;
|
|
84
|
+
providerPlanId: string | undefined;
|
|
85
|
+
provider: "stripe" | null | undefined;
|
|
86
|
+
paymentProfileId: string | undefined;
|
|
87
|
+
runtimeConfigRevisionId: string | undefined;
|
|
88
|
+
anonymousUserId: string | undefined;
|
|
89
|
+
idempotencyKey: string | undefined;
|
|
90
|
+
title: string;
|
|
91
|
+
description: string | undefined;
|
|
92
|
+
amountCents: number;
|
|
93
|
+
billingInterval: import("../../../../index.js").BillingPlanInterval;
|
|
94
|
+
billingIntervalCount: number;
|
|
95
|
+
followUpProviderPlanId: string | undefined;
|
|
96
|
+
followUpPlanTitle: string | undefined;
|
|
97
|
+
followUpBillingInterval: import("../../../../index.js").BillingPlanInterval | undefined;
|
|
98
|
+
followUpBillingIntervalCount: number | undefined;
|
|
99
|
+
introIterations: number | undefined;
|
|
100
|
+
couponId: string | undefined;
|
|
101
|
+
analyticsMetadata: Record<string, unknown> | undefined;
|
|
102
|
+
customerEmail: string | undefined;
|
|
103
|
+
returnUrl: string;
|
|
104
|
+
userId: string | undefined;
|
|
52
105
|
};
|
|
53
106
|
export declare const toSubscriptionCheckoutPlanRequest: (input: UpdateSubscriptionCheckoutPlanInput) => {
|
|
54
107
|
checkoutSessionId: string;
|
|
@@ -1,63 +1,20 @@
|
|
|
1
1
|
import { getPaywallPlanRequestId } from '../../../../services/paywallPlans/selection.js';
|
|
2
2
|
import { normalizeCheckoutAnalyticsMetadata } from '../shared/checkoutMetadata.js';
|
|
3
3
|
import { requireCheckoutRecurringConfig, requireCheckoutSessionId, } from '../shared/checkout.validator.js';
|
|
4
|
+
import { getCheckoutIdentityToken } from '@funnelsgrove/runtime';
|
|
5
|
+
const toCheckoutIdentityRequestFields = (userId) => {
|
|
6
|
+
const canonicalUserId = userId === null || userId === void 0 ? void 0 : userId.trim();
|
|
7
|
+
const checkoutIdentityToken = getCheckoutIdentityToken(canonicalUserId);
|
|
8
|
+
return checkoutIdentityToken ? { checkoutIdentityToken } : {};
|
|
9
|
+
};
|
|
4
10
|
export const toOneTimeCheckoutRequest = (input) => {
|
|
5
11
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
6
|
-
return {
|
|
7
|
-
planKey: ((_a = input.plan.funnelPlanKey) === null || _a === void 0 ? void 0 : _a.trim()) || input.plan.id,
|
|
8
|
-
runtimeConfigRevisionId: input.runtimeConfigRevisionId,
|
|
9
|
-
paymentProfileId: input.paymentProfileId,
|
|
10
|
-
provider: input.provider,
|
|
11
|
-
anonymousUserId: input.anonymousUserId,
|
|
12
|
-
idempotencyKey: input.idempotencyKey,
|
|
13
|
-
offerSetId: ((_b = input.plan.offerSetId) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
14
|
-
offerSetKey: ((_c = input.plan.offerSetKey) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
|
|
15
|
-
planId: getPaywallPlanRequestId(input.plan),
|
|
16
|
-
providerPlanId: ((_d = input.plan.providerPlanId) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
|
17
|
-
title: input.plan.title,
|
|
18
|
-
description: ((_e = input.plan.description) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
|
|
19
|
-
amountCents: input.plan.amountCents,
|
|
20
|
-
couponId: ((_f = input.couponId) === null || _f === void 0 ? void 0 : _f.trim()) || undefined,
|
|
21
|
-
analyticsMetadata: normalizeCheckoutAnalyticsMetadata(input.analyticsMetadata, input.plan),
|
|
22
|
-
customerEmail: ((_g = input.customerEmail) === null || _g === void 0 ? void 0 : _g.trim()) || undefined,
|
|
23
|
-
returnUrl: input.returnUrl,
|
|
24
|
-
userId: ((_h = input.user_id) === null || _h === void 0 ? void 0 : _h.trim()) || undefined,
|
|
25
|
-
environment: input.environment,
|
|
26
|
-
runtimeConfig: input.runtimeConfig,
|
|
27
|
-
};
|
|
12
|
+
return Object.assign(Object.assign({ planKey: ((_a = input.plan.funnelPlanKey) === null || _a === void 0 ? void 0 : _a.trim()) || input.plan.id, runtimeConfigRevisionId: input.runtimeConfigRevisionId, paymentProfileId: input.paymentProfileId, provider: input.provider, anonymousUserId: input.anonymousUserId, idempotencyKey: input.idempotencyKey, offerSetId: ((_b = input.plan.offerSetId) === null || _b === void 0 ? void 0 : _b.trim()) || undefined, offerSetKey: ((_c = input.plan.offerSetKey) === null || _c === void 0 ? void 0 : _c.trim()) || undefined, planId: getPaywallPlanRequestId(input.plan), providerPlanId: ((_d = input.plan.providerPlanId) === null || _d === void 0 ? void 0 : _d.trim()) || undefined, title: input.plan.title, description: ((_e = input.plan.description) === null || _e === void 0 ? void 0 : _e.trim()) || undefined, amountCents: input.plan.amountCents, couponId: ((_f = input.couponId) === null || _f === void 0 ? void 0 : _f.trim()) || undefined, analyticsMetadata: normalizeCheckoutAnalyticsMetadata(input.analyticsMetadata, input.plan), customerEmail: ((_g = input.customerEmail) === null || _g === void 0 ? void 0 : _g.trim()) || undefined, returnUrl: input.returnUrl, userId: ((_h = input.user_id) === null || _h === void 0 ? void 0 : _h.trim()) || undefined }, toCheckoutIdentityRequestFields(input.user_id)), { environment: input.environment, runtimeConfig: input.runtimeConfig });
|
|
28
13
|
};
|
|
29
14
|
export const toSubscriptionCheckoutRequest = (input) => {
|
|
30
15
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
31
16
|
const recurring = requireCheckoutRecurringConfig(input.plan);
|
|
32
|
-
return {
|
|
33
|
-
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
34
|
-
offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
|
|
35
|
-
planId: getPaywallPlanRequestId(input.plan),
|
|
36
|
-
planKey: ((_c = input.plan.funnelPlanKey) === null || _c === void 0 ? void 0 : _c.trim()) || input.plan.id,
|
|
37
|
-
providerPlanId: ((_d = input.plan.providerPlanId) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
|
|
38
|
-
provider: input.provider,
|
|
39
|
-
paymentProfileId: ((_e = input.paymentProfileId) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
|
|
40
|
-
runtimeConfigRevisionId: ((_f = input.runtimeConfigRevisionId) === null || _f === void 0 ? void 0 : _f.trim()) || undefined,
|
|
41
|
-
anonymousUserId: ((_g = input.anonymousUserId) === null || _g === void 0 ? void 0 : _g.trim()) || undefined,
|
|
42
|
-
idempotencyKey: ((_h = input.idempotencyKey) === null || _h === void 0 ? void 0 : _h.trim()) || undefined,
|
|
43
|
-
title: input.plan.title,
|
|
44
|
-
description: ((_j = input.plan.description) === null || _j === void 0 ? void 0 : _j.trim()) || undefined,
|
|
45
|
-
amountCents: input.plan.amountCents,
|
|
46
|
-
billingInterval: recurring.interval,
|
|
47
|
-
billingIntervalCount: recurring.intervalCount,
|
|
48
|
-
followUpProviderPlanId: ((_k = input.plan.followUpProviderPlanId) === null || _k === void 0 ? void 0 : _k.trim()) || undefined,
|
|
49
|
-
followUpPlanTitle: ((_l = input.plan.followUpPlanTitle) === null || _l === void 0 ? void 0 : _l.trim()) || undefined,
|
|
50
|
-
followUpBillingInterval: input.plan.followUpBillingInterval,
|
|
51
|
-
followUpBillingIntervalCount: input.plan.followUpBillingIntervalCount,
|
|
52
|
-
introIterations: input.plan.introIterations,
|
|
53
|
-
couponId: ((_m = input.couponId) === null || _m === void 0 ? void 0 : _m.trim()) || undefined,
|
|
54
|
-
analyticsMetadata: normalizeCheckoutAnalyticsMetadata(input.analyticsMetadata, input.plan),
|
|
55
|
-
customerEmail: ((_o = input.customerEmail) === null || _o === void 0 ? void 0 : _o.trim()) || undefined,
|
|
56
|
-
returnUrl: input.returnUrl,
|
|
57
|
-
userId: ((_p = input.user_id) === null || _p === void 0 ? void 0 : _p.trim()) || undefined,
|
|
58
|
-
environment: input.environment,
|
|
59
|
-
runtimeConfig: input.runtimeConfig,
|
|
60
|
-
};
|
|
17
|
+
return Object.assign(Object.assign({ offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined, offerSetKey: ((_b = input.plan.offerSetKey) === null || _b === void 0 ? void 0 : _b.trim()) || undefined, planId: getPaywallPlanRequestId(input.plan), planKey: ((_c = input.plan.funnelPlanKey) === null || _c === void 0 ? void 0 : _c.trim()) || input.plan.id, providerPlanId: ((_d = input.plan.providerPlanId) === null || _d === void 0 ? void 0 : _d.trim()) || undefined, provider: input.provider, paymentProfileId: ((_e = input.paymentProfileId) === null || _e === void 0 ? void 0 : _e.trim()) || undefined, runtimeConfigRevisionId: ((_f = input.runtimeConfigRevisionId) === null || _f === void 0 ? void 0 : _f.trim()) || undefined, anonymousUserId: ((_g = input.anonymousUserId) === null || _g === void 0 ? void 0 : _g.trim()) || undefined, idempotencyKey: ((_h = input.idempotencyKey) === null || _h === void 0 ? void 0 : _h.trim()) || undefined, title: input.plan.title, description: ((_j = input.plan.description) === null || _j === void 0 ? void 0 : _j.trim()) || undefined, amountCents: input.plan.amountCents, billingInterval: recurring.interval, billingIntervalCount: recurring.intervalCount, followUpProviderPlanId: ((_k = input.plan.followUpProviderPlanId) === null || _k === void 0 ? void 0 : _k.trim()) || undefined, followUpPlanTitle: ((_l = input.plan.followUpPlanTitle) === null || _l === void 0 ? void 0 : _l.trim()) || undefined, followUpBillingInterval: input.plan.followUpBillingInterval, followUpBillingIntervalCount: input.plan.followUpBillingIntervalCount, introIterations: input.plan.introIterations, couponId: ((_m = input.couponId) === null || _m === void 0 ? void 0 : _m.trim()) || undefined, analyticsMetadata: normalizeCheckoutAnalyticsMetadata(input.analyticsMetadata, input.plan), customerEmail: ((_o = input.customerEmail) === null || _o === void 0 ? void 0 : _o.trim()) || undefined, returnUrl: input.returnUrl, userId: ((_p = input.user_id) === null || _p === void 0 ? void 0 : _p.trim()) || undefined }, toCheckoutIdentityRequestFields(input.user_id)), { environment: input.environment, runtimeConfig: input.runtimeConfig });
|
|
61
18
|
};
|
|
62
19
|
export const toSubscriptionCheckoutPlanRequest = (input) => {
|
|
63
20
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
@@ -29,6 +29,20 @@ type CheckoutFailureShownInput = CheckoutObservabilityInput & {
|
|
|
29
29
|
providerErrorCode?: string | null;
|
|
30
30
|
providerErrorType?: string | null;
|
|
31
31
|
};
|
|
32
|
+
export type CheckoutFailureCategory = 'network_failure' | 'payment_rejection' | 'platform_failure' | 'session_failure' | 'validation_failure';
|
|
33
|
+
export type CheckoutFailureClassificationInput = Pick<CheckoutFailureShownInput, 'errorMessage' | 'provider' | 'providerDeclineCode' | 'providerErrorCode' | 'providerErrorType'>;
|
|
34
|
+
export declare const classifyCheckoutFailure: (input: CheckoutFailureClassificationInput) => CheckoutFailureCategory;
|
|
32
35
|
export declare const trackCheckoutFailureShown: (input: CheckoutFailureShownInput) => string | null;
|
|
33
36
|
export declare const trackCheckoutCanceled: (input: CheckoutObservabilityInput) => string | null;
|
|
37
|
+
export type CheckoutWalletReadyInput = {
|
|
38
|
+
analytics: CheckoutObservabilityAnalyticsContext;
|
|
39
|
+
checkoutSessionId: string;
|
|
40
|
+
paymentMethod: unknown;
|
|
41
|
+
provider: 'stripe';
|
|
42
|
+
walletClientSecretToReadyMs: number;
|
|
43
|
+
walletMountToReadyMs: number;
|
|
44
|
+
walletMountToRequestMs: number;
|
|
45
|
+
walletRequestMs: number;
|
|
46
|
+
};
|
|
47
|
+
export declare const trackCheckoutWalletReady: (input: CheckoutWalletReadyInput) => string | null;
|
|
34
48
|
export {};
|
|
@@ -37,7 +37,31 @@ const STRIPE_PAYMENT_REJECTION_CODES = new Set([
|
|
|
37
37
|
'invalid_expiry_month',
|
|
38
38
|
'invalid_expiry_year',
|
|
39
39
|
'payment_intent_authentication_failure',
|
|
40
|
+
'paymentFailed',
|
|
40
41
|
]);
|
|
42
|
+
export const classifyCheckoutFailure = (input) => {
|
|
43
|
+
const providerDeclineCode = asNonEmptyString(input.providerDeclineCode);
|
|
44
|
+
const providerErrorCode = asNonEmptyString(input.providerErrorCode);
|
|
45
|
+
const providerErrorType = asNonEmptyString(input.providerErrorType);
|
|
46
|
+
if (input.provider === 'stripe' && (Boolean(providerDeclineCode)
|
|
47
|
+
|| providerErrorType === 'card_error'
|
|
48
|
+
|| (providerErrorCode ? STRIPE_PAYMENT_REJECTION_CODES.has(providerErrorCode) : false))) {
|
|
49
|
+
return 'payment_rejection';
|
|
50
|
+
}
|
|
51
|
+
const message = input.errorMessage.trim();
|
|
52
|
+
if (/checkout session.*no longer active/i.test(message)
|
|
53
|
+
|| /selected offer is no longer available/i.test(message)) {
|
|
54
|
+
return 'session_failure';
|
|
55
|
+
}
|
|
56
|
+
if (/failed to fetch|load failed|network|connection to (?:the )?provider|too many requests/i.test(message)) {
|
|
57
|
+
return 'network_failure';
|
|
58
|
+
}
|
|
59
|
+
if (/(?:email|card number|expiration|expiry|cvc|zip|postal code|billing address|country).*(?:required|incomplete|incorrect|invalid|unsupported)/i.test(message)
|
|
60
|
+
|| /(?:required|incomplete|incorrect|invalid|unsupported).*(?:email|card number|expiration|expiry|cvc|zip|postal code|billing address|country)/i.test(message)) {
|
|
61
|
+
return 'validation_failure';
|
|
62
|
+
}
|
|
63
|
+
return 'platform_failure';
|
|
64
|
+
};
|
|
41
65
|
const sanitizeErrorMessage = (value) => value
|
|
42
66
|
.replace(/[\r\n\t]+/g, ' ')
|
|
43
67
|
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]')
|
|
@@ -68,6 +92,7 @@ const reportCheckoutObservability = (input) => {
|
|
|
68
92
|
environment,
|
|
69
93
|
errorCode: input.errorCode,
|
|
70
94
|
errorMessage: input.errorMessage,
|
|
95
|
+
failureCategory: input.failureCategory,
|
|
71
96
|
eventId: input.eventId,
|
|
72
97
|
eventType: input.eventType,
|
|
73
98
|
failureStage: input.failureStage,
|
|
@@ -78,6 +103,10 @@ const reportCheckoutObservability = (input) => {
|
|
|
78
103
|
providerDeclineCode: input.providerDeclineCode,
|
|
79
104
|
providerErrorCode: input.providerErrorCode,
|
|
80
105
|
providerErrorType: input.providerErrorType,
|
|
106
|
+
walletClientSecretToReadyMs: input.walletClientSecretToReadyMs,
|
|
107
|
+
walletMountToReadyMs: input.walletMountToReadyMs,
|
|
108
|
+
walletMountToRequestMs: input.walletMountToRequestMs,
|
|
109
|
+
walletRequestMs: input.walletRequestMs,
|
|
81
110
|
}),
|
|
82
111
|
keepalive: true,
|
|
83
112
|
}).then(() => undefined);
|
|
@@ -96,9 +125,9 @@ const resolveErrorCode = (message, requestedCode) => {
|
|
|
96
125
|
return 'checkout_failure';
|
|
97
126
|
};
|
|
98
127
|
const emitCheckoutObservability = (eventType, input, failure) => {
|
|
99
|
-
var _a, _b;
|
|
128
|
+
var _a, _b, _c, _d;
|
|
100
129
|
const paymentMethod = toAnalyticsPaymentMethod(input.paymentMethod);
|
|
101
|
-
const metadata = Object.assign(Object.assign({}, ((_a = input.analytics.metadata) !== null && _a !== void 0 ? _a : {})), { checkout_session_id: ((_b = input.checkoutSessionId) === null || _b === void 0 ? void 0 : _b.trim()) || undefined, checkout_failure_stage: input.failureStage, payment_provider: input.provider, payment_method: paymentMethod || undefined, error_code: failure === null || failure === void 0 ? void 0 : failure.errorCode, error_message: failure === null || failure === void 0 ? void 0 : failure.errorMessage, provider_decline_code: failure === null || failure === void 0 ? void 0 : failure.providerDeclineCode, provider_error_code: failure === null || failure === void 0 ? void 0 : failure.providerErrorCode, provider_error_type: failure === null || failure === void 0 ? void 0 : failure.providerErrorType });
|
|
130
|
+
const metadata = Object.assign(Object.assign({}, ((_a = input.analytics.metadata) !== null && _a !== void 0 ? _a : {})), { checkout_session_id: ((_b = input.checkoutSessionId) === null || _b === void 0 ? void 0 : _b.trim()) || undefined, checkout_failure_stage: input.failureStage, payment_provider: input.provider, payment_method: paymentMethod || undefined, error_code: failure === null || failure === void 0 ? void 0 : failure.errorCode, error_message: failure === null || failure === void 0 ? void 0 : failure.errorMessage, checkout_failure_category: (_c = failure === null || failure === void 0 ? void 0 : failure.failureCategory) !== null && _c !== void 0 ? _c : (eventType === 'checkout_canceled' ? 'user_canceled' : undefined), provider_decline_code: failure === null || failure === void 0 ? void 0 : failure.providerDeclineCode, provider_error_code: failure === null || failure === void 0 ? void 0 : failure.providerErrorCode, provider_error_type: failure === null || failure === void 0 ? void 0 : failure.providerErrorType });
|
|
102
131
|
const eventId = publicAnalyticsSdk.track({
|
|
103
132
|
eventType,
|
|
104
133
|
environment: input.analytics.environment,
|
|
@@ -117,6 +146,7 @@ const emitCheckoutObservability = (eventType, input, failure) => {
|
|
|
117
146
|
checkoutSessionId: input.checkoutSessionId,
|
|
118
147
|
errorCode: failure === null || failure === void 0 ? void 0 : failure.errorCode,
|
|
119
148
|
errorMessage: failure === null || failure === void 0 ? void 0 : failure.errorMessage,
|
|
149
|
+
failureCategory: (_d = failure === null || failure === void 0 ? void 0 : failure.failureCategory) !== null && _d !== void 0 ? _d : (eventType === 'checkout_canceled' ? 'user_canceled' : undefined),
|
|
120
150
|
eventId,
|
|
121
151
|
eventType,
|
|
122
152
|
failureStage: input.failureStage,
|
|
@@ -138,17 +168,69 @@ export const trackCheckoutFailureShown = (input) => {
|
|
|
138
168
|
const providerDeclineCode = (_a = asNonEmptyString(input.providerDeclineCode)) === null || _a === void 0 ? void 0 : _a.slice(0, 120);
|
|
139
169
|
const providerErrorCode = (_b = asNonEmptyString(input.providerErrorCode)) === null || _b === void 0 ? void 0 : _b.slice(0, 120);
|
|
140
170
|
const providerErrorType = (_c = asNonEmptyString(input.providerErrorType)) === null || _c === void 0 ? void 0 : _c.slice(0, 120);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
const failureCategory = classifyCheckoutFailure({
|
|
172
|
+
errorMessage,
|
|
173
|
+
provider: input.provider,
|
|
174
|
+
providerDeclineCode,
|
|
175
|
+
providerErrorCode,
|
|
176
|
+
providerErrorType,
|
|
177
|
+
});
|
|
178
|
+
const paymentRejected = failureCategory === 'payment_rejection';
|
|
144
179
|
return emitCheckoutObservability(paymentRejected ? 'checkout_payment_rejected' : 'checkout_failure_shown', input, {
|
|
145
180
|
errorCode: paymentRejected
|
|
146
181
|
? providerDeclineCode || providerErrorCode || 'payment_rejected'
|
|
147
182
|
: resolveErrorCode(errorMessage, input.errorCode),
|
|
148
183
|
errorMessage,
|
|
184
|
+
failureCategory,
|
|
149
185
|
providerDeclineCode,
|
|
150
186
|
providerErrorCode,
|
|
151
187
|
providerErrorType,
|
|
152
188
|
});
|
|
153
189
|
};
|
|
154
190
|
export const trackCheckoutCanceled = (input) => emitCheckoutObservability('checkout_canceled', input);
|
|
191
|
+
const normalizeWalletTiming = (value) => (Number.isFinite(value) && value >= 0 && value <= 300000 ? value : null);
|
|
192
|
+
export const trackCheckoutWalletReady = (input) => {
|
|
193
|
+
var _a;
|
|
194
|
+
const checkoutSessionId = input.checkoutSessionId.trim();
|
|
195
|
+
const paymentMethod = toAnalyticsPaymentMethod(input.paymentMethod);
|
|
196
|
+
const walletClientSecretToReadyMs = normalizeWalletTiming(input.walletClientSecretToReadyMs);
|
|
197
|
+
const walletMountToReadyMs = normalizeWalletTiming(input.walletMountToReadyMs);
|
|
198
|
+
const walletMountToRequestMs = normalizeWalletTiming(input.walletMountToRequestMs);
|
|
199
|
+
const walletRequestMs = normalizeWalletTiming(input.walletRequestMs);
|
|
200
|
+
if (!checkoutSessionId
|
|
201
|
+
|| !paymentMethod
|
|
202
|
+
|| walletClientSecretToReadyMs === null
|
|
203
|
+
|| walletMountToReadyMs === null
|
|
204
|
+
|| walletMountToRequestMs === null
|
|
205
|
+
|| walletRequestMs === null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
const eventId = publicAnalyticsSdk.track({
|
|
209
|
+
environment: input.analytics.environment,
|
|
210
|
+
eventId: `wallet-ready:${checkoutSessionId}:${paymentMethod}`,
|
|
211
|
+
eventType: 'checkout_wallet_ready',
|
|
212
|
+
featureFlags: input.analytics.featureFlags,
|
|
213
|
+
metadata: Object.assign(Object.assign({}, ((_a = input.analytics.metadata) !== null && _a !== void 0 ? _a : {})), { checkout_session_id: checkoutSessionId, payment_method: paymentMethod, payment_provider: input.provider, wallet_client_secret_to_ready_ms: walletClientSecretToReadyMs, wallet_mount_to_ready_ms: walletMountToReadyMs, wallet_mount_to_request_ms: walletMountToRequestMs, wallet_checkout_request_ms: walletRequestMs }),
|
|
214
|
+
stepContractVersion: input.analytics.stepContractVersion,
|
|
215
|
+
stepId: input.analytics.stepId,
|
|
216
|
+
stepName: input.analytics.stepName,
|
|
217
|
+
stepType: input.analytics.stepType,
|
|
218
|
+
});
|
|
219
|
+
if (!eventId) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
void reportCheckoutObservability({
|
|
223
|
+
analytics: input.analytics,
|
|
224
|
+
checkoutSessionId,
|
|
225
|
+
eventId,
|
|
226
|
+
eventType: 'checkout_wallet_ready',
|
|
227
|
+
paymentMethod,
|
|
228
|
+
provider: input.provider,
|
|
229
|
+
walletClientSecretToReadyMs,
|
|
230
|
+
walletMountToReadyMs,
|
|
231
|
+
walletMountToRequestMs,
|
|
232
|
+
walletRequestMs,
|
|
233
|
+
}).catch(() => undefined);
|
|
234
|
+
void publicAnalyticsSdk.flush().catch(() => 0);
|
|
235
|
+
return eventId;
|
|
236
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/payments",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.17",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@funnelsgrove/analytics": "^0.1.86",
|
|
36
|
-
"@funnelsgrove/runtime": "^0.11.
|
|
36
|
+
"@funnelsgrove/runtime": "^0.11.4",
|
|
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",
|