@funnelfox/billing 0.9.3-feat-defer-session-on-interaction.34 → 0.10.0

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/README.md CHANGED
@@ -421,6 +421,8 @@ try {
421
421
  console.log('Error Code:', error.errorCode); // e.g., 'double_purchase'
422
422
  console.log('Error Type:', error.errorType); // e.g., 'api_exception'
423
423
  console.log('Request ID:', error.requestId); // For support
424
+ console.log('Decline Code:', error.declineCode); // e.g., 'INSUFFICIENT_FUNDS', set on failed payments
425
+ console.log('Decline Source:', error.declineCodeSource); // 'primer' | 'stripe' | 'adyen', when the provider reports one
424
426
  console.log('Message:', error.message);
425
427
  } else if (error instanceof PrimerError) {
426
428
  // Primer SDK error
@@ -634,6 +636,8 @@ const paymentMethod = await Billing.initMethod(
634
636
  },
635
637
  onPaymentFail: error => {
636
638
  console.error('Payment failed:', error.message);
639
+ console.log('Decline code:', error.declineCode); // e.g., 'INSUFFICIENT_FUNDS', when the provider reports one
640
+ console.log('Decline source:', error.declineCodeSource); // 'primer' | 'stripe' | 'adyen'
637
641
  },
638
642
  onPaymentCancel: () => {
639
643
  console.log('Payment was cancelled');
@@ -119,6 +119,8 @@ class APIError extends FunnefoxSDKError {
119
119
  this.errorType = options.errorType || null;
120
120
  this.requestId = options.requestId || null;
121
121
  this.response = options.response || null;
122
+ this.declineCode = options.declineCode || null;
123
+ this.declineCodeSource = options.declineCodeSource || null;
122
124
  }
123
125
  }
124
126
  class PrimerError extends FunnefoxSDKError {
@@ -323,6 +325,48 @@ async function loadPrimerSDK(version) {
323
325
  /**
324
326
  * @fileoverview Helper utilities for Funnefox SDK
325
327
  */
328
+ // Mirrors Stripe Elements: a card number that can never become valid is reported while the buyer
329
+ // types, an incomplete value only after they leave the field, and an empty one only after a submit
330
+ // attempt. Primer reports every error on every keystroke, even for fields the buyer has not reached
331
+ // yet, and its expiry codes cannot tell a typo from a half-typed date ("1" is already
332
+ // expiryYearInvalid, "01/2" is cardExpired), so expiry errors wait for the buyer to leave the field.
333
+ const HOSTED_INPUT_ERRORS_SHOWN_WHILE_TYPING = new Set(['cardInvalid']);
334
+ const HOSTED_INPUT_ERRORS_FOR_EMPTY_FIELD = new Set([
335
+ 'cardRequired',
336
+ 'expiryRequired',
337
+ 'cvvRequired',
338
+ ]);
339
+ function getVisibleHostedInputError(input) {
340
+ if (!input.error) {
341
+ return null;
342
+ }
343
+ if (input.errorCode &&
344
+ HOSTED_INPUT_ERRORS_SHOWN_WHILE_TYPING.has(input.errorCode)) {
345
+ return input.error;
346
+ }
347
+ if (input.active) {
348
+ return null;
349
+ }
350
+ if (input.submitted) {
351
+ return input.error;
352
+ }
353
+ if (input.errorCode &&
354
+ HOSTED_INPUT_ERRORS_FOR_EMPTY_FIELD.has(input.errorCode)) {
355
+ return null;
356
+ }
357
+ return input.touched ? input.error : null;
358
+ }
359
+ function resolveTaxDisplay(source) {
360
+ const taxBehavior = source.tax_behavior ??
361
+ ((source.tax_amount ?? 0) > 0 ? 'exclusive' : undefined);
362
+ if (taxBehavior === 'inclusive') {
363
+ return { taxBehavior, taxAmount: source.tax_inclusive_amount ?? 0 };
364
+ }
365
+ if (taxBehavior === 'exclusive') {
366
+ return { taxBehavior, taxAmount: source.tax_amount ?? 0 };
367
+ }
368
+ return { taxAmount: 0 };
369
+ }
326
370
  function merge(...objects) {
327
371
  const result = {};
328
372
  for (const obj of objects) {
@@ -491,7 +535,7 @@ exports.PaymentMethod = void 0;
491
535
  /**
492
536
  * @fileoverview Constants for Funnefox SDK
493
537
  */
494
- const SDK_VERSION = '0.9.3-feat-defer-session-on-interaction.34';
538
+ const SDK_VERSION = '0.10.0';
495
539
  const DEFAULTS = {
496
540
  BASE_URL: 'https://billing.funnelfox.com',
497
541
  REGION: 'default',
@@ -628,141 +672,6 @@ function requireString(value, fieldName) {
628
672
  return true;
629
673
  }
630
674
 
631
- /**
632
- * @fileoverview Support for Apple's official <apple-pay-button> custom element.
633
- *
634
- * Primer's checkout-web v2 renders the Apple Pay button through the CSS
635
- * approach (-webkit-appearance: -apple-pay-button), and WebKit only accepts
636
- * the 7 legacy values for -apple-pay-button-type. Newer types such as
637
- * 'continue' are silently dropped and fall back to 'plain' (verified in
638
- * Safari 26 on macOS and iOS — see PRD-1426).
639
- *
640
- * Apple's official custom element supports the full set of button types, so
641
- * for "native-only" types we overlay it on top of the (visually hidden)
642
- * Primer button and forward clicks, keeping Primer's payment flow untouched.
643
- */
644
- const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
645
- /** How long to wait for the <apple-pay-button> element definition before
646
- * giving up and falling back to Primer's own button. */
647
- const ELEMENT_DEFINITION_TIMEOUT_MS = 4000;
648
- /**
649
- * Button types WebKit can render through Primer's CSS approach.
650
- * Everything outside this list requires the native <apple-pay-button> element.
651
- */
652
- const CSS_RENDERABLE_APPLE_PAY_TYPES = [
653
- 'plain',
654
- 'buy',
655
- 'set-up',
656
- 'donate',
657
- 'check-out',
658
- 'book',
659
- 'subscribe',
660
- ];
661
- /**
662
- * Returns true when the requested Apple Pay buttonType can only be rendered
663
- * with Apple's native <apple-pay-button> element.
664
- */
665
- function isNativeOnlyApplePayButtonType(type) {
666
- return !!type && !CSS_RENDERABLE_APPLE_PAY_TYPES.includes(type);
667
- }
668
- /**
669
- * Loads Apple's apple-pay-sdk.js (defines the <apple-pay-button> custom
670
- * element). Resolves to true when the element is available.
671
- */
672
- async function loadApplePayButtonElement() {
673
- if (typeof window === 'undefined' || !window.customElements) {
674
- return false;
675
- }
676
- if (window.customElements.get('apple-pay-button')) {
677
- return true;
678
- }
679
- try {
680
- await loadScript$1({
681
- src: APPLE_PAY_SDK_URL,
682
- crossOrigin: 'anonymous',
683
- appendTo: 'head',
684
- });
685
- // Never hang on a script that loaded but did not define the element —
686
- // fall back to Primer's own button instead.
687
- const defined = window.customElements
688
- .whenDefined('apple-pay-button')
689
- .then(() => true);
690
- const timeout = new Promise(resolve => {
691
- setTimeout(() => resolve(false), ELEMENT_DEFINITION_TIMEOUT_MS);
692
- });
693
- return await Promise.race([defined, timeout]);
694
- }
695
- catch {
696
- return false;
697
- }
698
- }
699
- function createNativeApplePayElement(options) {
700
- const native = document.createElement('apple-pay-button');
701
- native.setAttribute('type', options.buttonType);
702
- native.setAttribute('buttonstyle', options.buttonStyle || 'black');
703
- native.setAttribute('locale', (typeof navigator !== 'undefined' && navigator.language) || 'en-US');
704
- native.style.cssText = [
705
- 'position:absolute',
706
- 'inset:0',
707
- 'width:100%',
708
- 'height:100%',
709
- '--apple-pay-button-width:100%',
710
- '--apple-pay-button-height:100%',
711
- '--apple-pay-button-border-radius:28px',
712
- 'display:block',
713
- 'cursor:pointer',
714
- 'z-index:1',
715
- ].join(';');
716
- return native;
717
- }
718
- /**
719
- * Renders Apple's native <apple-pay-button> stretched over `wrapper` and
720
- * forwards clicks to the Primer-rendered <button> inside it. The synchronous
721
- * forwarded click preserves the user activation required by ApplePaySession.
722
- *
723
- * Returns the overlay element, or null when the Apple Pay SDK could not be
724
- * loaded (callers should keep Primer's own button visible as a fallback).
725
- */
726
- async function overlayNativeApplePayButton(wrapper, options) {
727
- const available = await loadApplePayButtonElement();
728
- if (!available) {
729
- return null;
730
- }
731
- // The overlay is only safe when Primer's own clickable button is present —
732
- // otherwise there is nothing to forward the click to.
733
- if (!wrapper.querySelector('button')) {
734
- return null;
735
- }
736
- const native = createNativeApplePayElement(options);
737
- wrapper.style.position = 'relative';
738
- native.addEventListener('click', () => {
739
- const primerButton = wrapper.querySelector('button');
740
- primerButton?.click();
741
- });
742
- wrapper.appendChild(native);
743
- return native;
744
- }
745
- /**
746
- * Renders the native button with no Primer button behind it, for the deferred
747
- * flow: the visitor sees the real Apple Pay button immediately while the
748
- * client session is still un-created. `onActivate` fires on tap so the caller
749
- * can start (or await) initialization.
750
- *
751
- * Returns null when Apple's SDK is unavailable — callers must fall back to
752
- * creating the session eagerly so a button of some kind is always rendered.
753
- */
754
- async function renderPlaceholderApplePayButton(wrapper, options) {
755
- const available = await loadApplePayButtonElement();
756
- if (!available) {
757
- return null;
758
- }
759
- const native = createNativeApplePayElement(options);
760
- wrapper.style.position = 'relative';
761
- native.addEventListener('click', options.onActivate);
762
- wrapper.appendChild(native);
763
- return native;
764
- }
765
-
766
675
  /**
767
676
  * @fileoverview Primer SDK integration wrapper
768
677
  */
@@ -801,7 +710,6 @@ class PrimerWrapper {
801
710
  }
802
711
  async createHeadlessCheckout(clientToken, options, method) {
803
712
  await this.ensurePrimerLoaded();
804
- this.applePayOptions = options.applePay;
805
713
  this.currentHeadless = PrimerWrapper.headlessManager.getOrCreate(clientToken, options, method);
806
714
  return this.currentHeadless;
807
715
  }
@@ -848,53 +756,16 @@ class PrimerWrapper {
848
756
  }
849
757
  htmlNode.appendChild(wrapper);
850
758
  /* end hack */
851
- /* PRD-1426: WebKit cannot render newer Apple Pay button types (e.g.
852
- * 'continue') through the CSS approach Primer v2 uses. For those types
853
- * we overlay Apple's official <apple-pay-button> element on top of the
854
- * visually hidden Primer button and forward clicks, keeping Primer's
855
- * payment flow untouched. The Primer button is hidden BEFORE it renders
856
- * to avoid a flash of the plain button; if the Apple SDK fails to load,
857
- * the rule is removed and Primer's own button stays visible (graceful
858
- * fallback to 'plain'). */
859
- const applePayConfig = this.applePayOptions;
860
- const nativeButtonType = allowedPaymentMethod === exports.PaymentMethod.APPLE_PAY &&
861
- applePayConfig?.buttonType &&
862
- isNativeOnlyApplePayButtonType(applePayConfig.buttonType)
863
- ? applePayConfig.buttonType
864
- : null;
865
- let hideRuleIndex = null;
866
- if (nativeButtonType && sheet) {
867
- hideRuleIndex = sheet.insertRule(`
868
- .${wrapper.className} > button {
869
- opacity: 0 !important;
870
- }
871
- `);
872
- }
873
759
  button = pmManager.createButton();
874
760
  await button.render(wrapper, {});
875
- let nativeOverlay = null;
876
- if (nativeButtonType) {
877
- nativeOverlay = await overlayNativeApplePayButton(wrapper, {
878
- buttonType: nativeButtonType,
879
- buttonStyle: applePayConfig?.buttonStyle,
880
- });
881
- if (!nativeOverlay && sheet && hideRuleIndex !== null) {
882
- sheet.deleteRule(hideRuleIndex);
883
- }
884
- }
885
761
  this.destroyCallbacks.push(() => button.clean());
886
762
  onMethodRender(allowedPaymentMethod);
887
763
  return {
888
764
  setDisabled: (disabled) => {
889
765
  button.setDisabled(disabled);
890
- if (nativeOverlay) {
891
- nativeOverlay.style.pointerEvents = disabled ? 'none' : '';
892
- nativeOverlay.style.opacity = disabled ? '0.5' : '';
893
- }
894
766
  },
895
767
  destroy: () => {
896
768
  styleEl.remove();
897
- nativeOverlay?.remove();
898
769
  button.clean();
899
770
  },
900
771
  };
@@ -979,11 +850,22 @@ class PrimerWrapper {
979
850
  ? 'Please enter a postal code'
980
851
  : null;
981
852
  };
853
+ const focusedHostedInputs = new Set();
982
854
  const onHostedInputChange = (name) => (event) => {
983
855
  const input = event;
984
- if (input.submitted) {
985
- dispatchError(name, input.error);
856
+ const justFocused = input.active && !focusedHostedInputs.has(name);
857
+ if (input.active) {
858
+ focusedHostedInputs.add(name);
859
+ }
860
+ else {
861
+ focusedHostedInputs.delete(name);
986
862
  }
863
+ // Primer sends a change on focus before any keystroke; like Stripe, the error the buyer
864
+ // came back to fix stays visible until they start typing.
865
+ if (justFocused) {
866
+ return;
867
+ }
868
+ dispatchError(name, getVisibleHostedInputError(input));
987
869
  };
988
870
  const cardHolderOnChange = async (e) => {
989
871
  pmManager.setCardholderName(e.target.value);
@@ -1242,29 +1124,34 @@ class PrimerWrapper {
1242
1124
  PrimerWrapper.headlessManager = new HeadlessManager();
1243
1125
 
1244
1126
  /**
1245
- * @fileoverview Tags the deferred session request so the warm-up trigger can
1246
- * be measured without adding a single extra network call.
1247
- *
1248
- * The backend already logs every request header, so `X-FF-Warmup` lands in
1249
- * Elasticsearch for free. That gives us, per session actually created:
1250
- * - which signal started it (dwell vs interaction vs a cold tap)
1251
- * - how long after mount it fired
1252
- *
1253
- * Which is what tells us whether the dwell can be lengthened (more savings)
1254
- * or must stay short (cold-tap risk), instead of guessing at an interaction
1255
- * rate nobody has measured.
1127
+ * @fileoverview Normalization of payment failure errors
1256
1128
  */
1257
- const WARMUP_HEADER = 'X-FF-Warmup';
1258
- let pendingTag = null;
1259
- /** Records the trigger for the session request that is about to be sent. */
1260
- function setWarmupTag(trigger, msSinceMount) {
1261
- pendingTag = `${trigger};${Math.max(0, Math.round(msSinceMount))}`;
1129
+ const STRIPE_CARD_ERROR = 'card_error';
1130
+ function declineCodeFromStripeError(error) {
1131
+ const { type, code } = error;
1132
+ if (type !== STRIPE_CARD_ERROR || typeof code !== 'string' || !code) {
1133
+ return null;
1134
+ }
1135
+ return code.toUpperCase();
1262
1136
  }
1263
- /** Consumes the tag — it describes one request only. */
1264
- function takeWarmupTag() {
1265
- const tag = pendingTag;
1266
- pendingTag = null;
1267
- return tag;
1137
+ function toPaymentFailure(error) {
1138
+ const failure = new Error(error?.message || 'Payment processing failed');
1139
+ if (error instanceof APIError) {
1140
+ if (error.declineCode) {
1141
+ failure.declineCode = error.declineCode;
1142
+ }
1143
+ if (error.declineCodeSource) {
1144
+ failure.declineCodeSource = error.declineCodeSource;
1145
+ }
1146
+ }
1147
+ else if (error instanceof Error) {
1148
+ const declineCode = declineCodeFromStripeError(error);
1149
+ if (declineCode) {
1150
+ failure.declineCode = declineCode;
1151
+ failure.declineCodeSource = 'stripe';
1152
+ }
1153
+ }
1154
+ return failure;
1268
1155
  }
1269
1156
 
1270
1157
  /**
@@ -1284,7 +1171,6 @@ class APIClient {
1284
1171
  headers: {
1285
1172
  'Content-Type': 'application/json',
1286
1173
  'X-SDK-Version': SDK_VERSION,
1287
- ...warmupHeader(),
1288
1174
  ...(options.headers || {}),
1289
1175
  },
1290
1176
  ...options,
@@ -1465,7 +1351,11 @@ class APIClient {
1465
1351
  status: 'succeeded',
1466
1352
  };
1467
1353
  case 'failed':
1468
- throw new APIError(data.failed_message_for_user || 'Payment failed', null, { response });
1354
+ throw new APIError(data.failed_message_for_user || 'Payment failed', null, {
1355
+ response,
1356
+ declineCode: data.decline_code,
1357
+ declineCodeSource: data.decline_code_source,
1358
+ });
1469
1359
  case 'cancelled':
1470
1360
  throw new APIError('Payment was cancelled by user', null, {
1471
1361
  response,
@@ -1499,10 +1389,6 @@ class APIClient {
1499
1389
  }));
1500
1390
  }
1501
1391
  }
1502
- function warmupHeader() {
1503
- const tag = takeWarmupTag();
1504
- return tag ? { [WARMUP_HEADER]: tag } : {};
1505
- }
1506
1392
 
1507
1393
  class SessionService {
1508
1394
  constructor() {
@@ -1585,96 +1471,6 @@ const hideLoader = () => {
1585
1471
  }
1586
1472
  };
1587
1473
 
1588
- /**
1589
- * @fileoverview Decides when a deferred client session should be created.
1590
- *
1591
- * Fires on whichever comes first: the visitor's first interaction, or a short
1592
- * dwell once the page is actually visible. The dwell is what makes the
1593
- * deferral safe — ApplePaySession must be constructed inside the tap gesture,
1594
- * so the session has to exist *before* the first tap. A human cannot read a
1595
- * price and tap in under ~800ms, whereas prerenders, background tabs, crawlers
1596
- * and instant bounces never reach the dwell at all and so never cost a
1597
- * session (nor the order row behind it).
1598
- */
1599
- const INTERACTION_EVENTS = [
1600
- 'pointerdown',
1601
- 'touchstart',
1602
- 'keydown',
1603
- 'wheel',
1604
- 'scroll',
1605
- ];
1606
- /** Long enough to filter prerenders and instant bounces, far shorter than
1607
- * any realistic time-to-tap on a paywall. */
1608
- const DEFAULT_WARMUP_DWELL_MS = 800;
1609
- /** True while the page is prerendering or not visible to the user. */
1610
- function isPageHidden() {
1611
- if (typeof document === 'undefined') {
1612
- return true;
1613
- }
1614
- if (document.prerendering) {
1615
- return true;
1616
- }
1617
- return document.visibilityState === 'hidden';
1618
- }
1619
- /**
1620
- * Invokes `handler` once — on first interaction, or after `dwellMs` of visible
1621
- * time, whichever happens first. Returns a cleanup for the un-fired case.
1622
- */
1623
- function onSessionWarmup(handler, dwellMs = DEFAULT_WARMUP_DWELL_MS) {
1624
- if (typeof document === 'undefined') {
1625
- return () => { };
1626
- }
1627
- let fired = false;
1628
- let timer;
1629
- const cleanup = () => {
1630
- if (timer !== undefined) {
1631
- clearTimeout(timer);
1632
- timer = undefined;
1633
- }
1634
- for (const event of INTERACTION_EVENTS) {
1635
- document.removeEventListener(event, onInteraction, true);
1636
- }
1637
- document.removeEventListener('visibilitychange', onVisibilityChange);
1638
- document.removeEventListener('prerenderingchange', onVisibilityChange);
1639
- };
1640
- const fire = (source) => {
1641
- if (fired) {
1642
- return;
1643
- }
1644
- fired = true;
1645
- cleanup();
1646
- handler(source);
1647
- };
1648
- function onInteraction() {
1649
- if (!isPageHidden()) {
1650
- fire('interaction');
1651
- }
1652
- }
1653
- function startDwell() {
1654
- if (fired || timer !== undefined) {
1655
- return;
1656
- }
1657
- timer = setTimeout(() => fire('dwell'), dwellMs);
1658
- }
1659
- function onVisibilityChange() {
1660
- if (!isPageHidden()) {
1661
- startDwell();
1662
- }
1663
- }
1664
- for (const event of INTERACTION_EVENTS) {
1665
- document.addEventListener(event, onInteraction, {
1666
- capture: true,
1667
- passive: true,
1668
- });
1669
- }
1670
- document.addEventListener('visibilitychange', onVisibilityChange);
1671
- document.addEventListener('prerenderingchange', onVisibilityChange);
1672
- if (!isPageHidden()) {
1673
- startDwell();
1674
- }
1675
- return cleanup;
1676
- }
1677
-
1678
1474
  var V3_URL = 'https://js.stripe.com/v3';
1679
1475
  var V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/;
1680
1476
  var EXISTING_SCRIPT_MESSAGE = 'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used';
@@ -2160,8 +1956,8 @@ class CheckoutInstance extends EventEmitter {
2160
1956
  }
2161
1957
  return {
2162
1958
  amountTotal: data.amount_total,
2163
- taxAmount: data.tax_amount ?? 0,
2164
1959
  currency: data.currency ?? '',
1960
+ ...resolveTaxDisplay(data),
2165
1961
  };
2166
1962
  };
2167
1963
  this.emitTaxInfo = (info) => {
@@ -2194,8 +1990,8 @@ class CheckoutInstance extends EventEmitter {
2194
1990
  }
2195
1991
  this.emitTaxInfo({
2196
1992
  amountTotal: tax.amount_total,
2197
- taxAmount: tax.tax_amount,
2198
1993
  currency: tax.currency,
1994
+ ...resolveTaxDisplay(tax),
2199
1995
  });
2200
1996
  }
2201
1997
  catch (err) {
@@ -2214,6 +2010,7 @@ class CheckoutInstance extends EventEmitter {
2214
2010
  if (this.taxRecalcDebounce) {
2215
2011
  clearTimeout(this.taxRecalcDebounce);
2216
2012
  }
2013
+ this.checkoutConfig.onTaxPending?.();
2217
2014
  this.emit(EVENTS.TAX_PENDING);
2218
2015
  this.taxRecalcDebounce = setTimeout(this.runTaxRecalc, TAX_RECALC_DEBOUNCE_MS);
2219
2016
  };
@@ -2271,8 +2068,8 @@ class CheckoutInstance extends EventEmitter {
2271
2068
  }
2272
2069
  catch (error) {
2273
2070
  this._setState('error');
2274
- this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
2275
- primerHandler.handleFailure(error.message || 'Payment processing failed');
2071
+ this.emit(EVENTS.PURCHASE_FAILURE, toPaymentFailure(error));
2072
+ primerHandler.handleFailure(error?.message || 'Payment processing failed');
2276
2073
  }
2277
2074
  finally {
2278
2075
  this.onLoaderChangeWithRace(false);
@@ -2296,8 +2093,8 @@ class CheckoutInstance extends EventEmitter {
2296
2093
  }
2297
2094
  catch (error) {
2298
2095
  this._setState('error');
2299
- this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
2300
- primerHandler.handleFailure(error.message || 'Payment processing failed');
2096
+ this.emit(EVENTS.PURCHASE_FAILURE, toPaymentFailure(error));
2097
+ primerHandler.handleFailure(error?.message || 'Payment processing failed');
2301
2098
  }
2302
2099
  finally {
2303
2100
  this.emit(EVENTS.PURCHASE_COMPLETED);
@@ -2832,98 +2629,6 @@ class CheckoutInstance extends EventEmitter {
2832
2629
  }
2833
2630
  async initMethod(method, element, callbacks) {
2834
2631
  this._ensureNotDestroyed();
2835
- const deferred = await this.initMethodDeferred(method, element, callbacks);
2836
- if (deferred) {
2837
- return deferred;
2838
- }
2839
- return await this.initMethodNow(method, element, callbacks);
2840
- }
2841
- /**
2842
- * Renders Apple's native button immediately and postpones session creation
2843
- * until the visitor interacts. Returns null when deferral does not apply, so
2844
- * the caller falls back to creating the session on mount.
2845
- */
2846
- async initMethodDeferred(method, element, callbacks) {
2847
- const applePay = this.checkoutConfig.applePay;
2848
- const buttonType = applePay?.buttonType;
2849
- if (this.checkoutConfig.sessionCreation !== 'onInteraction' ||
2850
- method !== exports.PaymentMethod.APPLE_PAY ||
2851
- this.isReady() ||
2852
- !buttonType ||
2853
- !isNativeOnlyApplePayButtonType(buttonType)) {
2854
- return null;
2855
- }
2856
- let started = null;
2857
- let real = null;
2858
- let disabled = false;
2859
- let stopWaiting = () => { };
2860
- let placeholder = null;
2861
- const mountedAt = Date.now();
2862
- const start = (trigger) => {
2863
- const fromTap = trigger === 'tap';
2864
- if (!started) {
2865
- setWarmupTag(trigger, Date.now() - mountedAt);
2866
- started = (async () => {
2867
- stopWaiting();
2868
- if (fromTap) {
2869
- callbacks.onLoaderChange?.(true);
2870
- }
2871
- try {
2872
- placeholder?.remove();
2873
- placeholder = null;
2874
- const methodInterface = await this.initMethodNow(method, element, callbacks);
2875
- real = methodInterface;
2876
- if (disabled) {
2877
- methodInterface.setDisabled(true);
2878
- }
2879
- return methodInterface;
2880
- }
2881
- finally {
2882
- if (fromTap) {
2883
- callbacks.onLoaderChange?.(false);
2884
- }
2885
- }
2886
- })();
2887
- }
2888
- return started;
2889
- };
2890
- placeholder = await renderPlaceholderApplePayButton(element, {
2891
- buttonType,
2892
- buttonStyle: applePay?.buttonStyle,
2893
- // Cold tap: the visitor's first interaction is the button itself, so the
2894
- // session cannot exist yet and ApplePaySession would lose the gesture.
2895
- // Start now and let the loader cover the (rare) second tap.
2896
- onActivate: () => void start('tap'),
2897
- });
2898
- if (!placeholder) {
2899
- return null;
2900
- }
2901
- // Free head start: Primer's SDK is a static asset on their CDN, so
2902
- // fetching it now costs no backend call and leaves only the session
2903
- // request to complete before the first tap.
2904
- void loadPrimerSDK().catch(() => { });
2905
- stopWaiting = onSessionWarmup(trigger => void start(trigger));
2906
- return {
2907
- setDisabled: (value) => {
2908
- disabled = value;
2909
- real?.setDisabled(value);
2910
- if (placeholder) {
2911
- placeholder.style.pointerEvents = value ? 'none' : '';
2912
- placeholder.style.opacity = value ? '0.5' : '';
2913
- }
2914
- },
2915
- destroy: async () => {
2916
- stopWaiting();
2917
- placeholder?.remove();
2918
- placeholder = null;
2919
- if (started) {
2920
- await (await started).destroy?.();
2921
- }
2922
- await this.destroy();
2923
- },
2924
- };
2925
- }
2926
- async initMethodNow(method, element, callbacks) {
2927
2632
  if (!this.isReady()) {
2928
2633
  await this.createSession();
2929
2634
  }
@@ -2978,6 +2683,9 @@ class CheckoutInstance extends EventEmitter {
2978
2683
  await this.primerWrapper.initializeHeadlessCheckout(this.clientToken, checkoutOptions, method);
2979
2684
  const methodInterface = await this.primerWrapper.initMethod(method, element, methodOptions);
2980
2685
  this.startUnhandledTelemetry(method);
2686
+ if (this.isTaxEnabled() && !this.lastTaxInfo) {
2687
+ this.emitSessionTaxEstimate();
2688
+ }
2981
2689
  return {
2982
2690
  ...methodInterface,
2983
2691
  destroy: async () => {
@@ -3141,7 +2849,9 @@ async function initMethod(method, element, options) {
3141
2849
  ...DEFAULT_BUTTONS_OPTIONS[exports.PaymentMethod.GOOGLE_PAY],
3142
2850
  ...(options.googlePay || {}),
3143
2851
  },
3144
- sessionCreation: options.sessionCreation,
2852
+ onTaxChange: options.onTaxChange,
2853
+ onTaxError: options.onTaxError,
2854
+ onTaxPending: options.onTaxPending,
3145
2855
  },
3146
2856
  });
3147
2857
  return await checkoutInstance.initMethod(method, element, {
@@ -3337,3 +3047,5 @@ exports.createCheckout = createCheckout;
3337
3047
  exports.createClientSession = createClientSession;
3338
3048
  exports.getAvailablePaymentMethods = getAvailablePaymentMethods;
3339
3049
  exports.loadStripe = loadStripe;
3050
+ exports.resolveTaxDisplay = resolveTaxDisplay;
3051
+ exports.toPaymentFailure = toPaymentFailure;