@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.
@@ -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
- export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, unsupportedCountryMessage, }) {
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
- reportFailure(message, 'wallet_confirmation', event.expressPaymentType, result.error);
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
- if (!input.forceNew && ((_a = input.current) === null || _a === void 0 ? void 0 : _a.key) === input.intentKey) {
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
- clearActiveCheckoutSession();
131
- setErrorState(null);
132
- onError === null || onError === void 0 ? void 0 : onError(null);
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 paymentRejected = input.provider === 'stripe' && (Boolean(providerDeclineCode)
142
- || providerErrorType === 'card_error'
143
- || (providerErrorCode ? STRIPE_PAYMENT_REJECTION_CODES.has(providerErrorCode) : false));
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.15",
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.2",
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",