@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 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 (_a) {
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: onClose, "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) => {
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 = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, copyRef.current.error);
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
- onClose();
283
+ handleClose();
259
284
  }
260
285
  };
261
286
  window.addEventListener('keydown', handleKeyDown);
262
287
  return () => window.removeEventListener('keydown', handleKeyDown);
263
- }, [onClose]);
264
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: 'shared-stripe-checkout-overlay', role: 'presentation', onClick: onClose, 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: {
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: onClose })) }) }) }), _jsx("style", { children: sharedStripeCheckoutDialogStyles })] }));
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
- onClose();
378
+ handleClose();
349
379
  }
350
380
  };
351
381
  window.addEventListener('keydown', handleKeyDown);
352
382
  return () => window.removeEventListener('keydown', handleKeyDown);
353
- }, [onClose]);
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: onClose, supportedCountries: normalizedSupportedCountries, variant: variant })) }) }) }), _jsx("style", { children: sharedStripeCheckoutV2Styles })] }));
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
- onError === null || onError === void 0 ? void 0 : onError(updateResult.error.message || 'Unable to update checkout session.');
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(unsupportedCountryError);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- queueCheckoutStarted(enabledMethods.length === 1 ? enabledMethods[0][0] : undefined, 'wallet_cancel');
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
- onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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
- onError === null || onError === void 0 ? void 0 : onError(message);
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: onCancel, onClick: handleClick, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
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
- onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
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 function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
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 function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }) {
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
- setError(getFriendlyStripeCheckoutError(subscriptionError));
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
- setError(getFriendlyStripeCheckoutError(subscriptionError));
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.11.6",
3
+ "version": "0.11.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",