@funnelfox/billing 0.10.0 → 0.11.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.
@@ -357,6 +357,32 @@ function getVisibleHostedInputError(input) {
357
357
  return input.touched ? input.error : null;
358
358
  }
359
359
  function resolveTaxDisplay(source) {
360
+ return {
361
+ ...resolveTaxAmount(source),
362
+ ...resolveTaxBreakdown(source.tax_breakdown),
363
+ };
364
+ }
365
+ function resolveTaxBreakdown(breakdown) {
366
+ if (!Array.isArray(breakdown)) {
367
+ return {};
368
+ }
369
+ const taxBreakdown = breakdown.map(entry => ({
370
+ amount: entry.amount,
371
+ percentage: entry.percentage,
372
+ inclusive: entry.inclusive,
373
+ taxType: entry.tax_type,
374
+ country: entry.country,
375
+ subdivision: entry.subdivision,
376
+ taxabilityReason: entry.taxability_reason,
377
+ }));
378
+ const percentages = taxBreakdown.map(entry => Number(entry.percentage));
379
+ if (!percentages.length || !percentages.every(Number.isFinite)) {
380
+ return { taxBreakdown };
381
+ }
382
+ const taxRate = Number(percentages.reduce((sum, percentage) => sum + percentage, 0).toFixed(6));
383
+ return { taxBreakdown, taxRate };
384
+ }
385
+ function resolveTaxAmount(source) {
360
386
  const taxBehavior = source.tax_behavior ??
361
387
  ((source.tax_amount ?? 0) > 0 ? 'exclusive' : undefined);
362
388
  if (taxBehavior === 'inclusive') {
@@ -535,7 +561,7 @@ exports.PaymentMethod = void 0;
535
561
  /**
536
562
  * @fileoverview Constants for Funnefox SDK
537
563
  */
538
- const SDK_VERSION = '0.10.0';
564
+ const SDK_VERSION = '0.11.0';
539
565
  const DEFAULTS = {
540
566
  BASE_URL: 'https://billing.funnelfox.com',
541
567
  REGION: 'default',
@@ -672,6 +698,117 @@ function requireString(value, fieldName) {
672
698
  return true;
673
699
  }
674
700
 
701
+ /**
702
+ * @fileoverview Support for Apple's official <apple-pay-button> custom element.
703
+ *
704
+ * Primer's checkout-web v2 renders the Apple Pay button through the CSS
705
+ * approach (-webkit-appearance: -apple-pay-button), and WebKit only accepts
706
+ * the 7 legacy values for -apple-pay-button-type. Newer types such as
707
+ * 'continue' are silently dropped and fall back to 'plain' (verified in
708
+ * Safari 26 on macOS and iOS, see PRD-1426).
709
+ *
710
+ * Apple's official custom element supports the full set of button types, so
711
+ * for "native-only" types we overlay it on top of the (visually hidden)
712
+ * Primer button and forward clicks, keeping Primer's payment flow untouched.
713
+ */
714
+ const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
715
+ /** How long to wait for the <apple-pay-button> element definition before
716
+ * giving up and falling back to Primer's own button. */
717
+ const ELEMENT_DEFINITION_TIMEOUT_MS = 4000;
718
+ /**
719
+ * Button types WebKit can render through Primer's CSS approach.
720
+ * Everything outside this list requires the native <apple-pay-button> element.
721
+ */
722
+ const CSS_RENDERABLE_APPLE_PAY_TYPES = [
723
+ 'plain',
724
+ 'buy',
725
+ 'set-up',
726
+ 'donate',
727
+ 'check-out',
728
+ 'book',
729
+ 'subscribe',
730
+ ];
731
+ /**
732
+ * Returns true when the requested Apple Pay buttonType can only be rendered
733
+ * with Apple's native <apple-pay-button> element.
734
+ */
735
+ function isNativeOnlyApplePayButtonType(type) {
736
+ return !!type && !CSS_RENDERABLE_APPLE_PAY_TYPES.includes(type);
737
+ }
738
+ /**
739
+ * Loads Apple's apple-pay-sdk.js (defines the <apple-pay-button> custom
740
+ * element). Resolves to true when the element is available.
741
+ */
742
+ async function loadApplePayButtonElement() {
743
+ if (typeof window === 'undefined' || !window.customElements) {
744
+ return false;
745
+ }
746
+ if (window.customElements.get('apple-pay-button')) {
747
+ return true;
748
+ }
749
+ try {
750
+ await loadScript$1({
751
+ src: APPLE_PAY_SDK_URL,
752
+ crossOrigin: 'anonymous',
753
+ appendTo: 'head',
754
+ });
755
+ // Never hang on a script that loaded but did not define the element;
756
+ // fall back to Primer's own button instead.
757
+ const defined = window.customElements
758
+ .whenDefined('apple-pay-button')
759
+ .then(() => true);
760
+ const timeout = new Promise(resolve => {
761
+ setTimeout(() => resolve(false), ELEMENT_DEFINITION_TIMEOUT_MS);
762
+ });
763
+ return await Promise.race([defined, timeout]);
764
+ }
765
+ catch {
766
+ return false;
767
+ }
768
+ }
769
+ /**
770
+ * Renders Apple's native <apple-pay-button> stretched over `wrapper` and
771
+ * forwards clicks to the Primer-rendered <button> inside it. The synchronous
772
+ * forwarded click preserves the user activation required by ApplePaySession.
773
+ *
774
+ * Returns the overlay element, or null when the Apple Pay SDK could not be
775
+ * loaded (callers should keep Primer's own button visible as a fallback).
776
+ */
777
+ async function overlayNativeApplePayButton(wrapper, options) {
778
+ const available = await loadApplePayButtonElement();
779
+ if (!available) {
780
+ return null;
781
+ }
782
+ // The overlay is only safe when Primer's own clickable button is present;
783
+ // otherwise there is nothing to forward the click to.
784
+ if (!wrapper.querySelector('button')) {
785
+ return null;
786
+ }
787
+ const native = document.createElement('apple-pay-button');
788
+ native.setAttribute('type', options.buttonType);
789
+ native.setAttribute('buttonstyle', options.buttonStyle || 'black');
790
+ native.setAttribute('locale', (typeof navigator !== 'undefined' && navigator.language) || 'en-US');
791
+ native.style.cssText = [
792
+ 'position:absolute',
793
+ 'inset:0',
794
+ 'width:100%',
795
+ 'height:100%',
796
+ '--apple-pay-button-width:100%',
797
+ '--apple-pay-button-height:100%',
798
+ '--apple-pay-button-border-radius:28px',
799
+ 'display:block',
800
+ 'cursor:pointer',
801
+ 'z-index:1',
802
+ ].join(';');
803
+ wrapper.style.position = 'relative';
804
+ native.addEventListener('click', () => {
805
+ const primerButton = wrapper.querySelector('button');
806
+ primerButton?.click();
807
+ });
808
+ wrapper.appendChild(native);
809
+ return native;
810
+ }
811
+
675
812
  /**
676
813
  * @fileoverview Primer SDK integration wrapper
677
814
  */
@@ -710,6 +847,7 @@ class PrimerWrapper {
710
847
  }
711
848
  async createHeadlessCheckout(clientToken, options, method) {
712
849
  await this.ensurePrimerLoaded();
850
+ this.applePayOptions = options.applePay;
713
851
  this.currentHeadless = PrimerWrapper.headlessManager.getOrCreate(clientToken, options, method);
714
852
  return this.currentHeadless;
715
853
  }
@@ -756,16 +894,56 @@ class PrimerWrapper {
756
894
  }
757
895
  htmlNode.appendChild(wrapper);
758
896
  /* end hack */
897
+ /* PRD-1426: WebKit cannot render newer Apple Pay button types (e.g.
898
+ * 'continue') through the CSS approach Primer v2 uses. For those types
899
+ * we overlay Apple's official <apple-pay-button> element on top of the
900
+ * visually hidden Primer button and forward clicks, keeping Primer's
901
+ * payment flow untouched. The Primer button is hidden BEFORE it renders
902
+ * to avoid a flash of the plain button; if the Apple SDK fails to load,
903
+ * the rule is removed and Primer's own button stays visible (graceful
904
+ * fallback to 'plain'). */
905
+ const applePayConfig = this.applePayOptions;
906
+ const nativeButtonType = allowedPaymentMethod === exports.PaymentMethod.APPLE_PAY &&
907
+ applePayConfig?.buttonType &&
908
+ isNativeOnlyApplePayButtonType(applePayConfig.buttonType)
909
+ ? applePayConfig.buttonType
910
+ : null;
911
+ let hideRuleIndex = null;
912
+ if (nativeButtonType && sheet) {
913
+ hideRuleIndex = sheet.insertRule(`
914
+ .${wrapper.className} > button {
915
+ opacity: 0 !important;
916
+ }
917
+ `);
918
+ }
759
919
  button = pmManager.createButton();
760
920
  await button.render(wrapper, {});
761
- this.destroyCallbacks.push(() => button.clean());
921
+ let nativeOverlay = null;
922
+ if (nativeButtonType) {
923
+ nativeOverlay = await overlayNativeApplePayButton(wrapper, {
924
+ buttonType: nativeButtonType,
925
+ buttonStyle: applePayConfig?.buttonStyle,
926
+ });
927
+ if (!nativeOverlay && sheet && hideRuleIndex !== null) {
928
+ sheet.deleteRule(hideRuleIndex);
929
+ }
930
+ }
931
+ this.destroyCallbacks.push(() => {
932
+ nativeOverlay?.remove();
933
+ button.clean();
934
+ });
762
935
  onMethodRender(allowedPaymentMethod);
763
936
  return {
764
937
  setDisabled: (disabled) => {
765
938
  button.setDisabled(disabled);
939
+ if (nativeOverlay) {
940
+ nativeOverlay.style.pointerEvents = disabled ? 'none' : '';
941
+ nativeOverlay.style.opacity = disabled ? '0.5' : '';
942
+ }
766
943
  },
767
944
  destroy: () => {
768
945
  styleEl.remove();
946
+ nativeOverlay?.remove();
769
947
  button.clean();
770
948
  },
771
949
  };
@@ -355,6 +355,32 @@ function getVisibleHostedInputError(input) {
355
355
  return input.touched ? input.error : null;
356
356
  }
357
357
  function resolveTaxDisplay(source) {
358
+ return {
359
+ ...resolveTaxAmount(source),
360
+ ...resolveTaxBreakdown(source.tax_breakdown),
361
+ };
362
+ }
363
+ function resolveTaxBreakdown(breakdown) {
364
+ if (!Array.isArray(breakdown)) {
365
+ return {};
366
+ }
367
+ const taxBreakdown = breakdown.map(entry => ({
368
+ amount: entry.amount,
369
+ percentage: entry.percentage,
370
+ inclusive: entry.inclusive,
371
+ taxType: entry.tax_type,
372
+ country: entry.country,
373
+ subdivision: entry.subdivision,
374
+ taxabilityReason: entry.taxability_reason,
375
+ }));
376
+ const percentages = taxBreakdown.map(entry => Number(entry.percentage));
377
+ if (!percentages.length || !percentages.every(Number.isFinite)) {
378
+ return { taxBreakdown };
379
+ }
380
+ const taxRate = Number(percentages.reduce((sum, percentage) => sum + percentage, 0).toFixed(6));
381
+ return { taxBreakdown, taxRate };
382
+ }
383
+ function resolveTaxAmount(source) {
358
384
  const taxBehavior = source.tax_behavior ??
359
385
  ((source.tax_amount ?? 0) > 0 ? 'exclusive' : undefined);
360
386
  if (taxBehavior === 'inclusive') {
@@ -533,7 +559,7 @@ var PaymentMethod;
533
559
  /**
534
560
  * @fileoverview Constants for Funnefox SDK
535
561
  */
536
- const SDK_VERSION = '0.10.0';
562
+ const SDK_VERSION = '0.11.0';
537
563
  const DEFAULTS = {
538
564
  BASE_URL: 'https://billing.funnelfox.com',
539
565
  REGION: 'default',
@@ -670,6 +696,117 @@ function requireString(value, fieldName) {
670
696
  return true;
671
697
  }
672
698
 
699
+ /**
700
+ * @fileoverview Support for Apple's official <apple-pay-button> custom element.
701
+ *
702
+ * Primer's checkout-web v2 renders the Apple Pay button through the CSS
703
+ * approach (-webkit-appearance: -apple-pay-button), and WebKit only accepts
704
+ * the 7 legacy values for -apple-pay-button-type. Newer types such as
705
+ * 'continue' are silently dropped and fall back to 'plain' (verified in
706
+ * Safari 26 on macOS and iOS, see PRD-1426).
707
+ *
708
+ * Apple's official custom element supports the full set of button types, so
709
+ * for "native-only" types we overlay it on top of the (visually hidden)
710
+ * Primer button and forward clicks, keeping Primer's payment flow untouched.
711
+ */
712
+ const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
713
+ /** How long to wait for the <apple-pay-button> element definition before
714
+ * giving up and falling back to Primer's own button. */
715
+ const ELEMENT_DEFINITION_TIMEOUT_MS = 4000;
716
+ /**
717
+ * Button types WebKit can render through Primer's CSS approach.
718
+ * Everything outside this list requires the native <apple-pay-button> element.
719
+ */
720
+ const CSS_RENDERABLE_APPLE_PAY_TYPES = [
721
+ 'plain',
722
+ 'buy',
723
+ 'set-up',
724
+ 'donate',
725
+ 'check-out',
726
+ 'book',
727
+ 'subscribe',
728
+ ];
729
+ /**
730
+ * Returns true when the requested Apple Pay buttonType can only be rendered
731
+ * with Apple's native <apple-pay-button> element.
732
+ */
733
+ function isNativeOnlyApplePayButtonType(type) {
734
+ return !!type && !CSS_RENDERABLE_APPLE_PAY_TYPES.includes(type);
735
+ }
736
+ /**
737
+ * Loads Apple's apple-pay-sdk.js (defines the <apple-pay-button> custom
738
+ * element). Resolves to true when the element is available.
739
+ */
740
+ async function loadApplePayButtonElement() {
741
+ if (typeof window === 'undefined' || !window.customElements) {
742
+ return false;
743
+ }
744
+ if (window.customElements.get('apple-pay-button')) {
745
+ return true;
746
+ }
747
+ try {
748
+ await loadScript$1({
749
+ src: APPLE_PAY_SDK_URL,
750
+ crossOrigin: 'anonymous',
751
+ appendTo: 'head',
752
+ });
753
+ // Never hang on a script that loaded but did not define the element;
754
+ // fall back to Primer's own button instead.
755
+ const defined = window.customElements
756
+ .whenDefined('apple-pay-button')
757
+ .then(() => true);
758
+ const timeout = new Promise(resolve => {
759
+ setTimeout(() => resolve(false), ELEMENT_DEFINITION_TIMEOUT_MS);
760
+ });
761
+ return await Promise.race([defined, timeout]);
762
+ }
763
+ catch {
764
+ return false;
765
+ }
766
+ }
767
+ /**
768
+ * Renders Apple's native <apple-pay-button> stretched over `wrapper` and
769
+ * forwards clicks to the Primer-rendered <button> inside it. The synchronous
770
+ * forwarded click preserves the user activation required by ApplePaySession.
771
+ *
772
+ * Returns the overlay element, or null when the Apple Pay SDK could not be
773
+ * loaded (callers should keep Primer's own button visible as a fallback).
774
+ */
775
+ async function overlayNativeApplePayButton(wrapper, options) {
776
+ const available = await loadApplePayButtonElement();
777
+ if (!available) {
778
+ return null;
779
+ }
780
+ // The overlay is only safe when Primer's own clickable button is present;
781
+ // otherwise there is nothing to forward the click to.
782
+ if (!wrapper.querySelector('button')) {
783
+ return null;
784
+ }
785
+ const native = document.createElement('apple-pay-button');
786
+ native.setAttribute('type', options.buttonType);
787
+ native.setAttribute('buttonstyle', options.buttonStyle || 'black');
788
+ native.setAttribute('locale', (typeof navigator !== 'undefined' && navigator.language) || 'en-US');
789
+ native.style.cssText = [
790
+ 'position:absolute',
791
+ 'inset:0',
792
+ 'width:100%',
793
+ 'height:100%',
794
+ '--apple-pay-button-width:100%',
795
+ '--apple-pay-button-height:100%',
796
+ '--apple-pay-button-border-radius:28px',
797
+ 'display:block',
798
+ 'cursor:pointer',
799
+ 'z-index:1',
800
+ ].join(';');
801
+ wrapper.style.position = 'relative';
802
+ native.addEventListener('click', () => {
803
+ const primerButton = wrapper.querySelector('button');
804
+ primerButton?.click();
805
+ });
806
+ wrapper.appendChild(native);
807
+ return native;
808
+ }
809
+
673
810
  /**
674
811
  * @fileoverview Primer SDK integration wrapper
675
812
  */
@@ -708,6 +845,7 @@ class PrimerWrapper {
708
845
  }
709
846
  async createHeadlessCheckout(clientToken, options, method) {
710
847
  await this.ensurePrimerLoaded();
848
+ this.applePayOptions = options.applePay;
711
849
  this.currentHeadless = PrimerWrapper.headlessManager.getOrCreate(clientToken, options, method);
712
850
  return this.currentHeadless;
713
851
  }
@@ -754,16 +892,56 @@ class PrimerWrapper {
754
892
  }
755
893
  htmlNode.appendChild(wrapper);
756
894
  /* end hack */
895
+ /* PRD-1426: WebKit cannot render newer Apple Pay button types (e.g.
896
+ * 'continue') through the CSS approach Primer v2 uses. For those types
897
+ * we overlay Apple's official <apple-pay-button> element on top of the
898
+ * visually hidden Primer button and forward clicks, keeping Primer's
899
+ * payment flow untouched. The Primer button is hidden BEFORE it renders
900
+ * to avoid a flash of the plain button; if the Apple SDK fails to load,
901
+ * the rule is removed and Primer's own button stays visible (graceful
902
+ * fallback to 'plain'). */
903
+ const applePayConfig = this.applePayOptions;
904
+ const nativeButtonType = allowedPaymentMethod === PaymentMethod.APPLE_PAY &&
905
+ applePayConfig?.buttonType &&
906
+ isNativeOnlyApplePayButtonType(applePayConfig.buttonType)
907
+ ? applePayConfig.buttonType
908
+ : null;
909
+ let hideRuleIndex = null;
910
+ if (nativeButtonType && sheet) {
911
+ hideRuleIndex = sheet.insertRule(`
912
+ .${wrapper.className} > button {
913
+ opacity: 0 !important;
914
+ }
915
+ `);
916
+ }
757
917
  button = pmManager.createButton();
758
918
  await button.render(wrapper, {});
759
- this.destroyCallbacks.push(() => button.clean());
919
+ let nativeOverlay = null;
920
+ if (nativeButtonType) {
921
+ nativeOverlay = await overlayNativeApplePayButton(wrapper, {
922
+ buttonType: nativeButtonType,
923
+ buttonStyle: applePayConfig?.buttonStyle,
924
+ });
925
+ if (!nativeOverlay && sheet && hideRuleIndex !== null) {
926
+ sheet.deleteRule(hideRuleIndex);
927
+ }
928
+ }
929
+ this.destroyCallbacks.push(() => {
930
+ nativeOverlay?.remove();
931
+ button.clean();
932
+ });
760
933
  onMethodRender(allowedPaymentMethod);
761
934
  return {
762
935
  setDisabled: (disabled) => {
763
936
  button.setDisabled(disabled);
937
+ if (nativeOverlay) {
938
+ nativeOverlay.style.pointerEvents = disabled ? 'none' : '';
939
+ nativeOverlay.style.opacity = disabled ? '0.5' : '';
940
+ }
764
941
  },
765
942
  destroy: () => {
766
943
  styleEl.remove();
944
+ nativeOverlay?.remove();
767
945
  button.clean();
768
946
  },
769
947
  };
@@ -361,6 +361,32 @@
361
361
  return input.touched ? input.error : null;
362
362
  }
363
363
  function resolveTaxDisplay(source) {
364
+ return {
365
+ ...resolveTaxAmount(source),
366
+ ...resolveTaxBreakdown(source.tax_breakdown),
367
+ };
368
+ }
369
+ function resolveTaxBreakdown(breakdown) {
370
+ if (!Array.isArray(breakdown)) {
371
+ return {};
372
+ }
373
+ const taxBreakdown = breakdown.map(entry => ({
374
+ amount: entry.amount,
375
+ percentage: entry.percentage,
376
+ inclusive: entry.inclusive,
377
+ taxType: entry.tax_type,
378
+ country: entry.country,
379
+ subdivision: entry.subdivision,
380
+ taxabilityReason: entry.taxability_reason,
381
+ }));
382
+ const percentages = taxBreakdown.map(entry => Number(entry.percentage));
383
+ if (!percentages.length || !percentages.every(Number.isFinite)) {
384
+ return { taxBreakdown };
385
+ }
386
+ const taxRate = Number(percentages.reduce((sum, percentage) => sum + percentage, 0).toFixed(6));
387
+ return { taxBreakdown, taxRate };
388
+ }
389
+ function resolveTaxAmount(source) {
364
390
  const taxBehavior = source.tax_behavior ??
365
391
  ((source.tax_amount ?? 0) > 0 ? 'exclusive' : undefined);
366
392
  if (taxBehavior === 'inclusive') {
@@ -539,7 +565,7 @@
539
565
  /**
540
566
  * @fileoverview Constants for Funnefox SDK
541
567
  */
542
- const SDK_VERSION = '0.10.0';
568
+ const SDK_VERSION = '0.11.0';
543
569
  const DEFAULTS = {
544
570
  BASE_URL: 'https://billing.funnelfox.com',
545
571
  REGION: 'default',
@@ -676,6 +702,117 @@
676
702
  return true;
677
703
  }
678
704
 
705
+ /**
706
+ * @fileoverview Support for Apple's official <apple-pay-button> custom element.
707
+ *
708
+ * Primer's checkout-web v2 renders the Apple Pay button through the CSS
709
+ * approach (-webkit-appearance: -apple-pay-button), and WebKit only accepts
710
+ * the 7 legacy values for -apple-pay-button-type. Newer types such as
711
+ * 'continue' are silently dropped and fall back to 'plain' (verified in
712
+ * Safari 26 on macOS and iOS, see PRD-1426).
713
+ *
714
+ * Apple's official custom element supports the full set of button types, so
715
+ * for "native-only" types we overlay it on top of the (visually hidden)
716
+ * Primer button and forward clicks, keeping Primer's payment flow untouched.
717
+ */
718
+ const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
719
+ /** How long to wait for the <apple-pay-button> element definition before
720
+ * giving up and falling back to Primer's own button. */
721
+ const ELEMENT_DEFINITION_TIMEOUT_MS = 4000;
722
+ /**
723
+ * Button types WebKit can render through Primer's CSS approach.
724
+ * Everything outside this list requires the native <apple-pay-button> element.
725
+ */
726
+ const CSS_RENDERABLE_APPLE_PAY_TYPES = [
727
+ 'plain',
728
+ 'buy',
729
+ 'set-up',
730
+ 'donate',
731
+ 'check-out',
732
+ 'book',
733
+ 'subscribe',
734
+ ];
735
+ /**
736
+ * Returns true when the requested Apple Pay buttonType can only be rendered
737
+ * with Apple's native <apple-pay-button> element.
738
+ */
739
+ function isNativeOnlyApplePayButtonType(type) {
740
+ return !!type && !CSS_RENDERABLE_APPLE_PAY_TYPES.includes(type);
741
+ }
742
+ /**
743
+ * Loads Apple's apple-pay-sdk.js (defines the <apple-pay-button> custom
744
+ * element). Resolves to true when the element is available.
745
+ */
746
+ async function loadApplePayButtonElement() {
747
+ if (typeof window === 'undefined' || !window.customElements) {
748
+ return false;
749
+ }
750
+ if (window.customElements.get('apple-pay-button')) {
751
+ return true;
752
+ }
753
+ try {
754
+ await loadScript$1({
755
+ src: APPLE_PAY_SDK_URL,
756
+ crossOrigin: 'anonymous',
757
+ appendTo: 'head',
758
+ });
759
+ // Never hang on a script that loaded but did not define the element;
760
+ // fall back to Primer's own button instead.
761
+ const defined = window.customElements
762
+ .whenDefined('apple-pay-button')
763
+ .then(() => true);
764
+ const timeout = new Promise(resolve => {
765
+ setTimeout(() => resolve(false), ELEMENT_DEFINITION_TIMEOUT_MS);
766
+ });
767
+ return await Promise.race([defined, timeout]);
768
+ }
769
+ catch {
770
+ return false;
771
+ }
772
+ }
773
+ /**
774
+ * Renders Apple's native <apple-pay-button> stretched over `wrapper` and
775
+ * forwards clicks to the Primer-rendered <button> inside it. The synchronous
776
+ * forwarded click preserves the user activation required by ApplePaySession.
777
+ *
778
+ * Returns the overlay element, or null when the Apple Pay SDK could not be
779
+ * loaded (callers should keep Primer's own button visible as a fallback).
780
+ */
781
+ async function overlayNativeApplePayButton(wrapper, options) {
782
+ const available = await loadApplePayButtonElement();
783
+ if (!available) {
784
+ return null;
785
+ }
786
+ // The overlay is only safe when Primer's own clickable button is present;
787
+ // otherwise there is nothing to forward the click to.
788
+ if (!wrapper.querySelector('button')) {
789
+ return null;
790
+ }
791
+ const native = document.createElement('apple-pay-button');
792
+ native.setAttribute('type', options.buttonType);
793
+ native.setAttribute('buttonstyle', options.buttonStyle || 'black');
794
+ native.setAttribute('locale', (typeof navigator !== 'undefined' && navigator.language) || 'en-US');
795
+ native.style.cssText = [
796
+ 'position:absolute',
797
+ 'inset:0',
798
+ 'width:100%',
799
+ 'height:100%',
800
+ '--apple-pay-button-width:100%',
801
+ '--apple-pay-button-height:100%',
802
+ '--apple-pay-button-border-radius:28px',
803
+ 'display:block',
804
+ 'cursor:pointer',
805
+ 'z-index:1',
806
+ ].join(';');
807
+ wrapper.style.position = 'relative';
808
+ native.addEventListener('click', () => {
809
+ const primerButton = wrapper.querySelector('button');
810
+ primerButton?.click();
811
+ });
812
+ wrapper.appendChild(native);
813
+ return native;
814
+ }
815
+
679
816
  /**
680
817
  * @fileoverview Primer SDK integration wrapper
681
818
  */
@@ -714,6 +851,7 @@
714
851
  }
715
852
  async createHeadlessCheckout(clientToken, options, method) {
716
853
  await this.ensurePrimerLoaded();
854
+ this.applePayOptions = options.applePay;
717
855
  this.currentHeadless = PrimerWrapper.headlessManager.getOrCreate(clientToken, options, method);
718
856
  return this.currentHeadless;
719
857
  }
@@ -760,16 +898,56 @@
760
898
  }
761
899
  htmlNode.appendChild(wrapper);
762
900
  /* end hack */
901
+ /* PRD-1426: WebKit cannot render newer Apple Pay button types (e.g.
902
+ * 'continue') through the CSS approach Primer v2 uses. For those types
903
+ * we overlay Apple's official <apple-pay-button> element on top of the
904
+ * visually hidden Primer button and forward clicks, keeping Primer's
905
+ * payment flow untouched. The Primer button is hidden BEFORE it renders
906
+ * to avoid a flash of the plain button; if the Apple SDK fails to load,
907
+ * the rule is removed and Primer's own button stays visible (graceful
908
+ * fallback to 'plain'). */
909
+ const applePayConfig = this.applePayOptions;
910
+ const nativeButtonType = allowedPaymentMethod === exports.PaymentMethod.APPLE_PAY &&
911
+ applePayConfig?.buttonType &&
912
+ isNativeOnlyApplePayButtonType(applePayConfig.buttonType)
913
+ ? applePayConfig.buttonType
914
+ : null;
915
+ let hideRuleIndex = null;
916
+ if (nativeButtonType && sheet) {
917
+ hideRuleIndex = sheet.insertRule(`
918
+ .${wrapper.className} > button {
919
+ opacity: 0 !important;
920
+ }
921
+ `);
922
+ }
763
923
  button = pmManager.createButton();
764
924
  await button.render(wrapper, {});
765
- this.destroyCallbacks.push(() => button.clean());
925
+ let nativeOverlay = null;
926
+ if (nativeButtonType) {
927
+ nativeOverlay = await overlayNativeApplePayButton(wrapper, {
928
+ buttonType: nativeButtonType,
929
+ buttonStyle: applePayConfig?.buttonStyle,
930
+ });
931
+ if (!nativeOverlay && sheet && hideRuleIndex !== null) {
932
+ sheet.deleteRule(hideRuleIndex);
933
+ }
934
+ }
935
+ this.destroyCallbacks.push(() => {
936
+ nativeOverlay?.remove();
937
+ button.clean();
938
+ });
766
939
  onMethodRender(allowedPaymentMethod);
767
940
  return {
768
941
  setDisabled: (disabled) => {
769
942
  button.setDisabled(disabled);
943
+ if (nativeOverlay) {
944
+ nativeOverlay.style.pointerEvents = disabled ? 'none' : '';
945
+ nativeOverlay.style.opacity = disabled ? '0.5' : '';
946
+ }
770
947
  },
771
948
  destroy: () => {
772
949
  styleEl.remove();
950
+ nativeOverlay?.remove();
773
951
  button.clean();
774
952
  },
775
953
  };