@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.
- package/dist/chunk-index.cjs.js +180 -2
- package/dist/chunk-index.es.js +180 -2
- package/dist/funnelfox-billing.js +180 -2
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +62 -3
package/dist/chunk-index.cjs.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
};
|
package/dist/chunk-index.es.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
};
|