@funnelsgrove/payments 0.11.5 → 0.11.7
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 +29 -7
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.js +28 -6
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +60 -12
- package/dist/providers/stripe/components/StripeExpressCheckoutButton.js +46 -7
- package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.js +1 -1
- package/dist/providers/stripe/hooks/checkoutAttempt.d.ts +11 -0
- package/dist/providers/stripe/hooks/checkoutAttempt.js +10 -0
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +10 -5
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +4 -1
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +58 -4
- package/dist/providers/stripe/services/checkout/requests.d.ts +1 -0
- package/dist/providers/stripe/services/checkout/requests.js +8 -7
- package/dist/providers/stripe/services/checkout/types.d.ts +1 -0
- package/dist/services/checkoutObservability.service.d.ts +31 -0
- package/dist/services/checkoutObservability.service.js +124 -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,17 @@ 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) => {
|
|
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
|
+
});
|
|
155
|
+
}
|
|
145
156
|
setError(message);
|
|
146
157
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
147
158
|
};
|
|
@@ -227,7 +238,7 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
227
238
|
? 'is-positive'
|
|
228
239
|
: row.tone === 'negative'
|
|
229
240
|
? '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: [
|
|
241
|
+
: 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
242
|
'shared-stripe-checkout-text-input',
|
|
232
243
|
customerEmailEditable ? '' : 'is-readonly',
|
|
233
244
|
]
|
|
@@ -246,6 +257,17 @@ function SharedStripeCheckoutForm({ amountCents, checkoutAnalytics, checkoutSess
|
|
|
246
257
|
}
|
|
247
258
|
export function SharedStripeCheckoutDialog(_a) {
|
|
248
259
|
var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise"]);
|
|
260
|
+
const handleClose = useCallback(() => {
|
|
261
|
+
if (checkoutAnalytics) {
|
|
262
|
+
trackCheckoutCanceled({
|
|
263
|
+
analytics: checkoutAnalytics,
|
|
264
|
+
checkoutSessionId,
|
|
265
|
+
failureStage: 'checkout_dialog',
|
|
266
|
+
provider: 'stripe',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
onClose();
|
|
270
|
+
}, [checkoutAnalytics, checkoutSessionId, onClose]);
|
|
249
271
|
useEffect(() => {
|
|
250
272
|
if (!checkoutAnalytics) {
|
|
251
273
|
return;
|
|
@@ -255,15 +277,15 @@ export function SharedStripeCheckoutDialog(_a) {
|
|
|
255
277
|
useEffect(() => {
|
|
256
278
|
const handleKeyDown = (event) => {
|
|
257
279
|
if (event.key === 'Escape') {
|
|
258
|
-
|
|
280
|
+
handleClose();
|
|
259
281
|
}
|
|
260
282
|
};
|
|
261
283
|
window.addEventListener('keydown', handleKeyDown);
|
|
262
284
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
263
|
-
}, [
|
|
264
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-stripe-checkout-overlay', role: 'presentation', onClick:
|
|
285
|
+
}, [handleClose]);
|
|
286
|
+
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
287
|
clientSecret,
|
|
266
|
-
}, children: _jsx(SharedStripeCheckoutForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose:
|
|
288
|
+
}, children: _jsx(SharedStripeCheckoutForm, Object.assign({}, props, { checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, clientSecret: clientSecret, onClose: handleClose })) }) }) }), _jsx("style", { children: sharedStripeCheckoutDialogStyles })] }));
|
|
267
289
|
}
|
|
268
290
|
const sharedStripeCheckoutDialogStyles = `
|
|
269
291
|
.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',
|
|
@@ -151,11 +152,21 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
151
152
|
setError(null);
|
|
152
153
|
onInactiveCheckoutSession === null || onInactiveCheckoutSession === void 0 ? void 0 : onInactiveCheckoutSession();
|
|
153
154
|
};
|
|
154
|
-
const setSharedError = (message) => {
|
|
155
|
+
const setSharedError = (message, alreadyTracked = false) => {
|
|
155
156
|
if (isInactiveCheckoutSessionError(message)) {
|
|
156
157
|
handleInactiveCheckoutSession();
|
|
157
158
|
return;
|
|
158
159
|
}
|
|
160
|
+
if (message && checkoutAnalytics && !alreadyTracked) {
|
|
161
|
+
trackCheckoutFailureShown({
|
|
162
|
+
analytics: checkoutAnalytics,
|
|
163
|
+
checkoutSessionId,
|
|
164
|
+
errorMessage: message,
|
|
165
|
+
failureStage: 'card_form',
|
|
166
|
+
paymentMethod: 'card',
|
|
167
|
+
provider: 'stripe',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
159
170
|
setError(message);
|
|
160
171
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
161
172
|
};
|
|
@@ -300,7 +311,7 @@ function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, c
|
|
|
300
311
|
effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
|
|
301
312
|
]
|
|
302
313
|
.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: [
|
|
314
|
+
.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
315
|
'shared-checkout-v2-card-panel',
|
|
305
316
|
effectiveSelectedMethod === 'card' ? 'is-visible' : '',
|
|
306
317
|
]
|
|
@@ -336,6 +347,17 @@ export function SharedStripeCheckoutV2Dialog(_a) {
|
|
|
336
347
|
var { checkoutAnalytics, checkoutSessionId, clientSecret, onClose, stripePromise, supportedCountries, variant = 'standard' } = _a, props = __rest(_a, ["checkoutAnalytics", "checkoutSessionId", "clientSecret", "onClose", "stripePromise", "supportedCountries", "variant"]);
|
|
337
348
|
const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
|
|
338
349
|
const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
|
|
350
|
+
const handleClose = useCallback(() => {
|
|
351
|
+
if (checkoutAnalytics) {
|
|
352
|
+
trackCheckoutCanceled({
|
|
353
|
+
analytics: checkoutAnalytics,
|
|
354
|
+
checkoutSessionId,
|
|
355
|
+
failureStage: 'checkout_dialog',
|
|
356
|
+
provider: 'stripe',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
onClose();
|
|
360
|
+
}, [checkoutAnalytics, checkoutSessionId, onClose]);
|
|
339
361
|
useEffect(() => {
|
|
340
362
|
if (!checkoutAnalytics) {
|
|
341
363
|
return;
|
|
@@ -345,13 +367,13 @@ export function SharedStripeCheckoutV2Dialog(_a) {
|
|
|
345
367
|
useEffect(() => {
|
|
346
368
|
const handleKeyDown = (event) => {
|
|
347
369
|
if (event.key === 'Escape') {
|
|
348
|
-
|
|
370
|
+
handleClose();
|
|
349
371
|
}
|
|
350
372
|
};
|
|
351
373
|
window.addEventListener('keydown', handleKeyDown);
|
|
352
374
|
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:
|
|
375
|
+
}, [handleClose]);
|
|
376
|
+
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
377
|
}
|
|
356
378
|
function SharedCheckoutSpecialOfferGift({ discountLabel, imageAlt, imageSrc, }) {
|
|
357
379
|
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,19 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
103
120
|
checkoutStartSource,
|
|
104
121
|
paymentMethod }));
|
|
105
122
|
};
|
|
123
|
+
const reportFailure = (message, failureStage, paymentMethod) => {
|
|
124
|
+
if (checkoutAnalytics) {
|
|
125
|
+
trackCheckoutFailureShown({
|
|
126
|
+
analytics: checkoutAnalytics,
|
|
127
|
+
checkoutSessionId,
|
|
128
|
+
errorMessage: message,
|
|
129
|
+
failureStage,
|
|
130
|
+
paymentMethod,
|
|
131
|
+
provider: 'stripe',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
135
|
+
};
|
|
106
136
|
const handleConfirm = async (event) => {
|
|
107
137
|
var _a, _b, _c;
|
|
108
138
|
// Checkout Sessions omits the Express Checkout onClick callback. Confirmation
|
|
@@ -113,7 +143,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
113
143
|
const message = checkoutState.type === 'error'
|
|
114
144
|
? checkoutState.error.message
|
|
115
145
|
: 'Wallet checkout is not ready yet.';
|
|
116
|
-
|
|
146
|
+
reportFailure(message, 'wallet_checkout_not_ready', event.expressPaymentType);
|
|
117
147
|
event.paymentFailed({ reason: 'fail', message });
|
|
118
148
|
return;
|
|
119
149
|
}
|
|
@@ -124,14 +154,13 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
124
154
|
supportedCountries: normalizedSupportedCountries,
|
|
125
155
|
});
|
|
126
156
|
if (unsupportedCountryError) {
|
|
127
|
-
|
|
157
|
+
reportFailure(unsupportedCountryError, 'wallet_billing_country', event.expressPaymentType);
|
|
128
158
|
event.paymentFailed({ reason: 'invalid_billing_address', message: unsupportedCountryError });
|
|
129
159
|
return;
|
|
130
160
|
}
|
|
131
|
-
const updated = await ensureServerUpdated();
|
|
161
|
+
const updated = await ensureServerUpdated(event.expressPaymentType);
|
|
132
162
|
if (!updated) {
|
|
133
163
|
const message = 'Unable to update checkout session.';
|
|
134
|
-
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
135
164
|
event.paymentFailed({ reason: 'fail', message });
|
|
136
165
|
return;
|
|
137
166
|
}
|
|
@@ -144,14 +173,23 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
144
173
|
const message = confirmError instanceof Error && confirmError.message
|
|
145
174
|
? confirmError.message
|
|
146
175
|
: 'Unable to prepare wallet checkout.';
|
|
147
|
-
|
|
176
|
+
reportFailure(message, 'wallet_before_confirm', event.expressPaymentType);
|
|
148
177
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
149
178
|
return;
|
|
150
179
|
}
|
|
151
180
|
}
|
|
152
181
|
if (beforeConfirm && !preparedCustomerEmail) {
|
|
153
182
|
const message = 'Email is required to continue.';
|
|
154
|
-
|
|
183
|
+
if (checkoutAnalytics) {
|
|
184
|
+
trackCheckoutCanceled({
|
|
185
|
+
analytics: checkoutAnalytics,
|
|
186
|
+
checkoutSessionId,
|
|
187
|
+
failureStage: 'wallet_preconfirm',
|
|
188
|
+
paymentMethod: event.expressPaymentType,
|
|
189
|
+
provider: 'stripe',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
reportFailure(message, 'wallet_email_required', event.expressPaymentType);
|
|
155
193
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
156
194
|
return;
|
|
157
195
|
}
|
|
@@ -164,7 +202,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
164
202
|
: {})), { expressCheckoutConfirmEvent: event, redirect: 'if_required' }));
|
|
165
203
|
if (result.type === 'error') {
|
|
166
204
|
const message = result.error.message || 'Wallet checkout failed.';
|
|
167
|
-
|
|
205
|
+
reportFailure(message, 'wallet_confirmation', event.expressPaymentType);
|
|
168
206
|
event.paymentFailed({ reason: 'fail', message });
|
|
169
207
|
return;
|
|
170
208
|
}
|
|
@@ -184,7 +222,17 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
184
222
|
// Cancel has no method field; only single-method placements can identify it.
|
|
185
223
|
const enabledMethods = Object.entries((_a = resolvedOptions.paymentMethods) !== null && _a !== void 0 ? _a : {})
|
|
186
224
|
.filter(([, availability]) => availability !== 'never');
|
|
187
|
-
|
|
225
|
+
const paymentMethod = enabledMethods.length === 1 ? enabledMethods[0][0] : undefined;
|
|
226
|
+
queueCheckoutStarted(paymentMethod, 'wallet_cancel');
|
|
227
|
+
if (checkoutAnalytics) {
|
|
228
|
+
trackCheckoutCanceled({
|
|
229
|
+
analytics: checkoutAnalytics,
|
|
230
|
+
checkoutSessionId,
|
|
231
|
+
failureStage: 'wallet_sheet',
|
|
232
|
+
paymentMethod,
|
|
233
|
+
provider: 'stripe',
|
|
234
|
+
});
|
|
235
|
+
}
|
|
188
236
|
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
189
237
|
};
|
|
190
238
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
|
|
@@ -196,7 +244,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
|
|
|
196
244
|
.join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: handleCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
|
|
197
245
|
setWalletAvailable(false);
|
|
198
246
|
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
|
|
199
|
-
|
|
247
|
+
reportFailure(error.message || 'Unable to load wallet checkout.', 'wallet_load');
|
|
200
248
|
}, onReady: ({ availablePaymentMethods: availablePaymentMethodsMap }) => {
|
|
201
249
|
const available = Boolean(availabilityPaymentMethods.some((paymentMethod) => availablePaymentMethodsMap === null || availablePaymentMethodsMap === void 0 ? void 0 : availablePaymentMethodsMap[paymentMethod]));
|
|
202
250
|
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,24 @@ 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) => {
|
|
74
|
+
if (checkoutAnalytics) {
|
|
75
|
+
trackCheckoutFailureShown({
|
|
76
|
+
analytics: checkoutAnalytics,
|
|
77
|
+
checkoutSessionId,
|
|
78
|
+
errorMessage: message,
|
|
79
|
+
failureStage,
|
|
80
|
+
paymentMethod,
|
|
81
|
+
provider: 'stripe',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
85
|
+
};
|
|
72
86
|
const handleConfirm = async (event) => {
|
|
73
87
|
var _a;
|
|
74
88
|
if (!stripe || !elements) {
|
|
75
89
|
const message = 'Wallet checkout is not ready yet.';
|
|
76
|
-
|
|
90
|
+
reportFailure(message, 'wallet_checkout_not_ready', event.expressPaymentType);
|
|
77
91
|
event.paymentFailed({ reason: 'fail', message });
|
|
78
92
|
return;
|
|
79
93
|
}
|
|
@@ -87,21 +101,30 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
87
101
|
const message = confirmError instanceof Error && confirmError.message
|
|
88
102
|
? confirmError.message
|
|
89
103
|
: 'Unable to prepare wallet checkout.';
|
|
90
|
-
|
|
104
|
+
reportFailure(message, 'wallet_before_confirm', event.expressPaymentType);
|
|
91
105
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
92
106
|
return;
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
109
|
if (beforeConfirm && !preparedCustomerEmail) {
|
|
96
110
|
const message = 'Email is required to continue.';
|
|
97
|
-
|
|
111
|
+
if (checkoutAnalytics) {
|
|
112
|
+
trackCheckoutCanceled({
|
|
113
|
+
analytics: checkoutAnalytics,
|
|
114
|
+
checkoutSessionId,
|
|
115
|
+
failureStage: 'wallet_preconfirm',
|
|
116
|
+
paymentMethod: event.expressPaymentType,
|
|
117
|
+
provider: 'stripe',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
reportFailure(message, 'wallet_email_required', event.expressPaymentType);
|
|
98
121
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
99
122
|
return;
|
|
100
123
|
}
|
|
101
124
|
const submitResult = await elements.submit();
|
|
102
125
|
if (submitResult.error) {
|
|
103
126
|
const message = submitResult.error.message || 'Unable to start wallet checkout.';
|
|
104
|
-
|
|
127
|
+
reportFailure(message, 'wallet_payment_details', event.expressPaymentType);
|
|
105
128
|
event.paymentFailed({ reason: 'invalid_payment_data', message });
|
|
106
129
|
return;
|
|
107
130
|
}
|
|
@@ -127,7 +150,7 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
127
150
|
});
|
|
128
151
|
if (result.error) {
|
|
129
152
|
const message = result.error.message || 'Wallet checkout failed.';
|
|
130
|
-
|
|
153
|
+
reportFailure(message, 'wallet_confirmation', event.expressPaymentType);
|
|
131
154
|
event.paymentFailed({ reason: 'fail', message });
|
|
132
155
|
return;
|
|
133
156
|
}
|
|
@@ -144,16 +167,32 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, checko
|
|
|
144
167
|
}
|
|
145
168
|
event.resolve();
|
|
146
169
|
};
|
|
170
|
+
const handleCancel = () => {
|
|
171
|
+
var _a;
|
|
172
|
+
const enabledMethods = Object.entries((_a = resolvedOptions.paymentMethods) !== null && _a !== void 0 ? _a : {})
|
|
173
|
+
.filter(([, availability]) => availability !== 'never');
|
|
174
|
+
const paymentMethod = enabledMethods.length === 1 ? enabledMethods[0][0] : undefined;
|
|
175
|
+
if (checkoutAnalytics) {
|
|
176
|
+
trackCheckoutCanceled({
|
|
177
|
+
analytics: checkoutAnalytics,
|
|
178
|
+
checkoutSessionId,
|
|
179
|
+
failureStage: 'wallet_sheet',
|
|
180
|
+
paymentMethod,
|
|
181
|
+
provider: 'stripe',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
185
|
+
};
|
|
147
186
|
return (_jsxs(_Fragment, { children: [_jsx("div", { className: [
|
|
148
187
|
'stripe-express-checkout-button',
|
|
149
188
|
className !== null && className !== void 0 ? className : '',
|
|
150
189
|
effectiveWalletAvailable === false ? 'is-hidden' : '',
|
|
151
190
|
]
|
|
152
191
|
.filter(Boolean)
|
|
153
|
-
.join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel:
|
|
192
|
+
.join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel: handleCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
|
|
154
193
|
setWalletAvailable(false);
|
|
155
194
|
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
|
|
156
|
-
|
|
195
|
+
reportFailure(error.message || 'Unable to load wallet checkout.', 'wallet_load');
|
|
157
196
|
}, onReady: ({ availablePaymentMethods: availablePaymentMethodsMap }) => {
|
|
158
197
|
const available = Boolean(availabilityPaymentMethods.some((paymentMethod) => availablePaymentMethodsMap === null || availablePaymentMethodsMap === void 0 ? void 0 : availablePaymentMethodsMap[paymentMethod]));
|
|
159
198
|
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
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type StripeCheckoutAttempt = {
|
|
2
|
+
key: string;
|
|
3
|
+
id: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const resolveStripeCheckoutAttempt: (input: {
|
|
6
|
+
current: StripeCheckoutAttempt | null;
|
|
7
|
+
forceNew: boolean;
|
|
8
|
+
intentKey: string;
|
|
9
|
+
paymentsApiVersion?: "v1" | "v2" | null;
|
|
10
|
+
createId: () => string;
|
|
11
|
+
}) => StripeCheckoutAttempt | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const resolveStripeCheckoutAttempt = (input) => {
|
|
2
|
+
var _a;
|
|
3
|
+
if (input.paymentsApiVersion !== 'v2') {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
if (!input.forceNew && ((_a = input.current) === null || _a === void 0 ? void 0 : _a.key) === input.intentKey) {
|
|
7
|
+
return input.current;
|
|
8
|
+
}
|
|
9
|
+
return { key: input.intentKey, id: input.createId() };
|
|
10
|
+
};
|
|
@@ -15,6 +15,7 @@ import { createEmptyCheckoutPreparationLoading, } from '../../paymentProvider.ty
|
|
|
15
15
|
import { createStripeOneTimeCheckout, getStripePromise, isStripeConfigured, } from '../services/stripe.service.js';
|
|
16
16
|
import { isInactiveCheckoutSessionError } from './useStripeSubscriptionCheckoutSession.js';
|
|
17
17
|
import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalytics.service.js';
|
|
18
|
+
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
18
19
|
export function resolveStripeOneTimeCheckoutPreparationOptions(source, options = {}) {
|
|
19
20
|
var _a, _b;
|
|
20
21
|
const checkoutStartSource = (_a = options.checkoutStartSource) !== null && _a !== void 0 ? _a : (source === 'wallet' ? 'wallet_render' : 'card_click');
|
|
@@ -154,7 +155,7 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, payme
|
|
|
154
155
|
: Object.assign(Object.assign({}, current), { [source]: value }));
|
|
155
156
|
}, []);
|
|
156
157
|
const createCheckoutSession = useCallback(async (options = {}) => {
|
|
157
|
-
var _a, _b
|
|
158
|
+
var _a, _b;
|
|
158
159
|
const forceNew = options.forceNew === true;
|
|
159
160
|
if (!configured || !plan) {
|
|
160
161
|
return null;
|
|
@@ -169,10 +170,14 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, payme
|
|
|
169
170
|
clearActiveCheckoutSession();
|
|
170
171
|
attemptRef.current = null;
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
-
attemptRef.current
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
attemptRef.current = resolveStripeCheckoutAttempt({
|
|
174
|
+
current: attemptRef.current,
|
|
175
|
+
forceNew,
|
|
176
|
+
intentKey,
|
|
177
|
+
paymentsApiVersion: runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.paymentsApiVersion,
|
|
178
|
+
createId: () => crypto.randomUUID(),
|
|
179
|
+
});
|
|
180
|
+
const idempotencyKey = (_b = attemptRef.current) === null || _b === void 0 ? void 0 : _b.id;
|
|
176
181
|
if (idempotencyKey && !userId && !anonymousUserIdRef.current) {
|
|
177
182
|
anonymousUserIdRef.current = `anonymous_${crypto.randomUUID()}`;
|
|
178
183
|
}
|
|
@@ -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,8 @@ 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';
|
|
18
|
+
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
17
19
|
export function resolveStripeSubscriptionCheckoutPreparationOptions(source, options = {}) {
|
|
18
20
|
var _a, _b;
|
|
19
21
|
const checkoutStartSource = (_a = options.checkoutStartSource) !== null && _a !== void 0 ? _a : (source === 'wallet' ? 'wallet_render' : 'card_click');
|
|
@@ -67,7 +69,8 @@ export function isInactiveCheckoutSessionError(message) {
|
|
|
67
69
|
var _a;
|
|
68
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 : '');
|
|
69
71
|
}
|
|
70
|
-
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, }) {
|
|
71
74
|
var _a, _b, _c, _d, _e;
|
|
72
75
|
const [clientSecret, setClientSecret] = useState(null);
|
|
73
76
|
const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
|
|
@@ -78,6 +81,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
78
81
|
const [runtimeStripePublishableKey, setRuntimeStripePublishableKey] = useState(null);
|
|
79
82
|
const creatingIntentRef = useRef(null);
|
|
80
83
|
const requestIdRef = useRef(0);
|
|
84
|
+
const attemptRef = useRef(null);
|
|
81
85
|
const anonymousUserIdRef = useRef(null);
|
|
82
86
|
const configured = enabled &&
|
|
83
87
|
(isStripeConfigured(checkoutMode) ||
|
|
@@ -130,6 +134,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
130
134
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
131
135
|
}, [clearActiveCheckoutSession, onError]);
|
|
132
136
|
const reset = useCallback(() => {
|
|
137
|
+
attemptRef.current = null;
|
|
133
138
|
clearActiveCheckoutSession();
|
|
134
139
|
setLoading(createEmptyCheckoutPreparationLoading());
|
|
135
140
|
setRuntimeStripePublishableKey(null);
|
|
@@ -148,7 +153,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
148
153
|
: Object.assign(Object.assign({}, current), { [source]: value }));
|
|
149
154
|
}, []);
|
|
150
155
|
const createCheckoutSubscription = useCallback(async (options = {}) => {
|
|
151
|
-
var _a;
|
|
156
|
+
var _a, _b;
|
|
152
157
|
const forceNew = options.forceNew === true;
|
|
153
158
|
if (!configured || !plan || !displayPlan) {
|
|
154
159
|
return null;
|
|
@@ -161,6 +166,18 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
161
166
|
}
|
|
162
167
|
if (forceNew) {
|
|
163
168
|
clearActiveCheckoutSession();
|
|
169
|
+
attemptRef.current = null;
|
|
170
|
+
}
|
|
171
|
+
attemptRef.current = resolveStripeCheckoutAttempt({
|
|
172
|
+
current: attemptRef.current,
|
|
173
|
+
forceNew,
|
|
174
|
+
intentKey,
|
|
175
|
+
paymentsApiVersion: runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.paymentsApiVersion,
|
|
176
|
+
createId: () => crypto.randomUUID(),
|
|
177
|
+
});
|
|
178
|
+
const idempotencyKey = (_b = attemptRef.current) === null || _b === void 0 ? void 0 : _b.id;
|
|
179
|
+
if (idempotencyKey && !userId && !anonymousUserIdRef.current) {
|
|
180
|
+
anonymousUserIdRef.current = `anonymous_${crypto.randomUUID()}`;
|
|
164
181
|
}
|
|
165
182
|
const requestKey = intentKey;
|
|
166
183
|
const requestPlanKey = planKey;
|
|
@@ -182,6 +199,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
182
199
|
paymentProfileId,
|
|
183
200
|
provider,
|
|
184
201
|
anonymousUserId: anonymousUserIdRef.current,
|
|
202
|
+
idempotencyKey,
|
|
185
203
|
});
|
|
186
204
|
if (((_a = creatingIntentRef.current) === null || _a === void 0 ? void 0 : _a.key) !== requestKey ||
|
|
187
205
|
((_b = creatingIntentRef.current) === null || _b === void 0 ? void 0 : _b.id) !== requestId) {
|
|
@@ -202,7 +220,24 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
202
220
|
catch (subscriptionError) {
|
|
203
221
|
if (((_f = creatingIntentRef.current) === null || _f === void 0 ? void 0 : _f.key) === requestKey &&
|
|
204
222
|
((_g = creatingIntentRef.current) === null || _g === void 0 ? void 0 : _g.id) === requestId) {
|
|
205
|
-
|
|
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);
|
|
206
241
|
}
|
|
207
242
|
return null;
|
|
208
243
|
}
|
|
@@ -222,6 +257,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
222
257
|
}, [
|
|
223
258
|
activeClientSecret,
|
|
224
259
|
checkoutMode,
|
|
260
|
+
checkoutAnalytics,
|
|
225
261
|
clearActiveCheckoutSession,
|
|
226
262
|
configured,
|
|
227
263
|
couponId,
|
|
@@ -292,13 +328,31 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
|
|
|
292
328
|
return true;
|
|
293
329
|
}
|
|
294
330
|
catch (subscriptionError) {
|
|
295
|
-
|
|
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);
|
|
296
349
|
return false;
|
|
297
350
|
}
|
|
298
351
|
}, [
|
|
299
352
|
activeCheckoutSessionId,
|
|
300
353
|
activePlanKey,
|
|
301
354
|
checkoutMode,
|
|
355
|
+
checkoutAnalytics,
|
|
302
356
|
configured,
|
|
303
357
|
couponId,
|
|
304
358
|
customerEmail,
|
|
@@ -31,6 +31,7 @@ export declare const toSubscriptionCheckoutRequest: (input: CreateSubscriptionCh
|
|
|
31
31
|
paymentProfileId: string | undefined;
|
|
32
32
|
runtimeConfigRevisionId: string | undefined;
|
|
33
33
|
anonymousUserId: string | undefined;
|
|
34
|
+
idempotencyKey: string | undefined;
|
|
34
35
|
title: string;
|
|
35
36
|
description: string | undefined;
|
|
36
37
|
amountCents: number;
|
|
@@ -27,7 +27,7 @@ export const toOneTimeCheckoutRequest = (input) => {
|
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
29
|
export const toSubscriptionCheckoutRequest = (input) => {
|
|
30
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
30
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
31
31
|
const recurring = requireCheckoutRecurringConfig(input.plan);
|
|
32
32
|
return {
|
|
33
33
|
offerSetId: ((_a = input.plan.offerSetId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
|
|
@@ -39,21 +39,22 @@ export const toSubscriptionCheckoutRequest = (input) => {
|
|
|
39
39
|
paymentProfileId: ((_e = input.paymentProfileId) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
|
|
40
40
|
runtimeConfigRevisionId: ((_f = input.runtimeConfigRevisionId) === null || _f === void 0 ? void 0 : _f.trim()) || undefined,
|
|
41
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,
|
|
42
43
|
title: input.plan.title,
|
|
43
|
-
description: ((
|
|
44
|
+
description: ((_j = input.plan.description) === null || _j === void 0 ? void 0 : _j.trim()) || undefined,
|
|
44
45
|
amountCents: input.plan.amountCents,
|
|
45
46
|
billingInterval: recurring.interval,
|
|
46
47
|
billingIntervalCount: recurring.intervalCount,
|
|
47
|
-
followUpProviderPlanId: ((
|
|
48
|
-
followUpPlanTitle: ((
|
|
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,
|
|
49
50
|
followUpBillingInterval: input.plan.followUpBillingInterval,
|
|
50
51
|
followUpBillingIntervalCount: input.plan.followUpBillingIntervalCount,
|
|
51
52
|
introIterations: input.plan.introIterations,
|
|
52
|
-
couponId: ((
|
|
53
|
+
couponId: ((_m = input.couponId) === null || _m === void 0 ? void 0 : _m.trim()) || undefined,
|
|
53
54
|
analyticsMetadata: normalizeCheckoutAnalyticsMetadata(input.analyticsMetadata, input.plan),
|
|
54
|
-
customerEmail: ((
|
|
55
|
+
customerEmail: ((_o = input.customerEmail) === null || _o === void 0 ? void 0 : _o.trim()) || undefined,
|
|
55
56
|
returnUrl: input.returnUrl,
|
|
56
|
-
userId: ((
|
|
57
|
+
userId: ((_p = input.user_id) === null || _p === void 0 ? void 0 : _p.trim()) || undefined,
|
|
57
58
|
environment: input.environment,
|
|
58
59
|
runtimeConfig: input.runtimeConfig,
|
|
59
60
|
};
|
|
@@ -32,6 +32,7 @@ export type CreateSubscriptionCheckoutInput = {
|
|
|
32
32
|
paymentProfileId?: string | null;
|
|
33
33
|
provider?: 'stripe' | null;
|
|
34
34
|
anonymousUserId?: string | null;
|
|
35
|
+
idempotencyKey?: string | null;
|
|
35
36
|
couponId?: string | null;
|
|
36
37
|
analyticsMetadata?: Record<string, unknown> | null;
|
|
37
38
|
customerEmail?: string | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
};
|
|
29
|
+
export declare const trackCheckoutFailureShown: (input: CheckoutFailureShownInput) => string | null;
|
|
30
|
+
export declare const trackCheckoutCanceled: (input: CheckoutObservabilityInput) => string | null;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
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 sanitizeErrorMessage = (value) => value
|
|
30
|
+
.replace(/[\r\n\t]+/g, ' ')
|
|
31
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]')
|
|
32
|
+
.replace(/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]+\b/g, '[redacted-key]')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 500);
|
|
35
|
+
const reportCheckoutObservability = (input) => {
|
|
36
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
37
|
+
const runtimeConfig = input.analytics.runtimeConfig;
|
|
38
|
+
const funnelId = ((_a = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelId) === null || _a === void 0 ? void 0 : _a.trim()) || FUNNEL_ID.trim();
|
|
39
|
+
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'
|
|
40
|
+
? input.analytics.metadata.environment
|
|
41
|
+
: null);
|
|
42
|
+
if (!funnelId || !environment) {
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
const path = '/sdk/public/payments/checkout-observability';
|
|
46
|
+
const baseUrl = (_e = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.apiBaseUrl) === null || _e === void 0 ? void 0 : _e.trim().replace(/\/+$/, '');
|
|
47
|
+
const url = baseUrl ? `${baseUrl}${path}` : buildMainApiUrl(path);
|
|
48
|
+
const publishableKey = (_f = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey) === null || _f === void 0 ? void 0 : _f.trim();
|
|
49
|
+
return fetch(url, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: publishableKey
|
|
52
|
+
? { 'Content-Type': 'application/json', 'x-sdk-publishable-key': publishableKey }
|
|
53
|
+
: buildSdkHeaders({ 'Content-Type': 'application/json' }),
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
checkoutSessionId: input.checkoutSessionId || undefined,
|
|
56
|
+
environment,
|
|
57
|
+
errorCode: input.errorCode,
|
|
58
|
+
errorMessage: input.errorMessage,
|
|
59
|
+
eventId: input.eventId,
|
|
60
|
+
eventType: input.eventType,
|
|
61
|
+
failureStage: input.failureStage,
|
|
62
|
+
funnelId,
|
|
63
|
+
funnelVersionId: ((_g = runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelVersionId) === null || _g === void 0 ? void 0 : _g.trim()) || FUNNEL_VERSION_ID.trim() || undefined,
|
|
64
|
+
paymentMethod: input.paymentMethod || undefined,
|
|
65
|
+
provider: input.provider,
|
|
66
|
+
}),
|
|
67
|
+
keepalive: true,
|
|
68
|
+
}).then(() => undefined);
|
|
69
|
+
};
|
|
70
|
+
const resolveErrorCode = (message, requestedCode) => {
|
|
71
|
+
const normalizedCode = requestedCode === null || requestedCode === void 0 ? void 0 : requestedCode.trim().slice(0, 120);
|
|
72
|
+
if (normalizedCode) {
|
|
73
|
+
return normalizedCode;
|
|
74
|
+
}
|
|
75
|
+
if (message === 'Published Stripe checkout selection is incomplete') {
|
|
76
|
+
return 'published_checkout_selection_incomplete';
|
|
77
|
+
}
|
|
78
|
+
if (/checkout session.*no longer active/i.test(message)) {
|
|
79
|
+
return 'checkout_session_inactive';
|
|
80
|
+
}
|
|
81
|
+
return 'checkout_failure';
|
|
82
|
+
};
|
|
83
|
+
const emitCheckoutObservability = (eventType, input, failure) => {
|
|
84
|
+
var _a, _b;
|
|
85
|
+
const paymentMethod = toAnalyticsPaymentMethod(input.paymentMethod);
|
|
86
|
+
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 });
|
|
87
|
+
const eventId = publicAnalyticsSdk.track({
|
|
88
|
+
eventType,
|
|
89
|
+
environment: input.analytics.environment,
|
|
90
|
+
featureFlags: input.analytics.featureFlags,
|
|
91
|
+
metadata,
|
|
92
|
+
stepContractVersion: input.analytics.stepContractVersion,
|
|
93
|
+
stepId: input.analytics.stepId,
|
|
94
|
+
stepName: input.analytics.stepName,
|
|
95
|
+
stepType: input.analytics.stepType,
|
|
96
|
+
});
|
|
97
|
+
if (!eventId) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
void reportCheckoutObservability({
|
|
101
|
+
analytics: input.analytics,
|
|
102
|
+
checkoutSessionId: input.checkoutSessionId,
|
|
103
|
+
errorCode: failure === null || failure === void 0 ? void 0 : failure.errorCode,
|
|
104
|
+
errorMessage: failure === null || failure === void 0 ? void 0 : failure.errorMessage,
|
|
105
|
+
eventId,
|
|
106
|
+
eventType,
|
|
107
|
+
failureStage: input.failureStage,
|
|
108
|
+
paymentMethod,
|
|
109
|
+
provider: input.provider,
|
|
110
|
+
}).catch(() => undefined);
|
|
111
|
+
void publicAnalyticsSdk.flush().catch(() => 0);
|
|
112
|
+
return eventId;
|
|
113
|
+
};
|
|
114
|
+
export const trackCheckoutFailureShown = (input) => {
|
|
115
|
+
const errorMessage = sanitizeErrorMessage(input.errorMessage);
|
|
116
|
+
if (!errorMessage) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return emitCheckoutObservability('checkout_failure_shown', input, {
|
|
120
|
+
errorCode: resolveErrorCode(errorMessage, input.errorCode),
|
|
121
|
+
errorMessage,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
export const trackCheckoutCanceled = (input) => emitCheckoutObservability('checkout_canceled', input);
|