@funnelsgrove/payments 0.11.6 → 0.11.9
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +56 -8
- package/dist/providers/stripe/components/SharedStripeCheckoutDialog.js +33 -8
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.js +38 -8
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +63 -12
- package/dist/providers/stripe/components/StripeExpressCheckoutButton.js +49 -7
- package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.js +1 -1
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +4 -1
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +41 -3
- package/dist/services/checkoutObservability.service.d.ts +34 -0
- package/dist/services/checkoutObservability.service.js +154 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './services/runtimeBillingPlanCatalog.service.js';
|
|
|
7
7
|
export * from './services/publishedBillingRuntime.service.js';
|
|
8
8
|
export * from './services/paywallOffer.service.js';
|
|
9
9
|
export * from './services/paymentMethodAnalytics.service.js';
|
|
10
|
+
export * from './services/checkoutObservability.service.js';
|
|
10
11
|
export * from './providers/paymentProvider.types.js';
|
|
11
12
|
export * from './providers/stripe/index.js';
|
|
12
13
|
export * from './providers/solidgate/index.js';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './services/runtimeBillingPlanCatalog.service.js';
|
|
|
8
8
|
export * from './services/publishedBillingRuntime.service.js';
|
|
9
9
|
export * from './services/paywallOffer.service.js';
|
|
10
10
|
export * from './services/paymentMethodAnalytics.service.js';
|
|
11
|
+
export * from './services/checkoutObservability.service.js';
|
|
11
12
|
export * from './providers/paymentProvider.types.js';
|
|
12
13
|
// Provider-specific integrations are grouped behind provider barrels.
|
|
13
14
|
export * from './providers/stripe/index.js';
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
|
|
4
4
|
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
5
5
|
import { pollSolidgateCheckoutStatus, prepareSolidgateCheckout, } from '../services/solidgate.service.js';
|
|
6
|
+
import { trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
6
7
|
const SolidgatePayment = lazy(async () => ({
|
|
7
8
|
default: (await import('@solidgate/react-sdk')).default,
|
|
8
9
|
}));
|
|
@@ -250,6 +251,20 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
250
251
|
setView(nextView);
|
|
251
252
|
(_a = onStateChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onStateChangeRef, nextView.state);
|
|
252
253
|
}, []);
|
|
254
|
+
const trackFailure = useCallback((message, failureStage, attemptId) => {
|
|
255
|
+
const analytics = checkoutAnalyticsRef.current;
|
|
256
|
+
if (!analytics) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
trackCheckoutFailureShown({
|
|
260
|
+
analytics,
|
|
261
|
+
checkoutSessionId: attemptId,
|
|
262
|
+
errorMessage: message,
|
|
263
|
+
failureStage,
|
|
264
|
+
paymentMethod: 'card',
|
|
265
|
+
provider: 'solidgate',
|
|
266
|
+
});
|
|
267
|
+
}, []);
|
|
253
268
|
const requiresEmailCommit = Boolean(onCustomerEmailCommit);
|
|
254
269
|
useEffect(() => {
|
|
255
270
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -291,11 +306,12 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
291
306
|
var _a;
|
|
292
307
|
if (!controller.signal.aborted && !isAbortError(error)) {
|
|
293
308
|
setState({ state: 'recoverable_error', checkout: null, canResumeVerification: false });
|
|
309
|
+
trackFailure(copyRef.current.error, 'checkout_session_creation');
|
|
294
310
|
(_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, copyRef.current.error);
|
|
295
311
|
}
|
|
296
312
|
});
|
|
297
313
|
return () => controller.abort();
|
|
298
|
-
}, [committedEmail, normalizedRequest, open, requiresEmailCommit, setState]);
|
|
314
|
+
}, [committedEmail, normalizedRequest, open, requiresEmailCommit, setState, trackFailure]);
|
|
299
315
|
const paymentFormMounted = Boolean(view.checkout && mountedPaymentAttemptId === view.checkout.attemptId);
|
|
300
316
|
const showPreparingLoader = view.state === 'loading'
|
|
301
317
|
|| (view.state === 'ready' && !paymentFormMounted);
|
|
@@ -308,7 +324,18 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
308
324
|
? document.activeElement
|
|
309
325
|
: null;
|
|
310
326
|
const handleKeyDown = (event) => {
|
|
327
|
+
var _a;
|
|
311
328
|
if (event.key === 'Escape') {
|
|
329
|
+
const analytics = checkoutAnalyticsRef.current;
|
|
330
|
+
if (analytics) {
|
|
331
|
+
trackCheckoutCanceled({
|
|
332
|
+
analytics,
|
|
333
|
+
checkoutSessionId: (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId,
|
|
334
|
+
failureStage: 'checkout_dialog',
|
|
335
|
+
paymentMethod: 'card',
|
|
336
|
+
provider: 'solidgate',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
312
339
|
onCloseRef.current();
|
|
313
340
|
}
|
|
314
341
|
};
|
|
@@ -320,7 +347,7 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
320
347
|
window.removeEventListener('keydown', handleKeyDown);
|
|
321
348
|
previouslyFocused === null || previouslyFocused === void 0 ? void 0 : previouslyFocused.focus();
|
|
322
349
|
};
|
|
323
|
-
}, [open, showPreparingLoader]);
|
|
350
|
+
}, [open, showPreparingLoader, view.checkout]);
|
|
324
351
|
useEffect(() => () => {
|
|
325
352
|
var _a, _b;
|
|
326
353
|
(_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -356,9 +383,11 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
356
383
|
}
|
|
357
384
|
else if (result.status === 'expired') {
|
|
358
385
|
setState({ state: 'expired', checkout, canResumeVerification: false });
|
|
386
|
+
trackFailure(copyRef.current.expired, 'payment_expired', checkout.attemptId);
|
|
359
387
|
}
|
|
360
388
|
else if (result.status === 'failed') {
|
|
361
389
|
setState({ state: 'recoverable_error', checkout, canResumeVerification: false });
|
|
390
|
+
trackFailure(copyRef.current.error, 'payment_failed', checkout.attemptId);
|
|
362
391
|
(_c = onErrorRef.current) === null || _c === void 0 ? void 0 : _c.call(onErrorRef, copyRef.current.error);
|
|
363
392
|
}
|
|
364
393
|
else if (result.status === 'requires_payment') {
|
|
@@ -371,15 +400,18 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
371
400
|
catch (error) {
|
|
372
401
|
if (!controller.signal.aborted && !isAbortError(error)) {
|
|
373
402
|
setState({ state: 'recoverable_error', checkout, canResumeVerification: true });
|
|
403
|
+
trackFailure(copyRef.current.error, 'payment_verification', checkout.attemptId);
|
|
374
404
|
(_d = onErrorRef.current) === null || _d === void 0 ? void 0 : _d.call(onErrorRef, copyRef.current.error);
|
|
375
405
|
}
|
|
376
406
|
}
|
|
377
|
-
}, [normalizedRequest, setState, view]);
|
|
407
|
+
}, [normalizedRequest, setState, trackFailure, view]);
|
|
378
408
|
const commitCustomerEmail = useCallback(async (event) => {
|
|
409
|
+
var _a, _b;
|
|
379
410
|
event.preventDefault();
|
|
380
411
|
const normalized = normalizeValidEmail(emailInput);
|
|
381
412
|
if (!normalized || !onCustomerEmailCommit) {
|
|
382
413
|
setEmailError(copy.customerEmailInvalid);
|
|
414
|
+
trackFailure(copy.customerEmailInvalid, 'customer_email', (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId);
|
|
383
415
|
return;
|
|
384
416
|
}
|
|
385
417
|
setEmailSubmitting(true);
|
|
@@ -392,13 +424,28 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
392
424
|
setEmailInput(committed);
|
|
393
425
|
setCommittedEmail(committed);
|
|
394
426
|
}
|
|
395
|
-
catch (
|
|
427
|
+
catch (_c) {
|
|
396
428
|
setEmailError(copy.customerEmailUnavailable);
|
|
429
|
+
trackFailure(copy.customerEmailUnavailable, 'customer_email', (_b = view.checkout) === null || _b === void 0 ? void 0 : _b.attemptId);
|
|
397
430
|
}
|
|
398
431
|
finally {
|
|
399
432
|
setEmailSubmitting(false);
|
|
400
433
|
}
|
|
401
|
-
}, [copy.customerEmailInvalid, copy.customerEmailUnavailable, emailInput, onCustomerEmailCommit]);
|
|
434
|
+
}, [copy.customerEmailInvalid, copy.customerEmailUnavailable, emailInput, onCustomerEmailCommit, trackFailure, view.checkout]);
|
|
435
|
+
const handleClose = () => {
|
|
436
|
+
var _a;
|
|
437
|
+
const analytics = checkoutAnalyticsRef.current;
|
|
438
|
+
if (analytics) {
|
|
439
|
+
trackCheckoutCanceled({
|
|
440
|
+
analytics,
|
|
441
|
+
checkoutSessionId: (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId,
|
|
442
|
+
failureStage: 'checkout_dialog',
|
|
443
|
+
paymentMethod: 'card',
|
|
444
|
+
provider: 'solidgate',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
onCloseRef.current();
|
|
448
|
+
};
|
|
402
449
|
if (!open) {
|
|
403
450
|
return null;
|
|
404
451
|
}
|
|
@@ -429,7 +476,7 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
429
476
|
'solidgate-checkout-dialog',
|
|
430
477
|
showPreparingLoader ? 'is-preparing' : '',
|
|
431
478
|
presentation ? 'is-branded' : '',
|
|
432
|
-
].filter(Boolean).join(' '), children: [_jsxs("div", { className: 'solidgate-checkout-header', children: [_jsx("button", { ref: closeButtonRef, type: 'button', onClick:
|
|
479
|
+
].filter(Boolean).join(' '), children: [_jsxs("div", { className: 'solidgate-checkout-header', children: [_jsx("button", { ref: closeButtonRef, type: 'button', onClick: handleClose, "aria-label": copy.closeAriaLabel, className: 'solidgate-checkout-close', children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'solidgate-checkout-title', className: 'solidgate-checkout-title', children: (presentation === null || presentation === void 0 ? void 0 : presentation.title) || copy.title })] }), presentation ? (_jsxs("section", { className: 'solidgate-checkout-summary', children: [showPromo ? (_jsxs("p", { className: 'solidgate-checkout-countdown', children: [presentation.discountPercent, "% ", presentation.countdownLabel, ' ', formatCountdown(presentation.remainingSeconds), " min"] })) : null, _jsxs("p", { className: 'solidgate-checkout-satisfaction', children: [_jsx("strong", { children: presentation.satisfactionPercent }), ' ', presentation.satisfactionLabel] }), showPromo ? (_jsxs("div", { className: 'solidgate-checkout-price-stack', children: [_jsxs("div", { className: 'solidgate-checkout-price-row is-muted', children: [_jsx("span", { children: presentation.originalPriceLabel }), _jsx("span", { className: 'solidgate-checkout-strike', children: presentation.originalPriceValue })] }), _jsxs("div", { className: 'solidgate-checkout-price-row is-discount', children: [_jsx("strong", { children: presentation.discountLabel }), _jsx("strong", { children: presentation.discountAmountLabel })] }), _jsxs("div", { className: 'solidgate-checkout-promo', children: [_jsx("span", { children: presentation.promoCodeLabel }), _jsx("strong", { children: presentation.promoCode.trim() })] })] })) : null, _jsxs("div", { className: `solidgate-checkout-total${showPromo ? '' : ' is-plain'}`, children: [_jsx("span", { children: presentation.totalLabel }), _jsx("strong", { children: presentation.totalValue })] }), showPromo ? (_jsxs("p", { className: 'solidgate-checkout-saved', children: [_jsx("span", { "aria-hidden": 'true', children: "\uD83D\uDD25" }), presentation.savedLabel] })) : null] })) : null, view.state === 'collecting_email' ? (_jsxs("form", { className: 'solidgate-checkout-email-form', onSubmit: (event) => void commitCustomerEmail(event), children: [_jsxs("label", { className: 'solidgate-checkout-email-label', children: [copy.customerEmailLabel, _jsx("input", { type: 'email', autoComplete: 'email', inputMode: 'email', maxLength: 320, required: true, value: emailInput, onChange: (event) => {
|
|
433
480
|
setEmailInput(event.target.value);
|
|
434
481
|
setEmailError(null);
|
|
435
482
|
}, placeholder: copy.customerEmailPlaceholder, "aria-invalid": Boolean(emailError), className: 'solidgate-checkout-email-input' })] }), emailError ? (_jsx("p", { role: 'alert', className: 'solidgate-checkout-alert', children: emailError })) : null, _jsx("button", { type: 'submit', disabled: emailSubmitting, className: 'solidgate-checkout-action', children: copy.customerEmailContinue })] })) : null, view.checkout ? (_jsx(SolidgatePaymentForm, { buttonColor: (presentation === null || presentation === void 0 ? void 0 : presentation.buttonColor) || '#3f51b5', cardSubmitLabel: (presentation === null || presentation === void 0 ? void 0 : presentation.cardSubmitLabel) || 'Pay securely', checkout: view.checkout, hidden: !showForm, loadingLabel: copy.loading, onMounted: () => {
|
|
@@ -443,9 +490,10 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
|
|
|
443
490
|
}
|
|
444
491
|
setState(Object.assign(Object.assign({}, view), { state: 'submitting' }));
|
|
445
492
|
}, onVerify: () => void verifyCanonicalStatus(), onError: () => {
|
|
446
|
-
var _a;
|
|
493
|
+
var _a, _b;
|
|
447
494
|
setState(Object.assign(Object.assign({}, view), { state: 'recoverable_error', canResumeVerification: false }));
|
|
448
|
-
(_a =
|
|
495
|
+
trackFailure(copyRef.current.error, 'payment_form', (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId);
|
|
496
|
+
(_b = onErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onErrorRef, copyRef.current.error);
|
|
449
497
|
} }, view.checkout.attemptId)) : null, presentation && view.checkout && showForm ? (_jsx("p", { className: 'solidgate-checkout-secure', children: presentation.secureLabel })) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.state === 'verifying' && view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.retry })) : null] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
|
|
450
498
|
}
|
|
451
499
|
const solidgateCheckoutDialogStyles = `
|
|
@@ -11,12 +11,13 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
14
|
-
import { useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
|
|
15
15
|
import { CardCvcElement, CardExpiryElement, CardNumberElement, Elements, useElements, useStripe, } from '@stripe/react-stripe-js';
|
|
16
16
|
import { ApplePaySubscribeButton } from './ApplePaySubscribeButton.js';
|
|
17
17
|
import { PaymentBrandMark, SecurityLockIcon, StripeWordmark, } from './CheckoutBrandAssets.js';
|
|
18
18
|
import { StripeExpressCheckoutButton } from './StripeExpressCheckoutButton.js';
|
|
19
19
|
import { queueStripeSubscriptionCheckoutStarted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
20
|
+
import { trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
20
21
|
const isValidEmail = (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
21
22
|
const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'PayPal', 'G Pay', 'Apple Pay'];
|
|
22
23
|
const normalizeBillingName = (input) => {
|
|
@@ -141,7 +142,20 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
141
142
|
!submitting;
|
|
142
143
|
const walletCheckoutAllowed = allowApplePayExpressCheckout || allowApplePayQrFallback;
|
|
143
144
|
const walletFallbackDisabled = !walletCheckoutAllowed || walletAvailable === false;
|
|
144
|
-
const setSharedError = (message) => {
|
|
145
|
+
const setSharedError = (message, alreadyTracked = false, providerError) => {
|
|
146
|
+
if (message && checkoutAnalytics && !alreadyTracked) {
|
|
147
|
+
trackCheckoutFailureShown({
|
|
148
|
+
analytics: checkoutAnalytics,
|
|
149
|
+
checkoutSessionId,
|
|
150
|
+
errorMessage: message,
|
|
151
|
+
failureStage: 'card_form',
|
|
152
|
+
paymentMethod: 'card',
|
|
153
|
+
provider: 'stripe',
|
|
154
|
+
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
155
|
+
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
156
|
+
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
145
159
|
setError(message);
|
|
146
160
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
147
161
|
};
|
|
@@ -205,7 +219,7 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
205
219
|
return_url: returnUrl,
|
|
206
220
|
});
|
|
207
221
|
if (result.error) {
|
|
208
|
-
setSharedError(result.error.message || 'Payment failed.');
|
|
222
|
+
setSharedError(result.error.message || 'Payment failed.', false, result.error);
|
|
209
223
|
setSubmitting(false);
|
|
210
224
|
return;
|
|
211
225
|
}
|
|
@@ -227,7 +241,7 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
227
241
|
? 'is-positive'
|
|
228
242
|
: row.tone === 'negative'
|
|
229
243
|
? 'is-negative'
|
|
230
|
-
: undefined, children: row.value })] }, row.id))), _jsxs("div", { className: 'shared-stripe-checkout-summary-total', children: [_jsx("span", { children: totalLabel }), _jsx("strong", { children: totalValue })] }), summaryNote ? (_jsx("p", { className: 'shared-stripe-checkout-summary-note', children: summaryNote })) : null] }), _jsxs("form", { className: 'shared-stripe-checkout-form', onSubmit: (event) => void handleSubmit(event), children: [_jsxs("div", { className: 'shared-stripe-checkout-wallet-shell', children: [walletAvailable !== true ? (_jsx(ApplePaySubscribeButton, { className: 'shared-stripe-checkout-wallet-placeholder', disabled: walletFallbackDisabled, label: walletPlaceholderLabel })) : null, walletCheckoutAllowed ? (_jsx(StripeExpressCheckoutButton, { amountCents: amountCents, beforeConfirm: ensureCustomerEmail, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, summaryLabel: summaryLabel, returnUrl: returnUrl, customerEmail: resolvedCustomerEmail, customerName: customerName, className: 'shared-stripe-checkout-wallet-button', onAvailabilityChange: setWalletAvailable, onError: setSharedError })) : null] }), _jsxs("div", { className: 'shared-stripe-checkout-card-surface', children: [_jsxs("div", { className: 'shared-stripe-checkout-field-group shared-stripe-checkout-field-group--wide', children: [_jsx("p", { className: 'shared-stripe-checkout-field-label', children: customerEmailLabel }), _jsx("div", { className: 'shared-stripe-checkout-field shared-stripe-checkout-field--wide', children: _jsx("input", { type: 'email', inputMode: 'email', autoComplete: 'email', className: [
|
|
244
|
+
: undefined, children: row.value })] }, row.id))), _jsxs("div", { className: 'shared-stripe-checkout-summary-total', children: [_jsx("span", { children: totalLabel }), _jsx("strong", { children: totalValue })] }), summaryNote ? (_jsx("p", { className: 'shared-stripe-checkout-summary-note', children: summaryNote })) : null] }), _jsxs("form", { className: 'shared-stripe-checkout-form', onSubmit: (event) => void handleSubmit(event), children: [_jsxs("div", { className: 'shared-stripe-checkout-wallet-shell', children: [walletAvailable !== true ? (_jsx(ApplePaySubscribeButton, { className: 'shared-stripe-checkout-wallet-placeholder', disabled: walletFallbackDisabled, label: walletPlaceholderLabel })) : null, walletCheckoutAllowed ? (_jsx(StripeExpressCheckoutButton, { amountCents: amountCents, beforeConfirm: ensureCustomerEmail, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, summaryLabel: summaryLabel, returnUrl: returnUrl, customerEmail: resolvedCustomerEmail, customerName: customerName, className: 'shared-stripe-checkout-wallet-button', onAvailabilityChange: setWalletAvailable, onError: (message) => setSharedError(message, true) })) : null] }), _jsxs("div", { className: 'shared-stripe-checkout-card-surface', children: [_jsxs("div", { className: 'shared-stripe-checkout-field-group shared-stripe-checkout-field-group--wide', children: [_jsx("p", { className: 'shared-stripe-checkout-field-label', children: customerEmailLabel }), _jsx("div", { className: 'shared-stripe-checkout-field shared-stripe-checkout-field--wide', children: _jsx("input", { type: 'email', inputMode: 'email', autoComplete: 'email', className: [
|
|
231
245
|
'shared-stripe-checkout-text-input',
|
|
232
246
|
customerEmailEditable ? '' : 'is-readonly',
|
|
233
247
|
]
|
|
@@ -246,6 +260,17 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
246
260
|
}
|
|
247
261
|
export function SharedStripeCheckoutDialog(_a) {
|
|
248
262
|
var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise"]);
|
|
263
|
+
const handleClose = useCallback(() => {
|
|
264
|
+
if (checkoutAnalytics) {
|
|
265
|
+
trackCheckoutCanceled({
|
|
266
|
+
analytics: checkoutAnalytics,
|
|
267
|
+
checkoutSessionId,
|
|
268
|
+
failureStage: 'checkout_dialog',
|
|
269
|
+
provider: 'stripe',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
onClose();
|
|
273
|
+
}, [checkoutAnalytics, checkoutSessionId, onClose]);
|
|
249
274
|
useEffect(() => {
|
|
250
275
|
if (!checkoutAnalytics) {
|
|
251
276
|
return;
|
|
@@ -255,15 +280,15 @@ export function SharedStripeCheckoutDialog(_a) {
|
|
|
255
280
|
useEffect(() => {
|
|
256
281
|
const handleKeyDown = (event) => {
|
|
257
282
|
if (event.key === 'Escape') {
|
|
258
|
-
|
|
283
|
+
handleClose();
|
|
259
284
|
}
|
|
260
285
|
};
|
|
261
286
|
window.addEventListener('keydown', handleKeyDown);
|
|
262
287
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
263
|
-
}, [
|
|
264
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-stripe-checkout-overlay', role: 'presentation', onClick:
|
|
288
|
+
}, [handleClose]);
|
|
289
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-stripe-checkout-overlay', role: 'presentation', onClick: handleClose, children: _jsx("div", { className: 'shared-stripe-checkout-shell', role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'shared-stripe-checkout-title', onClick: (event) => event.stopPropagation(), children: _jsx(Elements, { stripe: stripePromise, options: {
|
|
265
290
|
clientSecret,
|
|
266
|
-
}, children: _jsx(SharedStripeCheckoutForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose:
|
|
291
|
+
}, children: _jsx(SharedStripeCheckoutForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose: handleClose })) }) }) }), _jsx("style", { children: sharedStripeCheckoutDialogStyles })] }));
|
|
267
292
|
}
|
|
268
293
|
const sharedStripeCheckoutDialogStyles = `
|
|
269
294
|
.shared-stripe-checkout-overlay {
|
|
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
return t;
|
|
12
12
|
};
|
|
13
13
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
14
|
-
import { useEffect, useMemo, useState, } from 'react';
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState, } from 'react';
|
|
15
15
|
import { CheckoutProvider, PaymentElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
|
|
16
16
|
import { PaymentBrandMark, SecurityLockIcon } from './CheckoutBrandAssets.js';
|
|
17
17
|
import { StripeCheckoutExpressCheckoutButton } from './StripeCheckoutExpressCheckoutButton.js';
|
|
@@ -19,6 +19,7 @@ import { getUnsupportedCheckoutCountryMessage, normalizeSupportedCheckoutCountri
|
|
|
19
19
|
import { isInactiveCheckoutSessionError } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
20
20
|
import { getStripeWalletPaymentMethodOrder, usePlatformWalletPaymentMethods, } from './walletPlatform.js';
|
|
21
21
|
import { queueStripeSubscriptionCheckoutStarted, trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
22
|
+
import { trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
22
23
|
const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'Maestro', 'Discover'];
|
|
23
24
|
const stripeCheckoutV2Appearance = {
|
|
24
25
|
labels: 'above',
|
|
@@ -69,6 +70,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
69
70
|
? true
|
|
70
71
|
: walletAvailable;
|
|
71
72
|
const [submitting, setSubmitting] = useState(false);
|
|
73
|
+
const [paymentElementReady, setPaymentElementReady] = useState(false);
|
|
72
74
|
const [error, setError] = useState(null);
|
|
73
75
|
const [billingCountry, setBillingCountry] = useState(null);
|
|
74
76
|
const initialCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || '';
|
|
@@ -151,11 +153,24 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
151
153
|
setError(null);
|
|
152
154
|
onInactiveCheckoutSession === null || onInactiveCheckoutSession === void 0 ? void 0 : onInactiveCheckoutSession();
|
|
153
155
|
};
|
|
154
|
-
const setSharedError = (message) => {
|
|
156
|
+
const setSharedError = (message, alreadyTracked = false, providerError) => {
|
|
155
157
|
if (isInactiveCheckoutSessionError(message)) {
|
|
156
158
|
handleInactiveCheckoutSession();
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
161
|
+
if (message && checkoutAnalytics && !alreadyTracked) {
|
|
162
|
+
trackCheckoutFailureShown({
|
|
163
|
+
analytics: checkoutAnalytics,
|
|
164
|
+
checkoutSessionId,
|
|
165
|
+
errorMessage: message,
|
|
166
|
+
failureStage: 'card_form',
|
|
167
|
+
paymentMethod: 'card',
|
|
168
|
+
provider: 'stripe',
|
|
169
|
+
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
170
|
+
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
171
|
+
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
159
174
|
setError(message);
|
|
160
175
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
161
176
|
};
|
|
@@ -249,6 +264,10 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
249
264
|
: 'Payment form is not ready yet.');
|
|
250
265
|
return;
|
|
251
266
|
}
|
|
267
|
+
if (!paymentElementReady) {
|
|
268
|
+
setSharedError('Payment form is not ready yet.', true);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
252
271
|
setSubmitting(true);
|
|
253
272
|
setSharedError(null);
|
|
254
273
|
try {
|
|
@@ -263,7 +282,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
263
282
|
});
|
|
264
283
|
if (result.type === 'error') {
|
|
265
284
|
const message = result.error.message || 'Payment failed.';
|
|
266
|
-
setSharedError(message);
|
|
285
|
+
setSharedError(message, false, result.error);
|
|
267
286
|
setSubmitting(false);
|
|
268
287
|
return;
|
|
269
288
|
}
|
|
@@ -300,7 +319,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
300
319
|
effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
|
|
301
320
|
]
|
|
302
321
|
.filter(Boolean)
|
|
303
|
-
.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: setSharedError, 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, 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: [
|
|
304
323
|
'shared-checkout-v2-card-panel',
|
|
305
324
|
effectiveSelectedMethod === 'card' ? 'is-visible' : '',
|
|
306
325
|
]
|
|
@@ -311,7 +330,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
311
330
|
}
|
|
312
331
|
setEmailDraft(event.target.value);
|
|
313
332
|
onCustomerEmailChange === null || onCustomerEmailChange === void 0 ? void 0 : onCustomerEmailChange(event.target.value);
|
|
314
|
-
} }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
|
|
333
|
+
} }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange, onReady: () => setPaymentElementReady(true) }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting || !paymentElementReady, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
|
|
315
334
|
}
|
|
316
335
|
function CheckoutSessionProvider({ children, clientSecret, fixedBillingCountry, stripePromise, variant, }) {
|
|
317
336
|
const providerChildren = children;
|
|
@@ -336,6 +355,17 @@ export function SharedStripeCheckoutV2Dialog(_a) {
|
|
|
336
355
|
var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries, variant = 'standard' } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise", "supportedCountries", "variant"]);
|
|
337
356
|
const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
|
|
338
357
|
const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
|
|
358
|
+
const handleClose = useCallback(() => {
|
|
359
|
+
if (checkoutAnalytics) {
|
|
360
|
+
trackCheckoutCanceled({
|
|
361
|
+
analytics: checkoutAnalytics,
|
|
362
|
+
checkoutSessionId,
|
|
363
|
+
failureStage: 'checkout_dialog',
|
|
364
|
+
provider: 'stripe',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
onClose();
|
|
368
|
+
}, [checkoutAnalytics, checkoutSessionId, onClose]);
|
|
339
369
|
useEffect(() => {
|
|
340
370
|
if (!checkoutAnalytics) {
|
|
341
371
|
return;
|
|
@@ -345,13 +375,13 @@ export function SharedStripeCheckoutV2Dialog(_a) {
|
|
|
345
375
|
useEffect(() => {
|
|
346
376
|
const handleKeyDown = (event) => {
|
|
347
377
|
if (event.key === 'Escape') {
|
|
348
|
-
|
|
378
|
+
handleClose();
|
|
349
379
|
}
|
|
350
380
|
};
|
|
351
381
|
window.addEventListener('keydown', handleKeyDown);
|
|
352
382
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
353
|
-
}, [
|
|
354
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-checkout-v2-overlay', children: _jsx("div", { className: 'shared-checkout-v2-dialog', "data-variant": variant, role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'shared-checkout-v2-title', children: _jsx(CheckoutSessionProvider, { clientSecret: clientSecret, fixedBillingCountry: fixedBillingCountry, stripePromise: stripePromise, variant: variant, children: _jsx(SharedStripeCheckoutV2CheckoutSessionForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose:
|
|
383
|
+
}, [handleClose]);
|
|
384
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-checkout-v2-overlay', children: _jsx("div", { className: 'shared-checkout-v2-dialog', "data-variant": variant, role: 'dialog', "aria-modal": 'true', "aria-labelledby": 'shared-checkout-v2-title', children: _jsx(CheckoutSessionProvider, { clientSecret: clientSecret, fixedBillingCountry: fixedBillingCountry, stripePromise: stripePromise, variant: variant, children: _jsx(SharedStripeCheckoutV2CheckoutSessionForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose: handleClose, supportedCountries: normalizedSupportedCountries, variant: variant })) }) }) }), _jsx("style", { children: sharedStripeCheckoutV2Styles })] }));
|
|
355
385
|
}
|
|
356
386
|
function SharedCheckoutSpecialOfferGift({ discountLabel, imageAlt, imageSrc, }) {
|
|
357
387
|
if (imageSrc) {
|
|
@@ -4,6 +4,7 @@ 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
8
|
const ExpressCheckoutElementWithCancel = ExpressCheckoutElement;
|
|
8
9
|
const buildDefaultOptions = () => ({
|
|
9
10
|
buttonHeight: 55,
|
|
@@ -43,7 +44,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
43
44
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
44
45
|
return Object.assign(Object.assign(Object.assign({}, baseOptions), (options !== null && options !== void 0 ? options : {})), { buttonTheme: Object.assign(Object.assign({}, ((_a = baseOptions.buttonTheme) !== null && _a !== void 0 ? _a : {})), ((_b = options === null || options === void 0 ? void 0 : options.buttonTheme) !== null && _b !== void 0 ? _b : {})), buttonType: Object.assign(Object.assign({}, ((_c = baseOptions.buttonType) !== null && _c !== void 0 ? _c : {})), ((_d = options === null || options === void 0 ? void 0 : options.buttonType) !== null && _d !== void 0 ? _d : {})), layout: Object.assign(Object.assign({}, ((_e = baseOptions.layout) !== null && _e !== void 0 ? _e : {})), ((_f = options === null || options === void 0 ? void 0 : options.layout) !== null && _f !== void 0 ? _f : {})), paymentMethods: Object.assign(Object.assign({}, ((_g = baseOptions.paymentMethods) !== null && _g !== void 0 ? _g : {})), ((_h = options === null || options === void 0 ? void 0 : options.paymentMethods) !== null && _h !== void 0 ? _h : {})), paymentMethodOrder: (_j = options === null || options === void 0 ? void 0 : options.paymentMethodOrder) !== null && _j !== void 0 ? _j : baseOptions.paymentMethodOrder });
|
|
45
46
|
}, [baseOptions, options]);
|
|
46
|
-
const ensureServerUpdated = useCallback(async () => {
|
|
47
|
+
const ensureServerUpdated = useCallback(async (paymentMethod) => {
|
|
47
48
|
if (!normalizedServerUpdateKey) {
|
|
48
49
|
return true;
|
|
49
50
|
}
|
|
@@ -57,14 +58,30 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
57
58
|
return serverUpdatePromiseRef.current;
|
|
58
59
|
}
|
|
59
60
|
const updatePromise = (async () => {
|
|
61
|
+
let updateCallbackFailed = false;
|
|
60
62
|
const updateResult = await checkoutState.checkout.runServerUpdate(async () => {
|
|
61
63
|
const updated = await onServerUpdate();
|
|
62
64
|
if (!updated) {
|
|
65
|
+
updateCallbackFailed = true;
|
|
63
66
|
throw new Error('Unable to update checkout session.');
|
|
64
67
|
}
|
|
65
68
|
});
|
|
66
69
|
if (updateResult.type === 'error') {
|
|
67
|
-
|
|
70
|
+
if (updateCallbackFailed) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const message = updateResult.error.message || 'Unable to update checkout session.';
|
|
74
|
+
if (checkoutAnalytics) {
|
|
75
|
+
trackCheckoutFailureShown({
|
|
76
|
+
analytics: checkoutAnalytics,
|
|
77
|
+
checkoutSessionId,
|
|
78
|
+
errorMessage: message,
|
|
79
|
+
failureStage: 'wallet_session_update',
|
|
80
|
+
paymentMethod,
|
|
81
|
+
provider: 'stripe',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
68
85
|
return false;
|
|
69
86
|
}
|
|
70
87
|
appliedServerUpdateKeyRef.current = normalizedServerUpdateKey;
|
|
@@ -78,7 +95,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
78
95
|
finally {
|
|
79
96
|
serverUpdatePromiseRef.current = null;
|
|
80
97
|
}
|
|
81
|
-
}, [checkoutState, normalizedServerUpdateKey, onError, onServerUpdate]);
|
|
98
|
+
}, [checkoutAnalytics, checkoutSessionId, checkoutState, normalizedServerUpdateKey, onError, onServerUpdate]);
|
|
82
99
|
useEffect(() => {
|
|
83
100
|
if (!normalizedServerUpdateKey) {
|
|
84
101
|
appliedServerUpdateKeyRef.current = '';
|
|
@@ -103,6 +120,22 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
103
120
|
checkoutStartSource,
|
|
104
121
|
paymentMethod }));
|
|
105
122
|
};
|
|
123
|
+
const reportFailure = (message, failureStage, paymentMethod, providerError) => {
|
|
124
|
+
if (checkoutAnalytics) {
|
|
125
|
+
trackCheckoutFailureShown({
|
|
126
|
+
analytics: checkoutAnalytics,
|
|
127
|
+
checkoutSessionId,
|
|
128
|
+
errorMessage: message,
|
|
129
|
+
failureStage,
|
|
130
|
+
paymentMethod,
|
|
131
|
+
provider: 'stripe',
|
|
132
|
+
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
133
|
+
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
134
|
+
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
138
|
+
};
|
|
106
139
|
const handleConfirm = async (event) => {
|
|
107
140
|
var _a, _b, _c;
|
|
108
141
|
// Checkout Sessions omits the Express Checkout onClick callback. Confirmation
|
|
@@ -113,7 +146,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
113
146
|
const message = checkoutState.type === 'error'
|
|
114
147
|
? checkoutState.error.message
|
|
115
148
|
: 'Wallet checkout is not ready yet.';
|
|
116
|
-
|
|
149
|
+
reportFailure(message, 'wallet_checkout_not_ready', event.expressPaymentType);
|
|
117
150
|
event.paymentFailed({ reason: 'fail', message });
|
|
118
151
|
return;
|
|
119
152
|
}
|
|
@@ -124,14 +157,13 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
124
157
|
supportedCountries: normalizedSupportedCountries,
|
|
125
158
|
});
|
|
126
159
|
if (unsupportedCountryError) {
|
|
127
|
-
|
|
160
|
+
reportFailure(unsupportedCountryError, 'wallet_billing_country', event.expressPaymentType);
|
|
128
161
|
event.paymentFailed({ reason: 'invalid_billing_address', message: unsupportedCountryError });
|
|
129
162
|
return;
|
|
130
163
|
}
|
|
131
|
-
const updated = await ensureServerUpdated();
|
|
164
|
+
const updated = await ensureServerUpdated(event.expressPaymentType);
|
|
132
165
|
if (!updated) {
|
|
133
166
|
const message = 'Unable to update checkout session.';
|
|
134
|
-
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
135
167
|
event.paymentFailed({ reason: 'fail', message });
|
|
136
168
|
return;
|
|
137
169
|
}
|
|
@@ -144,14 +176,23 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
144
176
|
const message = confirmError instanceof Error && confirmError.message
|
|
145
177
|
? confirmError.message
|
|
146
178
|
: 'Unable to prepare wallet checkout.';
|
|
147
|
-
|
|
179
|
+
reportFailure(message, 'wallet_before_confirm', event.expressPaymentType);
|
|
148
180
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
149
181
|
return;
|
|
150
182
|
}
|
|
151
183
|
}
|
|
152
184
|
if (beforeConfirm && !preparedCustomerEmail) {
|
|
153
185
|
const message = 'Email is required to continue.';
|
|
154
|
-
|
|
186
|
+
if (checkoutAnalytics) {
|
|
187
|
+
trackCheckoutCanceled({
|
|
188
|
+
analytics: checkoutAnalytics,
|
|
189
|
+
checkoutSessionId,
|
|
190
|
+
failureStage: 'wallet_preconfirm',
|
|
191
|
+
paymentMethod: event.expressPaymentType,
|
|
192
|
+
provider: 'stripe',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
reportFailure(message, 'wallet_email_required', event.expressPaymentType);
|
|
155
196
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
156
197
|
return;
|
|
157
198
|
}
|
|
@@ -164,7 +205,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
164
205
|
: {})), { expressCheckoutConfirmEvent: event, redirect: 'if_required' }));
|
|
165
206
|
if (result.type === 'error') {
|
|
166
207
|
const message = result.error.message || 'Wallet checkout failed.';
|
|
167
|
-
|
|
208
|
+
reportFailure(message, 'wallet_confirmation', event.expressPaymentType, result.error);
|
|
168
209
|
event.paymentFailed({ reason: 'fail', message });
|
|
169
210
|
return;
|
|
170
211
|
}
|
|
@@ -184,7 +225,17 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
184
225
|
// Cancel has no method field; only single-method placements can identify it.
|
|
185
226
|
const enabledMethods = Object.entries((_a = resolvedOptions.paymentMethods) !== null && _a !== void 0 ? _a : {})
|
|
186
227
|
.filter(([, availability]) => availability !== 'never');
|
|
187
|
-
|
|
228
|
+
const paymentMethod = enabledMethods.length === 1 ? enabledMethods[0][0] : undefined;
|
|
229
|
+
queueCheckoutStarted(paymentMethod, 'wallet_cancel');
|
|
230
|
+
if (checkoutAnalytics) {
|
|
231
|
+
trackCheckoutCanceled({
|
|
232
|
+
analytics: checkoutAnalytics,
|
|
233
|
+
checkoutSessionId,
|
|
234
|
+
failureStage: 'wallet_sheet',
|
|
235
|
+
paymentMethod,
|
|
236
|
+
provider: 'stripe',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
188
239
|
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
189
240
|
};
|
|
190
241
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
|
|
@@ -196,7 +247,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
196
247
|
.join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: handleCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
|
|
197
248
|
setWalletAvailable(false);
|
|
198
249
|
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
|
|
199
|
-
|
|
250
|
+
reportFailure(error.message || 'Unable to load wallet checkout.', 'wallet_load');
|
|
200
251
|
}, onReady: ({ availablePaymentMethods: availablePaymentMethodsMap }) => {
|
|
201
252
|
const available = Boolean(availabilityPaymentMethods.some((paymentMethod) => availablePaymentMethodsMap === null || availablePaymentMethodsMap === void 0 ? void 0 : availablePaymentMethodsMap[paymentMethod]));
|
|
202
253
|
const resolvedAvailable = available || (keepInitialAvailableOnReady && initialAvailable === true);
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { useMemo, useState } from 'react';
|
|
4
4
|
import { ExpressCheckoutElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
5
5
|
import { queueStripeSubscriptionCheckoutStarted, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
6
|
+
import { trackCheckoutCanceled, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
6
7
|
const normalizeBillingName = (input) => {
|
|
7
8
|
var _a, _b, _c;
|
|
8
9
|
const normalizedName = (_a = input.name) === null || _a === void 0 ? void 0 : _a.trim();
|
|
@@ -69,11 +70,27 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
69
70
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
70
71
|
return Object.assign(Object.assign(Object.assign({}, baseOptions), (options !== null && options !== void 0 ? options : {})), { buttonTheme: Object.assign(Object.assign({}, ((_a = baseOptions.buttonTheme) !== null && _a !== void 0 ? _a : {})), ((_b = options === null || options === void 0 ? void 0 : options.buttonTheme) !== null && _b !== void 0 ? _b : {})), buttonType: Object.assign(Object.assign({}, ((_c = baseOptions.buttonType) !== null && _c !== void 0 ? _c : {})), ((_d = options === null || options === void 0 ? void 0 : options.buttonType) !== null && _d !== void 0 ? _d : {})), layout: Object.assign(Object.assign({}, ((_e = baseOptions.layout) !== null && _e !== void 0 ? _e : {})), ((_f = options === null || options === void 0 ? void 0 : options.layout) !== null && _f !== void 0 ? _f : {})), paymentMethods: Object.assign(Object.assign({}, ((_g = baseOptions.paymentMethods) !== null && _g !== void 0 ? _g : {})), ((_h = options === null || options === void 0 ? void 0 : options.paymentMethods) !== null && _h !== void 0 ? _h : {})), lineItems: (_j = options === null || options === void 0 ? void 0 : options.lineItems) !== null && _j !== void 0 ? _j : baseOptions.lineItems, paymentMethodOrder: (_k = options === null || options === void 0 ? void 0 : options.paymentMethodOrder) !== null && _k !== void 0 ? _k : baseOptions.paymentMethodOrder });
|
|
71
72
|
}, [baseOptions, options]);
|
|
73
|
+
const reportFailure = (message, failureStage, paymentMethod, providerError) => {
|
|
74
|
+
if (checkoutAnalytics) {
|
|
75
|
+
trackCheckoutFailureShown({
|
|
76
|
+
analytics: checkoutAnalytics,
|
|
77
|
+
checkoutSessionId,
|
|
78
|
+
errorMessage: message,
|
|
79
|
+
failureStage,
|
|
80
|
+
paymentMethod,
|
|
81
|
+
provider: 'stripe',
|
|
82
|
+
providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code,
|
|
83
|
+
providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code,
|
|
84
|
+
providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
88
|
+
};
|
|
72
89
|
const handleConfirm = async (event) => {
|
|
73
90
|
var _a;
|
|
74
91
|
if (!stripe || !elements) {
|
|
75
92
|
const message = 'Wallet checkout is not ready yet.';
|
|
76
|
-
|
|
93
|
+
reportFailure(message, 'wallet_checkout_not_ready', event.expressPaymentType);
|
|
77
94
|
event.paymentFailed({ reason: 'fail', message });
|
|
78
95
|
return;
|
|
79
96
|
}
|
|
@@ -87,21 +104,30 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
87
104
|
const message = confirmError instanceof Error && confirmError.message
|
|
88
105
|
? confirmError.message
|
|
89
106
|
: 'Unable to prepare wallet checkout.';
|
|
90
|
-
|
|
107
|
+
reportFailure(message, 'wallet_before_confirm', event.expressPaymentType);
|
|
91
108
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
92
109
|
return;
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
if (beforeConfirm && !preparedCustomerEmail) {
|
|
96
113
|
const message = 'Email is required to continue.';
|
|
97
|
-
|
|
114
|
+
if (checkoutAnalytics) {
|
|
115
|
+
trackCheckoutCanceled({
|
|
116
|
+
analytics: checkoutAnalytics,
|
|
117
|
+
checkoutSessionId,
|
|
118
|
+
failureStage: 'wallet_preconfirm',
|
|
119
|
+
paymentMethod: event.expressPaymentType,
|
|
120
|
+
provider: 'stripe',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
reportFailure(message, 'wallet_email_required', event.expressPaymentType);
|
|
98
124
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
99
125
|
return;
|
|
100
126
|
}
|
|
101
127
|
const submitResult = await elements.submit();
|
|
102
128
|
if (submitResult.error) {
|
|
103
129
|
const message = submitResult.error.message || 'Unable to start wallet checkout.';
|
|
104
|
-
|
|
130
|
+
reportFailure(message, 'wallet_payment_details', event.expressPaymentType);
|
|
105
131
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
106
132
|
return;
|
|
107
133
|
}
|
|
@@ -127,7 +153,7 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
127
153
|
});
|
|
128
154
|
if (result.error) {
|
|
129
155
|
const message = result.error.message || 'Wallet checkout failed.';
|
|
130
|
-
|
|
156
|
+
reportFailure(message, 'wallet_confirmation', event.expressPaymentType, result.error);
|
|
131
157
|
event.paymentFailed({ reason: 'fail', message });
|
|
132
158
|
return;
|
|
133
159
|
}
|
|
@@ -144,16 +170,32 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
144
170
|
}
|
|
145
171
|
event.resolve();
|
|
146
172
|
};
|
|
173
|
+
const handleCancel = () => {
|
|
174
|
+
var _a;
|
|
175
|
+
const enabledMethods = Object.entries((_a = resolvedOptions.paymentMethods) !== null && _a !== void 0 ? _a : {})
|
|
176
|
+
.filter(([, availability]) => availability !== 'never');
|
|
177
|
+
const paymentMethod = enabledMethods.length === 1 ? enabledMethods[0][0] : undefined;
|
|
178
|
+
if (checkoutAnalytics) {
|
|
179
|
+
trackCheckoutCanceled({
|
|
180
|
+
analytics: checkoutAnalytics,
|
|
181
|
+
checkoutSessionId,
|
|
182
|
+
failureStage: 'wallet_sheet',
|
|
183
|
+
paymentMethod,
|
|
184
|
+
provider: 'stripe',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
188
|
+
};
|
|
147
189
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
|
|
148
190
|
'stripe-express-checkout-button',
|
|
149
191
|
className !== null && className !== void 0 ? className : '',
|
|
150
192
|
effectiveWalletAvailable === false ? 'is-hidden' : '',
|
|
151
193
|
]
|
|
152
194
|
.filter(Boolean)
|
|
153
|
-
.join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel:
|
|
195
|
+
.join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel: handleCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
|
|
154
196
|
setWalletAvailable(false);
|
|
155
197
|
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
|
|
156
|
-
|
|
198
|
+
reportFailure(error.message || 'Unable to load wallet checkout.', 'wallet_load');
|
|
157
199
|
}, onReady: ({ availablePaymentMethods: availablePaymentMethodsMap }) => {
|
|
158
200
|
const available = Boolean(availabilityPaymentMethods.some((paymentMethod) => availablePaymentMethodsMap === null || availablePaymentMethodsMap === void 0 ? void 0 : availablePaymentMethodsMap[paymentMethod]));
|
|
159
201
|
const resolvedAvailable = available || (keepInitialAvailableOnReady && initialAvailable === true);
|
|
@@ -20,6 +20,6 @@ export function StripeSubscriptionWalletSurface(_a) {
|
|
|
20
20
|
const normalizedSurfaceId = surfaceId.trim();
|
|
21
21
|
const { customerName } = checkout, checkoutSessionInput = __rest(checkout, ["customerName"]);
|
|
22
22
|
const surfaceAnalyticsMetadata = useMemo(() => buildStripeSubscriptionWalletSurfaceAnalyticsMetadata(checkout.analyticsMetadata, normalizedSurfaceId), [checkout.analyticsMetadata, normalizedSurfaceId]);
|
|
23
|
-
const session = useStripeSubscriptionCheckoutSession(Object.assign(Object.assign({}, checkoutSessionInput), { analyticsMetadata: surfaceAnalyticsMetadata }));
|
|
23
|
+
const session = useStripeSubscriptionCheckoutSession(Object.assign(Object.assign({}, checkoutSessionInput), { analyticsMetadata: surfaceAnalyticsMetadata, checkoutAnalytics: surfaceProps.checkoutAnalytics }));
|
|
24
24
|
return (_jsx(StripeWalletSurface, Object.assign({}, surfaceProps, { customerEmail: checkout.customerEmail, customerName: customerName, returnUrl: checkout.returnUrl, session: session, surfaceId: normalizedSurfaceId })));
|
|
25
25
|
}
|
|
@@ -3,8 +3,10 @@ import type { RuntimeMode } from '@funnelsgrove/runtime';
|
|
|
3
3
|
import { type CheckoutPreparationLoading, type CheckoutPreparationSource } from '../../paymentProvider.types.js';
|
|
4
4
|
import type { PaywallDisplayPlan } from '../../../services/paywallOffer.service.js';
|
|
5
5
|
import { type PaywallPlan, type StripeRuntimeConfigOverrides } from '../services/stripe.service.js';
|
|
6
|
+
import { type CheckoutObservabilityAnalyticsContext } from '../../../services/checkoutObservability.service.js';
|
|
6
7
|
export type StripeSubscriptionCheckoutSessionInput = {
|
|
7
8
|
analyticsMetadata?: Record<string, unknown> | null;
|
|
9
|
+
checkoutAnalytics?: CheckoutObservabilityAnalyticsContext | null;
|
|
8
10
|
checkoutMode: RuntimeMode;
|
|
9
11
|
couponId?: string | null;
|
|
10
12
|
customerEmail?: string | null;
|
|
@@ -52,4 +54,5 @@ export type StripeSubscriptionCheckoutIntentKeyInput = Pick<StripeSubscriptionCh
|
|
|
52
54
|
export declare function buildStripeSubscriptionCheckoutIntentKey({ checkoutMode, couponId, customerEmail, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutIntentKeyInput): string;
|
|
53
55
|
export declare function shouldResetStripeSubscriptionCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
|
|
54
56
|
export declare function isInactiveCheckoutSessionError(message?: string | null): boolean;
|
|
55
|
-
export declare
|
|
57
|
+
export declare const shouldReportStripeCheckoutFailureShown: (message: string) => boolean;
|
|
58
|
+
export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
|
|
@@ -14,6 +14,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
|
|
|
14
14
|
import { createEmptyCheckoutPreparationLoading, } from '../../paymentProvider.types.js';
|
|
15
15
|
import { createStripeSubscriptionCheckout, getStripePromise, isStripeConfigured, updateStripeSubscriptionCheckoutPlan, } from '../services/stripe.service.js';
|
|
16
16
|
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
17
|
+
import { resolveCheckoutObservabilityAnalyticsContext, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
17
18
|
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
18
19
|
export function resolveStripeSubscriptionCheckoutPreparationOptions(source, options = {}) {
|
|
19
20
|
var _a, _b;
|
|
@@ -68,7 +69,8 @@ export function isInactiveCheckoutSessionError(message) {
|
|
|
68
69
|
var _a;
|
|
69
70
|
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 : '');
|
|
70
71
|
}
|
|
71
|
-
export
|
|
72
|
+
export const shouldReportStripeCheckoutFailureShown = (message) => !isInactiveCheckoutSessionError(message);
|
|
73
|
+
export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }) {
|
|
72
74
|
var _a, _b, _c, _d, _e;
|
|
73
75
|
const [clientSecret, setClientSecret] = useState(null);
|
|
74
76
|
const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
|
|
@@ -218,7 +220,24 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
218
220
|
catch (subscriptionError) {
|
|
219
221
|
if (((_f = creatingIntentRef.current) === null || _f === void 0 ? void 0 : _f.key) === requestKey &&
|
|
220
222
|
((_g = creatingIntentRef.current) === null || _g === void 0 ? void 0 : _g.id) === requestId) {
|
|
221
|
-
|
|
223
|
+
const message = getFriendlyStripeCheckoutError(subscriptionError);
|
|
224
|
+
const observabilityAnalytics = resolveCheckoutObservabilityAnalyticsContext({
|
|
225
|
+
analytics: checkoutAnalytics,
|
|
226
|
+
environment: checkoutMode,
|
|
227
|
+
metadata: analyticsMetadataRef.current,
|
|
228
|
+
runtimeConfig,
|
|
229
|
+
});
|
|
230
|
+
if (observabilityAnalytics
|
|
231
|
+
&& shouldReportStripeCheckoutFailureShown(message)) {
|
|
232
|
+
trackCheckoutFailureShown({
|
|
233
|
+
analytics: observabilityAnalytics,
|
|
234
|
+
errorMessage: message,
|
|
235
|
+
failureStage: 'checkout_session_creation',
|
|
236
|
+
paymentMethod: options.paymentMethod,
|
|
237
|
+
provider: 'stripe',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
setError(message);
|
|
222
241
|
}
|
|
223
242
|
return null;
|
|
224
243
|
}
|
|
@@ -238,6 +257,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
238
257
|
}, [
|
|
239
258
|
activeClientSecret,
|
|
240
259
|
checkoutMode,
|
|
260
|
+
checkoutAnalytics,
|
|
241
261
|
clearActiveCheckoutSession,
|
|
242
262
|
configured,
|
|
243
263
|
couponId,
|
|
@@ -308,13 +328,31 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
308
328
|
return true;
|
|
309
329
|
}
|
|
310
330
|
catch (subscriptionError) {
|
|
311
|
-
|
|
331
|
+
const message = getFriendlyStripeCheckoutError(subscriptionError);
|
|
332
|
+
const observabilityAnalytics = resolveCheckoutObservabilityAnalyticsContext({
|
|
333
|
+
analytics: checkoutAnalytics,
|
|
334
|
+
environment: checkoutMode,
|
|
335
|
+
metadata: analyticsMetadataRef.current,
|
|
336
|
+
runtimeConfig,
|
|
337
|
+
});
|
|
338
|
+
if (observabilityAnalytics
|
|
339
|
+
&& shouldReportStripeCheckoutFailureShown(message)) {
|
|
340
|
+
trackCheckoutFailureShown({
|
|
341
|
+
analytics: observabilityAnalytics,
|
|
342
|
+
checkoutSessionId: activeCheckoutSessionId,
|
|
343
|
+
errorMessage: message,
|
|
344
|
+
failureStage: 'checkout_session_update',
|
|
345
|
+
provider: 'stripe',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
setError(message);
|
|
312
349
|
return false;
|
|
313
350
|
}
|
|
314
351
|
}, [
|
|
315
352
|
activeCheckoutSessionId,
|
|
316
353
|
activePlanKey,
|
|
317
354
|
checkoutMode,
|
|
355
|
+
checkoutAnalytics,
|
|
318
356
|
configured,
|
|
319
357
|
couponId,
|
|
320
358
|
customerEmail,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FunnelSdkRuntimeConfigOverrides, type FunnelStepType, type RuntimeMode } from '@funnelsgrove/runtime';
|
|
2
|
+
export type CheckoutObservabilityAnalyticsContext = {
|
|
3
|
+
environment?: RuntimeMode;
|
|
4
|
+
featureFlags?: Record<string, string | null | undefined>;
|
|
5
|
+
metadata?: Record<string, unknown> | null;
|
|
6
|
+
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
7
|
+
stepId?: string;
|
|
8
|
+
stepName?: string;
|
|
9
|
+
stepType?: Extract<FunnelStepType, 'checkout' | 'paywall_offer' | 'upsell_offer'>;
|
|
10
|
+
stepContractVersion?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare const resolveCheckoutObservabilityAnalyticsContext: (input: {
|
|
13
|
+
analytics?: CheckoutObservabilityAnalyticsContext | null;
|
|
14
|
+
environment?: RuntimeMode;
|
|
15
|
+
metadata?: Record<string, unknown> | null;
|
|
16
|
+
runtimeConfig?: FunnelSdkRuntimeConfigOverrides;
|
|
17
|
+
}) => CheckoutObservabilityAnalyticsContext;
|
|
18
|
+
type CheckoutObservabilityInput = {
|
|
19
|
+
analytics: CheckoutObservabilityAnalyticsContext;
|
|
20
|
+
checkoutSessionId?: string | null;
|
|
21
|
+
failureStage: string;
|
|
22
|
+
paymentMethod?: unknown;
|
|
23
|
+
provider: 'solidgate' | 'stripe';
|
|
24
|
+
};
|
|
25
|
+
type CheckoutFailureShownInput = CheckoutObservabilityInput & {
|
|
26
|
+
errorCode?: string | null;
|
|
27
|
+
errorMessage: string;
|
|
28
|
+
providerDeclineCode?: string | null;
|
|
29
|
+
providerErrorCode?: string | null;
|
|
30
|
+
providerErrorType?: string | null;
|
|
31
|
+
};
|
|
32
|
+
export declare const trackCheckoutFailureShown: (input: CheckoutFailureShownInput) => string | null;
|
|
33
|
+
export declare const trackCheckoutCanceled: (input: CheckoutObservabilityInput) => string | null;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
|
|
2
|
+
import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, } from '@funnelsgrove/runtime';
|
|
3
|
+
import { toAnalyticsPaymentMethod } from './paymentMethodAnalytics.service.js';
|
|
4
|
+
const asNonEmptyString = (value) => typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
5
|
+
export const resolveCheckoutObservabilityAnalyticsContext = (input) => {
|
|
6
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
7
|
+
const metadata = Object.assign(Object.assign({}, ((_a = input.metadata) !== null && _a !== void 0 ? _a : {})), ((_c = (_b = input.analytics) === null || _b === void 0 ? void 0 : _b.metadata) !== null && _c !== void 0 ? _c : {}));
|
|
8
|
+
const stepId = (_d = asNonEmptyString(metadata.step_id)) !== null && _d !== void 0 ? _d : asNonEmptyString(metadata.stepId);
|
|
9
|
+
const stepName = (_e = asNonEmptyString(metadata.step_name)) !== null && _e !== void 0 ? _e : asNonEmptyString(metadata.stepName);
|
|
10
|
+
const stepType = (_f = asNonEmptyString(metadata.step_type)) !== null && _f !== void 0 ? _f : asNonEmptyString(metadata.stepType);
|
|
11
|
+
const stepContractVersion = Number((_g = metadata.step_contract_version) !== null && _g !== void 0 ? _g : metadata.stepContractVersion);
|
|
12
|
+
const validStepType = ['checkout', 'paywall_offer', 'upsell_offer'].includes(stepType !== null && stepType !== void 0 ? stepType : '')
|
|
13
|
+
? stepType
|
|
14
|
+
: undefined;
|
|
15
|
+
const validStepContractVersion = Number.isInteger(stepContractVersion) && stepContractVersion >= 1
|
|
16
|
+
? stepContractVersion
|
|
17
|
+
: undefined;
|
|
18
|
+
return {
|
|
19
|
+
environment: (_j = (_h = input.analytics) === null || _h === void 0 ? void 0 : _h.environment) !== null && _j !== void 0 ? _j : input.environment,
|
|
20
|
+
featureFlags: (_k = input.analytics) === null || _k === void 0 ? void 0 : _k.featureFlags,
|
|
21
|
+
metadata,
|
|
22
|
+
runtimeConfig: (_m = (_l = input.analytics) === null || _l === void 0 ? void 0 : _l.runtimeConfig) !== null && _m !== void 0 ? _m : input.runtimeConfig,
|
|
23
|
+
stepId: (_q = (_p = (_o = input.analytics) === null || _o === void 0 ? void 0 : _o.stepId) !== null && _p !== void 0 ? _p : stepId) !== null && _q !== void 0 ? _q : undefined,
|
|
24
|
+
stepName: (_t = (_s = (_r = input.analytics) === null || _r === void 0 ? void 0 : _r.stepName) !== null && _s !== void 0 ? _s : stepName) !== null && _t !== void 0 ? _t : undefined,
|
|
25
|
+
stepType: (_v = (_u = input.analytics) === null || _u === void 0 ? void 0 : _u.stepType) !== null && _v !== void 0 ? _v : validStepType,
|
|
26
|
+
stepContractVersion: (_x = (_w = input.analytics) === null || _w === void 0 ? void 0 : _w.stepContractVersion) !== null && _x !== void 0 ? _x : validStepContractVersion,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const STRIPE_PAYMENT_REJECTION_CODES = new Set([
|
|
30
|
+
'card_declined',
|
|
31
|
+
'expired_card',
|
|
32
|
+
'incorrect_cvc',
|
|
33
|
+
'incorrect_number',
|
|
34
|
+
'incorrect_zip',
|
|
35
|
+
'insufficient_funds',
|
|
36
|
+
'invalid_cvc',
|
|
37
|
+
'invalid_expiry_month',
|
|
38
|
+
'invalid_expiry_year',
|
|
39
|
+
'payment_intent_authentication_failure',
|
|
40
|
+
]);
|
|
41
|
+
const sanitizeErrorMessage = (value) => value
|
|
42
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
43
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]')
|
|
44
|
+
.replace(/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]+\b/g, '[redacted-key]')
|
|
45
|
+
.trim()
|
|
46
|
+
.slice(0, 500);
|
|
47
|
+
const reportCheckoutObservability = (input) => {
|
|
48
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
49
|
+
const runtimeConfig = input.analytics.runtimeConfig;
|
|
50
|
+
const funnelId = ((_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId) === null || _a === void 0 ? void 0 : _a.trim()) || FUNNEL_ID.trim();
|
|
51
|
+
const environment = (_b = input.analytics.environment) !== null && _b !== void 0 ? _b : (((_c = input.analytics.metadata) === null || _c === void 0 ? void 0 : _c.environment) === 'test' || ((_d = input.analytics.metadata) === null || _d === void 0 ? void 0 : _d.environment) === 'live'
|
|
52
|
+
? input.analytics.metadata.environment
|
|
53
|
+
: null);
|
|
54
|
+
if (!funnelId || !environment) {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
}
|
|
57
|
+
const path = '/sdk/public/payments/checkout-observability';
|
|
58
|
+
const baseUrl = (_e = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl) === null || _e === void 0 ? void 0 : _e.trim().replace(/\/+$/, '');
|
|
59
|
+
const url = baseUrl ? `${baseUrl}${path}` : buildMainApiUrl(path);
|
|
60
|
+
const publishableKey = (_f = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey) === null || _f === void 0 ? void 0 : _f.trim();
|
|
61
|
+
return fetch(url, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: publishableKey
|
|
64
|
+
? { 'Content-Type': 'application/json', 'x-sdk-publishable-key': publishableKey }
|
|
65
|
+
: buildSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
checkoutSessionId: input.checkoutSessionId || undefined,
|
|
68
|
+
environment,
|
|
69
|
+
errorCode: input.errorCode,
|
|
70
|
+
errorMessage: input.errorMessage,
|
|
71
|
+
eventId: input.eventId,
|
|
72
|
+
eventType: input.eventType,
|
|
73
|
+
failureStage: input.failureStage,
|
|
74
|
+
funnelId,
|
|
75
|
+
funnelVersionId: ((_g = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelVersionId) === null || _g === void 0 ? void 0 : _g.trim()) || FUNNEL_VERSION_ID.trim() || undefined,
|
|
76
|
+
paymentMethod: input.paymentMethod || undefined,
|
|
77
|
+
provider: input.provider,
|
|
78
|
+
providerDeclineCode: input.providerDeclineCode,
|
|
79
|
+
providerErrorCode: input.providerErrorCode,
|
|
80
|
+
providerErrorType: input.providerErrorType,
|
|
81
|
+
}),
|
|
82
|
+
keepalive: true,
|
|
83
|
+
}).then(() => undefined);
|
|
84
|
+
};
|
|
85
|
+
const resolveErrorCode = (message, requestedCode) => {
|
|
86
|
+
const normalizedCode = requestedCode === null || requestedCode === void 0 ? void 0 : requestedCode.trim().slice(0, 120);
|
|
87
|
+
if (normalizedCode) {
|
|
88
|
+
return normalizedCode;
|
|
89
|
+
}
|
|
90
|
+
if (message === 'Published Stripe checkout selection is incomplete') {
|
|
91
|
+
return 'published_checkout_selection_incomplete';
|
|
92
|
+
}
|
|
93
|
+
if (/checkout session.*no longer active/i.test(message)) {
|
|
94
|
+
return 'checkout_session_inactive';
|
|
95
|
+
}
|
|
96
|
+
return 'checkout_failure';
|
|
97
|
+
};
|
|
98
|
+
const emitCheckoutObservability = (eventType, input, failure) => {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
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 });
|
|
102
|
+
const eventId = publicAnalyticsSdk.track({
|
|
103
|
+
eventType,
|
|
104
|
+
environment: input.analytics.environment,
|
|
105
|
+
featureFlags: input.analytics.featureFlags,
|
|
106
|
+
metadata,
|
|
107
|
+
stepContractVersion: input.analytics.stepContractVersion,
|
|
108
|
+
stepId: input.analytics.stepId,
|
|
109
|
+
stepName: input.analytics.stepName,
|
|
110
|
+
stepType: input.analytics.stepType,
|
|
111
|
+
});
|
|
112
|
+
if (!eventId) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
void reportCheckoutObservability({
|
|
116
|
+
analytics: input.analytics,
|
|
117
|
+
checkoutSessionId: input.checkoutSessionId,
|
|
118
|
+
errorCode: failure === null || failure === void 0 ? void 0 : failure.errorCode,
|
|
119
|
+
errorMessage: failure === null || failure === void 0 ? void 0 : failure.errorMessage,
|
|
120
|
+
eventId,
|
|
121
|
+
eventType,
|
|
122
|
+
failureStage: input.failureStage,
|
|
123
|
+
paymentMethod,
|
|
124
|
+
provider: input.provider,
|
|
125
|
+
providerDeclineCode: failure === null || failure === void 0 ? void 0 : failure.providerDeclineCode,
|
|
126
|
+
providerErrorCode: failure === null || failure === void 0 ? void 0 : failure.providerErrorCode,
|
|
127
|
+
providerErrorType: failure === null || failure === void 0 ? void 0 : failure.providerErrorType,
|
|
128
|
+
}).catch(() => undefined);
|
|
129
|
+
void publicAnalyticsSdk.flush().catch(() => 0);
|
|
130
|
+
return eventId;
|
|
131
|
+
};
|
|
132
|
+
export const trackCheckoutFailureShown = (input) => {
|
|
133
|
+
var _a, _b, _c;
|
|
134
|
+
const errorMessage = sanitizeErrorMessage(input.errorMessage);
|
|
135
|
+
if (!errorMessage) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const providerDeclineCode = (_a = asNonEmptyString(input.providerDeclineCode)) === null || _a === void 0 ? void 0 : _a.slice(0, 120);
|
|
139
|
+
const providerErrorCode = (_b = asNonEmptyString(input.providerErrorCode)) === null || _b === void 0 ? void 0 : _b.slice(0, 120);
|
|
140
|
+
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));
|
|
144
|
+
return emitCheckoutObservability(paymentRejected ? 'checkout_payment_rejected' : 'checkout_failure_shown', input, {
|
|
145
|
+
errorCode: paymentRejected
|
|
146
|
+
? providerDeclineCode || providerErrorCode || 'payment_rejected'
|
|
147
|
+
: resolveErrorCode(errorMessage, input.errorCode),
|
|
148
|
+
errorMessage,
|
|
149
|
+
providerDeclineCode,
|
|
150
|
+
providerErrorCode,
|
|
151
|
+
providerErrorType,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
export const trackCheckoutCanceled = (input) => emitCheckoutObservability('checkout_canceled', input);
|