@economist/web-apple-pay 0.1.1-beta.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 +186 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1642 -0
- package/dist/src/apple-pay-button.d.ts +45 -0
- package/dist/src/apple-pay-button.js +1621 -0
- package/dist/src/apple-pay-button.stories.js +1934 -0
- package/dist/src/apple-pay-logo.d.ts +1 -0
- package/dist/src/apple-pay-logo.js +14 -0
- package/dist/src/apple-pay-modal.d.ts +12 -0
- package/dist/src/apple-pay-modal.js +442 -0
- package/dist/src/apple-pay-modal.stories.js +1778 -0
- package/dist/src/clients/payment-checkout/apple-session.d.ts +18 -0
- package/dist/src/clients/payment-checkout/apple-session.js +243 -0
- package/dist/src/clients/payment-checkout/billing-contact-details.d.ts +21 -0
- package/dist/src/clients/payment-checkout/billing-contact-details.js +109 -0
- package/dist/src/clients/payment-checkout/config.d.ts +9 -0
- package/dist/src/clients/payment-checkout/config.js +23 -0
- package/dist/src/clients/payment-checkout/contact-validation.d.ts +21 -0
- package/dist/src/clients/payment-checkout/contact-validation.js +50 -0
- package/dist/src/clients/payment-checkout/detects.d.ts +3 -0
- package/dist/src/clients/payment-checkout/detects.js +56 -0
- package/dist/src/clients/payment-checkout/index.d.ts +5 -0
- package/dist/src/clients/payment-checkout/index.js +83 -0
- package/dist/src/clients/payment-checkout/messages.d.ts +33 -0
- package/dist/src/clients/payment-checkout/messages.js +19 -0
- package/dist/src/clients/payment-checkout/payment-handler.d.ts +36 -0
- package/dist/src/clients/payment-checkout/payment-handler.js +389 -0
- package/dist/src/clients/payment-checkout/session.d.ts +2 -0
- package/dist/src/clients/payment-checkout/session.js +67 -0
- package/dist/src/clients/payment-checkout/shipping-contact-details.d.ts +10 -0
- package/dist/src/clients/payment-checkout/shipping-contact-details.js +83 -0
- package/dist/src/clients/payment-checkout/types.d.ts +118 -0
- package/dist/src/clients/payment-checkout/types.js +14 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +1642 -0
- package/dist/src/stories/fixtures/offer.d.ts +2 -0
- package/dist/src/stories/fixtures/offer.js +28 -0
- package/dist/src/stories/utils/story-env.d.ts +11 -0
- package/dist/src/stories/utils/story-env.js +148 -0
- package/dist/src/types/html-template.d.js +0 -0
- package/dist/src/types/offer.types.d.ts +73 -0
- package/dist/src/types/offer.types.js +0 -0
- package/dist/src/utils/billing-countries.d.ts +19 -0
- package/dist/src/utils/billing-countries.js +263 -0
- package/dist/src/utils/define.d.ts +1 -0
- package/dist/src/utils/define.js +9 -0
- package/dist/src/utils/expressCheckoutRedirect.d.ts +55 -0
- package/dist/src/utils/expressCheckoutRedirect.js +46 -0
- package/dist/src/utils/expressCheckoutTracking.d.ts +64 -0
- package/dist/src/utils/expressCheckoutTracking.js +84 -0
- package/dist/src/utils/pricing.d.ts +1 -0
- package/dist/src/utils/pricing.js +60 -0
- package/dist/src/utils/recaptcha.d.ts +22 -0
- package/dist/src/utils/recaptcha.js +54 -0
- package/package.json +64 -0
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
// src/utils/define.ts
|
|
2
|
+
var define = (component, instance) => {
|
|
3
|
+
if (!window.customElements.get(component)) {
|
|
4
|
+
window.customElements.define(component, instance);
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/utils/pricing.ts
|
|
9
|
+
var PRICE_CONSTANTS = {
|
|
10
|
+
CENTS_MULTIPLIER: 100,
|
|
11
|
+
DECIMAL_PLACES: 2,
|
|
12
|
+
NO_DECIMAL_PLACES: 0
|
|
13
|
+
};
|
|
14
|
+
function numberOfDecimalPoints(value) {
|
|
15
|
+
if (!value) {
|
|
16
|
+
return PRICE_CONSTANTS.NO_DECIMAL_PLACES;
|
|
17
|
+
}
|
|
18
|
+
return Number(value.toFixed(PRICE_CONSTANTS.DECIMAL_PLACES)) % 1 === 0 ? PRICE_CONSTANTS.NO_DECIMAL_PLACES : PRICE_CONSTANTS.DECIMAL_PLACES;
|
|
19
|
+
}
|
|
20
|
+
var priceFormatters = {
|
|
21
|
+
CAD: (price) => `C$${price}`,
|
|
22
|
+
SEK: (price) => `${price}Kr`,
|
|
23
|
+
AUD: (price) => `A$${price}`,
|
|
24
|
+
NZD: (price) => `NZ$${price}`,
|
|
25
|
+
HKD: (price) => `HK$${price}`,
|
|
26
|
+
DKK: (price) => `${price}Kr`,
|
|
27
|
+
NOK: (price) => `${price}Kr`,
|
|
28
|
+
CHF: (price) => `${price}Fr`,
|
|
29
|
+
SGD: (price) => `S$${price}`,
|
|
30
|
+
TWD: (price) => `TW$${price}`
|
|
31
|
+
};
|
|
32
|
+
function formatPrice(price, currencyCode, shouldFixDecimalPlaces = true, roundUp = false) {
|
|
33
|
+
if (price === void 0) {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
const points = numberOfDecimalPoints(price);
|
|
37
|
+
const calculatedPrice = () => {
|
|
38
|
+
if (roundUp) {
|
|
39
|
+
return (Math.ceil(price * PRICE_CONSTANTS.CENTS_MULTIPLIER) / PRICE_CONSTANTS.CENTS_MULTIPLIER).toFixed(PRICE_CONSTANTS.DECIMAL_PLACES);
|
|
40
|
+
}
|
|
41
|
+
if (shouldFixDecimalPlaces) {
|
|
42
|
+
return (Math.round(price * PRICE_CONSTANTS.CENTS_MULTIPLIER) / PRICE_CONSTANTS.CENTS_MULTIPLIER).toFixed(PRICE_CONSTANTS.DECIMAL_PLACES);
|
|
43
|
+
}
|
|
44
|
+
return price;
|
|
45
|
+
};
|
|
46
|
+
const priceFixDecimal = calculatedPrice();
|
|
47
|
+
const priceFormatter = priceFormatters[currencyCode];
|
|
48
|
+
if (priceFormatter) {
|
|
49
|
+
const withCommas = Number(priceFixDecimal).toLocaleString("en", {
|
|
50
|
+
minimumFractionDigits: points
|
|
51
|
+
});
|
|
52
|
+
return priceFormatter(withCommas);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return Number(priceFixDecimal).toLocaleString("en", {
|
|
56
|
+
style: "currency",
|
|
57
|
+
currency: currencyCode,
|
|
58
|
+
minimumFractionDigits: points,
|
|
59
|
+
currencyDisplay: "narrowSymbol"
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
return `${currencyCode} ${priceFixDecimal}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/apple-pay-logo.ts
|
|
67
|
+
var LOGO_DEFS_ID = "apple-pay-logo-defs";
|
|
68
|
+
var LOGO_PATH = "M10.2651 4.21143C10.4299 4.21143 10.7106 4.23763 11.1074 4.29004C11.5042 4.34245 11.9385 4.49219 12.4102 4.73926C12.8893 4.97884 13.3236 5.37939 13.7129 5.94092C13.6904 5.96338 13.5819 6.04574 13.3872 6.18799C13.1925 6.32275 12.9754 6.5249 12.7358 6.79443C12.4963 7.05648 12.2866 7.39339 12.1069 7.80518C11.9272 8.20947 11.8374 8.69238 11.8374 9.25391C11.8374 9.89779 11.9497 10.4443 12.1743 10.8936C12.4064 11.3428 12.6722 11.7059 12.9717 11.9829C13.2786 12.2524 13.5482 12.4508 13.7803 12.5781C14.0199 12.7054 14.1471 12.7728 14.1621 12.7803C14.1546 12.8102 14.0573 13.076 13.8701 13.5776C13.6904 14.0793 13.391 14.637 12.9717 15.251C12.6048 15.7826 12.208 16.2729 11.7812 16.7222C11.362 17.1714 10.8566 17.396 10.2651 17.396C9.86833 17.396 9.54264 17.3398 9.28809 17.2275C9.03353 17.1077 8.77148 16.9917 8.50195 16.8794C8.23242 16.7596 7.8693 16.6997 7.4126 16.6997C6.97087 16.6997 6.60026 16.7596 6.30078 16.8794C6.00879 16.9992 5.72803 17.119 5.4585 17.2388C5.19645 17.3586 4.88574 17.4185 4.52637 17.4185C3.97982 17.4185 3.50065 17.2013 3.08887 16.7671C2.67708 16.3328 2.25407 15.8125 1.81982 15.2061C1.3182 14.4873 0.887695 13.6113 0.52832 12.5781C0.176432 11.5374 0.000488281 10.4893 0.000488281 9.43359C0.000488281 8.30306 0.213867 7.35596 0.640625 6.59229C1.06738 5.82113 1.61393 5.24089 2.28027 4.85156C2.9541 4.45475 3.65039 4.25635 4.36914 4.25635C4.75098 4.25635 5.11035 4.31999 5.44727 4.44727C5.78418 4.56706 6.09863 4.69059 6.39062 4.81787C6.6901 4.94515 6.95964 5.00879 7.19922 5.00879C7.43132 5.00879 7.70085 4.94141 8.00781 4.80664C8.31478 4.67188 8.65918 4.54085 9.04102 4.41357C9.42285 4.27881 9.83089 4.21143 10.2651 4.21143ZM9.64746 2.78516C9.35547 3.13704 8.98861 3.43278 8.54688 3.67236C8.10514 3.90446 7.68587 4.02051 7.28906 4.02051C7.20671 4.02051 7.12809 4.01302 7.05322 3.99805C7.04574 3.97559 7.03825 3.93441 7.03076 3.87451C7.02327 3.81462 7.01953 3.75098 7.01953 3.68359C7.01953 3.23438 7.11686 2.80013 7.31152 2.38086C7.50618 1.9541 7.72705 1.60221 7.97412 1.3252C8.28857 0.950846 8.68538 0.640137 9.16455 0.393066C9.64372 0.145996 10.1004 0.014974 10.5347 0C10.5571 0.0973307 10.5684 0.213379 10.5684 0.348145C10.5684 0.797363 10.4823 1.23535 10.3101 1.66211C10.1379 2.08138 9.91699 2.45573 9.64746 2.78516ZM17.6885 11.5112V9.71436H22.0459C23.2064 9.71436 24.1048 9.41488 24.7412 8.81592C25.3776 8.20947 25.6958 7.37467 25.6958 6.31152V6.28906C25.6958 5.21842 25.3776 4.38363 24.7412 3.78467C24.1048 3.18571 23.2064 2.88623 22.0459 2.88623H17.6885V1.08936H22.5625C23.5807 1.08936 24.4792 1.30648 25.2578 1.74072C26.0365 2.17497 26.6466 2.78141 27.0884 3.56006C27.5376 4.3387 27.7622 5.24089 27.7622 6.2666V6.28906C27.7622 7.31478 27.5376 8.2207 27.0884 9.00684C26.6466 9.79297 26.0365 10.4069 25.2578 10.8486C24.4792 11.2904 23.5807 11.5112 22.5625 11.5112H17.6885ZM16.6777 17.2949V1.08936H18.6992V17.2949H16.6777ZM32.6699 17.5195C31.9062 17.5195 31.2249 17.3735 30.626 17.0815C30.0345 16.7896 29.5703 16.3778 29.2334 15.8462C28.904 15.3071 28.7393 14.682 28.7393 13.9707V13.9482C28.7393 13.2594 28.9077 12.668 29.2446 12.1738C29.5815 11.6797 30.0682 11.2904 30.7046 11.0059C31.3485 10.7139 32.1234 10.5417 33.0293 10.4893L37.3979 10.2422V11.7695L33.2539 12.0278C32.4303 12.0728 31.8052 12.2599 31.3784 12.5894C30.9517 12.9188 30.7383 13.3643 30.7383 13.9258V13.9482C30.7383 14.5322 30.9554 14.9927 31.3896 15.3296C31.8239 15.659 32.3929 15.8237 33.0967 15.8237C33.7331 15.8237 34.2983 15.6927 34.7925 15.4307C35.2941 15.1686 35.6909 14.813 35.9829 14.3638C36.2749 13.9071 36.4209 13.3979 36.4209 12.8364V9.27637C36.4209 8.55762 36.1963 7.99609 35.7471 7.5918C35.2979 7.1875 34.6502 6.98535 33.8042 6.98535C33.063 6.98535 32.4603 7.13883 31.9961 7.4458C31.5394 7.74528 31.2474 8.14958 31.1201 8.65869L31.0977 8.7373H29.1885L29.1997 8.625C29.2746 7.98861 29.5067 7.41585 29.896 6.90674C30.2928 6.39762 30.8244 5.99707 31.4907 5.70508C32.1646 5.4056 32.9469 5.25586 33.8379 5.25586C34.7812 5.25586 35.5898 5.41683 36.2637 5.73877C36.945 6.05322 37.4653 6.50244 37.8247 7.08643C38.1916 7.66292 38.375 8.35173 38.375 9.15283V17.2949H36.4209V15.4307H36.3311C36.0915 15.8574 35.7845 16.228 35.4102 16.5425C35.0433 16.8569 34.6278 17.1003 34.1636 17.2725C33.6994 17.4372 33.2015 17.5195 32.6699 17.5195ZM42.1484 21.3267C41.9837 21.3267 41.8115 21.3192 41.6318 21.3042C41.4521 21.2892 41.2987 21.2705 41.1714 21.248V19.6421C41.2612 19.6646 41.366 19.6795 41.4858 19.687C41.6056 19.702 41.7292 19.7095 41.8564 19.7095C42.4554 19.7095 42.9383 19.5897 43.3052 19.3501C43.672 19.1105 43.9678 18.6576 44.1924 17.9912L44.4282 17.3062L40.0371 5.48047H42.126L45.6187 15.981L45.0796 15.2734H45.7759L45.248 15.981L48.7407 5.48047H50.7959L46.1802 18.0249C45.8807 18.826 45.555 19.4661 45.2031 19.9453C44.8512 20.432 44.432 20.7839 43.9453 21.001C43.4587 21.2181 42.8597 21.3267 42.1484 21.3267Z";
|
|
69
|
+
function ensureApplePayLogoSymbol() {
|
|
70
|
+
if (document.getElementById(LOGO_DEFS_ID)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const tmp = document.createElement("div");
|
|
74
|
+
tmp.innerHTML = `<svg aria-hidden="true" style="display:none" id="${LOGO_DEFS_ID}"><defs><symbol id="apple-pay-logo" viewBox="0 0 51 22"><path d="${LOGO_PATH}" fill="white"/></symbol></defs></svg>`;
|
|
75
|
+
document.body.prepend(tmp.firstElementChild);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/templates/apple-pay-modal.template.html
|
|
79
|
+
var apple_pay_modal_template_default = '<style>\n .apm-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 97;\n background: rgba(13, 13, 13, 0.7);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--wall-gutter, 2rem);\n box-sizing: border-box;\n }\n\n .apm {\n position: relative;\n background: #fff;\n border-radius: 0.5rem 0.5rem 0 0;\n border: 0.0625rem solid #d9d9d9;\n padding: var(--mb-spacing-lg, 1.5rem);\n width: 25rem;\n max-width: 25rem;\n box-sizing: border-box;\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n box-shadow: 0 0.25rem 1rem 0 rgba(13, 13, 13, 0.15);\n }\n\n .apm__close {\n position: absolute;\n top: 1.5rem;\n right: 1.3125rem;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .apm__close:focus {\n outline: solid 0.125rem var(--focus-border-color, #121212);\n border-radius: 0.25rem;\n }\n\n .apm__offer-name {\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: var(--mb-typestyle-title-3-size, 1.25rem);\n font-weight: 500;\n color: var(--mb-colour-greyscale-london-5, #0d0d0d);\n line-height: var(--mb-typestyle-title-3-leading, 1.75rem);\n font-style: normal;\n margin: 0 1.5rem 0.5rem 0;\n }\n\n .apm__due-today {\n color: #1a1a1a;\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: var(--mb-typestyle-label-2-size, 0.9375rem);\n font-style: normal;\n font-weight: 400;\n line-height: var(--mb-typestyle-label-2-leading, 1.25rem);\n margin: 0 0 0.75rem 0;\n }\n\n .apm__rule {\n border: none;\n border-top: 0.0625rem solid #d9d9d9;\n margin: 0 0 1rem 0;\n }\n\n .apm__terms {\n color: #1a1a1a;\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: var(--mb-typestyle-label-2-size, 0.9375rem);\n font-style: normal;\n font-weight: 400;\n line-height: var(--mb-typestyle-label-2-leading, 1.25rem);\n margin: 0 0 1rem 0;\n }\n\n .apm__terms-bullets {\n list-style: disc outside;\n padding-left: 1.25rem;\n margin: 0 0 0.75rem 0;\n }\n\n .apm__terms-bullets li {\n margin-bottom: 0.375rem;\n }\n\n .apm__terms-legal {\n margin: 0;\n }\n\n .apm__passive-terms {\n font-family: var(--wall-type-system-sans);\n font-size: 0.9375rem;\n color: #0d0d0d;\n line-height: 1.4;\n margin: 0 0 8px 0;\n }\n\n .apm__passive-terms a {\n color: #1a1a1a;\n font-weight: 700;\n text-decoration: underline;\n }\n\n .apm__checkbox-label {\n display: flex;\n align-items: flex-start;\n gap: var(--mb-spacing-sm, 0.5rem);\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: var(--mb-typestyle-label-2-size, 0.9375rem);\n color: var(--mb-colour-greyscale-london-20, #1a1a1a);\n line-height: var(--mb-typestyle-label-2-leading, 1.25rem);\n cursor: pointer;\n margin: 0 0 1.5rem 0;\n }\n\n .apm__checkbox-label--error {\n margin-bottom: 0.25rem;\n }\n\n .apm__checkbox-wrapper {\n position: relative;\n flex-shrink: 0;\n width: 1.25rem;\n height: 1.25rem;\n margin-top: 0.0625rem;\n }\n\n .apm__checkbox-input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n cursor: pointer;\n margin: 0;\n z-index: 1;\n }\n\n .apm__checkbox-custom {\n position: absolute;\n top: 0;\n left: 0;\n width: 1.25rem;\n height: 1.25rem;\n pointer-events: none;\n }\n\n .apm__checkbox-custom svg {\n display: none;\n width: 1.25rem;\n height: 1.25rem;\n }\n\n .apm__checkbox-input:not(:checked) ~ .apm__checkbox-custom .svg-unchecked {\n display: block;\n }\n\n .apm__checkbox-input:checked ~ .apm__checkbox-custom .svg-checked {\n display: block;\n }\n\n .apm__checkbox-label--error\n .apm__checkbox-input:not(:checked)\n ~ .apm__checkbox-custom\n .svg-unchecked {\n display: none;\n }\n\n .apm__checkbox-label--error\n .apm__checkbox-input:not(:checked)\n ~ .apm__checkbox-custom\n .svg-error {\n display: block;\n }\n\n .apm__checkbox-input:focus-visible ~ .apm__checkbox-custom {\n outline: solid 0.125rem #c91d42;\n outline-offset: 0.125rem;\n border-radius: 0.25rem;\n }\n\n .apm__checkbox-label a {\n color: #1a1a1a;\n font-weight: 700;\n text-decoration: underline;\n }\n\n .apm__error {\n display: none;\n align-items: center;\n gap: 0.75rem;\n color: var(--mb-colour-greyscale-london-5, #0d0d0d);\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: 1.0625rem;\n font-style: normal;\n font-weight: 500;\n line-height: 1.5rem;\n margin-bottom: 1rem;\n }\n\n .apm__error--visible {\n display: flex;\n }\n\n .apm__cta {\n display: flex;\n width: 100%;\n height: 2.75rem;\n padding: 0.375rem 1rem;\n justify-content: center;\n align-items: center;\n gap: 0.375rem;\n border-radius: 0.25rem;\n background: #000;\n border: none;\n cursor: pointer;\n color: #fff;\n text-align: center;\n font-family: var(\n --mb-typeface-sans,\n var(--wall-type-system-sans, system-ui, sans-serif)\n );\n font-size: 1.25rem;\n font-style: normal;\n font-weight: 500;\n line-height: normal;\n }\n\n .apm__cta svg {\n flex-shrink: 0;\n display: block;\n }\n\n @media (min-width: 22.5rem) and (max-width: 37.5rem) {\n .apm {\n width: 24.375rem;\n max-width: 24.375rem;\n }\n }\n\n @media (min-width: 37.5625rem) {\n .apm {\n border-radius: 0.5rem;\n }\n }\n\n @media (max-width: 22.4375rem) {\n .apm {\n width: 20rem;\n max-width: 20rem;\n }\n }\n\n @media (max-width: 26.1875rem) {\n .apm__cta {\n font-size: 1.125rem;\n }\n }\n</style>\n\n<div class="apm-overlay" id="apple-pay-modal-overlay">\n <div\n class="apm"\n role="dialog"\n aria-modal="true"\n aria-labelledby="apple-pay-modal-offer-name"\n id="apple-pay-modal"\n tabindex="-1"\n >\n <button class="apm__close" id="apple-pay-modal-close" aria-label="Close">\n <svg\n aria-hidden="true"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n >\n <g clip-path="url(#clip0_15712_93054)">\n <rect width="24" height="24" fill="white" fill-opacity="0.01" />\n <path\n fill-rule="evenodd"\n clip-rule="evenodd"\n d="M0 0H24V24H0V0Z"\n fill="white"\n fill-opacity="0.01"\n />\n <path\n fill-rule="evenodd"\n clip-rule="evenodd"\n d="M18.75 6.60964L17.3904 5.25L12 10.6404L6.60964 5.25L5.25 6.60964L10.6404 12L5.25 17.3904L6.60964 18.75L12 13.3596L17.3904 18.75L18.75 17.3904L13.3596 12L18.75 6.60964Z"\n fill="#333333"\n />\n </g>\n <defs>\n <clipPath id="clip0_15712_93054">\n <rect width="24" height="24" fill="white" />\n </clipPath>\n </defs>\n </svg>\n </button>\n <p class="apm__offer-name" id="apple-pay-modal-offer-name"></p>\n <p class="apm__due-today" id="apple-pay-modal-due-today"></p>\n <hr class="apm__rule" />\n <div class="apm__terms" id="apple-pay-modal-terms"></div>\n <p class="apm__passive-terms" id="apple-pay-modal-passive-terms"></p>\n <label\n class="apm__checkbox-label"\n id="apple-pay-modal-checkbox-label"\n for="apple-pay-modal-checkbox"\n >\n <div class="apm__checkbox-wrapper">\n <input\n class="apm__checkbox-input"\n type="checkbox"\n id="apple-pay-modal-checkbox"\n />\n <span class="apm__checkbox-custom">\n <svg\n aria-hidden="true"\n class="svg-unchecked"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M6 2.5H18C19.933 2.5 21.5 4.067 21.5 6V18C21.5 19.933 19.933 21.5 18 21.5H6C4.067 21.5 2.5 19.933 2.5 18V6C2.5 4.067 4.067 2.5 6 2.5Z"\n fill="white"\n stroke="#595959"\n />\n </svg>\n <svg\n aria-hidden="true"\n class="svg-checked"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <g clip-path="url(#clip0_20014_9555)">\n <path\n d="M2 6C2 3.79086 3.79086 2 6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6Z"\n fill="#333333"\n />\n <path\n d="M10.2857 17L6 12.6814L7.20857 11.4635L10.2857 14.5557L16.7914 8L18 9.22649L10.2857 17Z"\n fill="white"\n />\n </g>\n <defs>\n <clipPath id="clip0_20014_9555">\n <path\n d="M2 6C2 3.79086 3.79086 2 6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6Z"\n fill="white"\n />\n </clipPath>\n </defs>\n </svg>\n <svg\n aria-hidden="true"\n class="svg-error"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n d="M6 2.5H18C19.933 2.5 21.5 4.067 21.5 6V18C21.5 19.933 19.933 21.5 18 21.5H6C4.067 21.5 2.5 19.933 2.5 18V6C2.5 4.067 4.067 2.5 6 2.5Z"\n fill="white"\n stroke="#C91D42"\n stroke-width="2"\n />\n </svg>\n </span>\n </div>\n <span id="apple-pay-modal-checkbox-text"></span>\n </label>\n <div class="apm__error" id="apple-pay-modal-error" role="alert">\n <svg\n aria-hidden="true"\n width="20"\n height="20"\n viewBox="0 0 20 20"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n >\n <path\n fill-rule="evenodd"\n clip-rule="evenodd"\n d="M10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM0 10C0 4.47715 4.47715 0 10 0C15.5228 0 20 4.47715 20 10C20 15.5228 15.5228 20 10 20C4.47715 20 0 15.5228 0 10ZM11 13C11 13.5523 10.5523 14 10 14C9.44772 14 9 13.5523 9 13C9 12.4477 9.44772 12 10 12C10.5523 12 11 12.4477 11 13ZM10 6C10.5523 6 11 6.44772 11 7V10C11 10.5523 10.5523 11 10 11C9.44772 11 9 10.5523 9 10V7C9 6.44772 9.44772 6 10 6Z"\n fill="#C91D42"\n />\n </svg>\n Please accept the terms to continue\n </div>\n <button class="apm__cta" id="apple-pay-modal-cta">\n Continue with\n <svg width="51" height="22" aria-label="Apple Pay" role="img">\n <use href="#apple-pay-logo" />\n </svg>\n </button>\n </div>\n</div>\n\n';
|
|
80
|
+
|
|
81
|
+
// src/apple-pay-modal.ts
|
|
82
|
+
function getRenewalDateText(unit, duration) {
|
|
83
|
+
const now = /* @__PURE__ */ new Date();
|
|
84
|
+
let date = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
85
|
+
switch (unit) {
|
|
86
|
+
case "week":
|
|
87
|
+
date.setDate(date.getDate() + duration * 7);
|
|
88
|
+
break;
|
|
89
|
+
case "month":
|
|
90
|
+
date.setMonth(date.getMonth() + duration);
|
|
91
|
+
if (date.getMonth() !== (now.getMonth() + duration) % 12) {
|
|
92
|
+
date = new Date(date.getFullYear(), date.getMonth(), 0);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case "year":
|
|
96
|
+
date.setFullYear(date.getFullYear() + duration);
|
|
97
|
+
if (date.getMonth() !== now.getMonth()) {
|
|
98
|
+
date = new Date(date.getFullYear(), date.getMonth(), 0);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return date.toLocaleString("en-US", {
|
|
105
|
+
year: "numeric",
|
|
106
|
+
month: "long",
|
|
107
|
+
day: "numeric"
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
var MIN_TERM_DURATION_PATTERN = /^(\d+)\s+(\w+)s?$/;
|
|
111
|
+
function getMinTermDurationUnits(minTermContractLength) {
|
|
112
|
+
if (!minTermContractLength) {
|
|
113
|
+
return { duration: "", unit: "" };
|
|
114
|
+
}
|
|
115
|
+
const result = minTermContractLength.split(MIN_TERM_DURATION_PATTERN);
|
|
116
|
+
const [, duration = "", rawUnit = ""] = result;
|
|
117
|
+
return { duration, unit: rawUnit.replace(/s$/, "") };
|
|
118
|
+
}
|
|
119
|
+
function getOfferTypeText(offerType) {
|
|
120
|
+
switch (offerType) {
|
|
121
|
+
case "student":
|
|
122
|
+
return "student";
|
|
123
|
+
case "academic":
|
|
124
|
+
return "teacher/faculty";
|
|
125
|
+
default:
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var TERMS_LINK = '<a href="https://www.economistgroup.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>';
|
|
130
|
+
var PRIVACY_LINK = '<a href="https://www.economistgroup.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>';
|
|
131
|
+
function getCheckboxTermsHtml(isNYT, isLoggedIn, autoRenewal) {
|
|
132
|
+
const autoRenewalClause = autoRenewal ? " and auto-renewal terms above," : "";
|
|
133
|
+
if (isNYT) {
|
|
134
|
+
const base = `I accept The Economist ${TERMS_LINK} and auto-renewal terms above and acknowledge the ${PRIVACY_LINK}.`;
|
|
135
|
+
if (isLoggedIn) {
|
|
136
|
+
return base;
|
|
137
|
+
}
|
|
138
|
+
return `${base} I understand that by continuing, an account will be created for me if I don\u2019t already have one.`;
|
|
139
|
+
}
|
|
140
|
+
if (isLoggedIn) {
|
|
141
|
+
return `I accept the ${TERMS_LINK}${autoRenewalClause} and acknowledge the ${PRIVACY_LINK}.`;
|
|
142
|
+
}
|
|
143
|
+
return `I accept the ${TERMS_LINK}${autoRenewalClause} and acknowledge the ${PRIVACY_LINK}, and understand that by continuing, an account will be created for me if I don\u2019t already have one.`;
|
|
144
|
+
}
|
|
145
|
+
function getPassiveTermsHtml(isNYT, isLoggedIn, autoRenewal) {
|
|
146
|
+
const accountClause = isLoggedIn ? " " : "\u2019ll get an account and ";
|
|
147
|
+
const autoRenewalClause = autoRenewal ? "above auto-renewal terms, " : "";
|
|
148
|
+
const nytPrefix = isNYT ? " your subscription to The Economist," : "";
|
|
149
|
+
return `By purchasing${nytPrefix} you${accountClause}agree to the ${autoRenewalClause}${TERMS_LINK} and acknowledge the ${PRIVACY_LINK}.`;
|
|
150
|
+
}
|
|
151
|
+
function renewalPeriodText(unit, duration) {
|
|
152
|
+
switch (unit) {
|
|
153
|
+
case "week":
|
|
154
|
+
return duration === 1 ? "weekly" : `every ${duration} weeks`;
|
|
155
|
+
case "month":
|
|
156
|
+
return duration === 1 ? "monthly" : `every ${duration} months`;
|
|
157
|
+
case "year":
|
|
158
|
+
return duration === 1 ? "annually" : `every ${duration} years`;
|
|
159
|
+
default:
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function buildTermsContext(offer, country) {
|
|
164
|
+
const isUS = country === "US";
|
|
165
|
+
const isCA = country === "CA";
|
|
166
|
+
const isTaxIncluded = offer.price.tax?.inclusive;
|
|
167
|
+
const unit = offer.subscription_options?.renewal_interval?.unit ?? "";
|
|
168
|
+
const duration = offer.subscription_options?.renewal_interval?.duration ?? 1;
|
|
169
|
+
const isMinTermContract = offer.subscription_options?.min_term_contract || false;
|
|
170
|
+
const renewalPriceText = formatPrice(
|
|
171
|
+
isMinTermContract ? offer.price.pre_discount : offer.subscription_options?.renewal_price,
|
|
172
|
+
offer.price.currency_code,
|
|
173
|
+
!isMinTermContract
|
|
174
|
+
);
|
|
175
|
+
const offerTypeText = getOfferTypeText(offer.product?.offer_type);
|
|
176
|
+
const verificationText = offerTypeText ? `, pending ${offerTypeText} verification` : "";
|
|
177
|
+
const renewalDateText = getRenewalDateText(unit, duration);
|
|
178
|
+
return {
|
|
179
|
+
isUS,
|
|
180
|
+
isCA,
|
|
181
|
+
unit,
|
|
182
|
+
duration,
|
|
183
|
+
taxSuffix: isTaxIncluded ? "" : "*",
|
|
184
|
+
taxFootnote: isTaxIncluded ? "" : " *Plus any applicable sales tax.",
|
|
185
|
+
periodText: renewalPeriodText(unit, duration),
|
|
186
|
+
renewalPriceText,
|
|
187
|
+
renewalDateText,
|
|
188
|
+
renewalDateStr: `on ${renewalDateText}${verificationText}`,
|
|
189
|
+
verificationText,
|
|
190
|
+
usCancelNote: "Cancel auto-renewal at any time in My Account. To avoid being charged you must cancel at least 24 hrs before your renewal date. You\u2019ll still have full access until the end of your current billing period, when the cancellation will take effect.",
|
|
191
|
+
cancelAnytime: "Cancel auto-renewal at any time."
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function freeTrialTerms(offer, ctx) {
|
|
195
|
+
const trialUnit = offer.subscription_options?.trial_period?.unit ?? ctx.unit;
|
|
196
|
+
const trialDuration = offer.subscription_options?.trial_period?.duration ?? 1;
|
|
197
|
+
const trialDateStr = `on ${getRenewalDateText(trialUnit, trialDuration)}${ctx.verificationText}`;
|
|
198
|
+
if (ctx.isUS) {
|
|
199
|
+
return `Your subscription renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${trialDateStr} when your free trial ends, until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal until cancelled. ${ctx.usCancelNote}${ctx.taxFootnote}`;
|
|
200
|
+
}
|
|
201
|
+
return `Your subscription renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${trialDateStr} when your free trial ends, until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime}${ctx.taxFootnote}`;
|
|
202
|
+
}
|
|
203
|
+
function nytTerms(ctx) {
|
|
204
|
+
const nytAccessText = "Your complimentary access to The New York Times is for one year only and does not auto-renew. We will send your instructions on how to redeem access to The New York Times by email.";
|
|
205
|
+
if (ctx.isUS) {
|
|
206
|
+
return `Your subscription to <em>The Economist</em> auto-renews ${ctx.periodText} until cancelled{{liSeparator}}Your subscription renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${ctx.renewalDateStr}{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal until cancelled. ${ctx.usCancelNote}${ctx.taxFootnote}{{pSeparator}}${nytAccessText}`;
|
|
207
|
+
}
|
|
208
|
+
if (ctx.isCA) {
|
|
209
|
+
return `Your subscription to <em>The Economist</em> auto-renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${ctx.periodText} ${ctx.renewalDateStr} until cancelled.{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime}${ctx.taxFootnote}{{pSeparator}}${nytAccessText}`;
|
|
210
|
+
}
|
|
211
|
+
return `Your subscription to <em>The Economist</em> auto-renews at ${ctx.renewalPriceText} ${ctx.periodText} ${ctx.renewalDateStr} until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime} ${nytAccessText}`;
|
|
212
|
+
}
|
|
213
|
+
function minTermContractTerms(offer, ctx) {
|
|
214
|
+
const duePrice = formatPrice(offer.price.purchase, offer.price.currency_code);
|
|
215
|
+
const { duration: mtDuration, unit: mtUnit } = getMinTermDurationUnits(
|
|
216
|
+
offer.subscription_options?.min_term_contract_length
|
|
217
|
+
);
|
|
218
|
+
const contractLength = mtDuration && mtUnit ? `${mtDuration} ${mtUnit}` : "";
|
|
219
|
+
const initialContractText = contractLength ? `your initial ${contractLength} contract` : "your initial contract";
|
|
220
|
+
const mtRenewalDateText = mtUnit && mtDuration ? getRenewalDateText(mtUnit, Number(mtDuration)) : ctx.renewalDateText;
|
|
221
|
+
return `You\u2019ll be charged ${duePrice} a month for the duration of ${initialContractText}{{liSeparator}}Your subscription auto-renews annually at ${ctx.renewalPriceText} a month on ${mtRenewalDateText} until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime}`;
|
|
222
|
+
}
|
|
223
|
+
function paidTrialTerms(offer, ctx) {
|
|
224
|
+
const trialUnit = offer.subscription_options?.trial_period?.unit ?? ctx.unit;
|
|
225
|
+
const trialDuration = offer.subscription_options?.trial_period?.duration ?? ctx.duration;
|
|
226
|
+
const trialDateText = getRenewalDateText(trialUnit, trialDuration);
|
|
227
|
+
if (ctx.isUS) {
|
|
228
|
+
return `Your subscription auto-renews ${ctx.periodText} until cancelled{{liSeparator}}Your subscription renews at ${ctx.renewalPriceText}${ctx.taxSuffix} on ${trialDateText} when your paid trial ends{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.usCancelNote}${ctx.taxFootnote}`;
|
|
229
|
+
}
|
|
230
|
+
return `Your subscription auto-renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${ctx.periodText} on ${trialDateText} when your paid trial ends, until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime}${ctx.taxFootnote}`;
|
|
231
|
+
}
|
|
232
|
+
function autoRenewalTerms(ctx) {
|
|
233
|
+
if (ctx.isUS) {
|
|
234
|
+
return `Your subscription auto-renews ${ctx.periodText} until cancelled{{liSeparator}}Your subscription renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${ctx.renewalDateStr}{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal until cancelled. ${ctx.usCancelNote}${ctx.taxFootnote}`;
|
|
235
|
+
}
|
|
236
|
+
return `Your subscription auto-renews at ${ctx.renewalPriceText}${ctx.taxSuffix} ${ctx.periodText} ${ctx.renewalDateStr} until cancelled{{liSeparator}}You\u2019ll be charged the rate in effect at each renewal. ${ctx.cancelAnytime}${ctx.taxFootnote}`;
|
|
237
|
+
}
|
|
238
|
+
function noAutoRenewalTerms(ctx) {
|
|
239
|
+
if (ctx.isUS || ctx.isCA) {
|
|
240
|
+
return `Your subscription does not renew automatically${ctx.taxFootnote}{{liSeparator}}`;
|
|
241
|
+
}
|
|
242
|
+
return `Your subscription does not renew automatically{{liSeparator}}`;
|
|
243
|
+
}
|
|
244
|
+
function generateApplePayModalTerms(offer, country) {
|
|
245
|
+
const ctx = buildTermsContext(offer, country);
|
|
246
|
+
const opts = offer.subscription_options;
|
|
247
|
+
if (opts?.free_trial) {
|
|
248
|
+
return freeTrialTerms(offer, ctx);
|
|
249
|
+
}
|
|
250
|
+
if (offer.product?.offer_type === "bogof_nyt") {
|
|
251
|
+
return nytTerms(ctx);
|
|
252
|
+
}
|
|
253
|
+
if (opts?.auto_renewal && (opts?.min_term_contract || false)) {
|
|
254
|
+
return minTermContractTerms(offer, ctx);
|
|
255
|
+
}
|
|
256
|
+
if (opts?.auto_renewal && !opts?.free_trial && opts?.variable_term) {
|
|
257
|
+
return paidTrialTerms(offer, ctx);
|
|
258
|
+
}
|
|
259
|
+
if (opts?.auto_renewal) {
|
|
260
|
+
return autoRenewalTerms(ctx);
|
|
261
|
+
}
|
|
262
|
+
return noAutoRenewalTerms(ctx);
|
|
263
|
+
}
|
|
264
|
+
function renderTermsHtml(termsText) {
|
|
265
|
+
const renewalTextLists = termsText.split("{{liSeparator}}");
|
|
266
|
+
const lastPart = renewalTextLists.pop() || "";
|
|
267
|
+
const renewalTextParagraphs = lastPart.split("{{pSeparator}}");
|
|
268
|
+
const ulHtml = `<ul class="apm__terms-bullets">${renewalTextLists.map((value) => `<li>${value}</li>`).join("")}</ul>`;
|
|
269
|
+
const paragraphHtml = renewalTextParagraphs.filter(Boolean).map((value) => `<p class="apm__terms-legal">${value}</p>`).join("");
|
|
270
|
+
return ulHtml + paragraphHtml;
|
|
271
|
+
}
|
|
272
|
+
function getApplePayProductName(offer) {
|
|
273
|
+
const unit = offer.subscription_options?.renewal_interval?.unit ?? "";
|
|
274
|
+
const duration = offer.subscription_options?.renewal_interval?.duration ?? 1;
|
|
275
|
+
let intervalLabel = "";
|
|
276
|
+
switch (unit) {
|
|
277
|
+
case "year":
|
|
278
|
+
intervalLabel = duration === 1 ? "Annual" : `${duration} year`;
|
|
279
|
+
break;
|
|
280
|
+
case "month":
|
|
281
|
+
intervalLabel = duration === 1 ? "Monthly" : `${duration} month`;
|
|
282
|
+
break;
|
|
283
|
+
case "week":
|
|
284
|
+
intervalLabel = duration === 12 ? "Quarterly" : duration === 4 ? "4-week" : duration === 1 ? "Weekly" : `${duration} week`;
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
intervalLabel = "";
|
|
288
|
+
}
|
|
289
|
+
return [intervalLabel, offer.display_name ?? ""].filter(Boolean).join(" ");
|
|
290
|
+
}
|
|
291
|
+
var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
292
|
+
static tag = "teg-apple-pay-modal";
|
|
293
|
+
static define() {
|
|
294
|
+
define(_ApplePayModal.tag, _ApplePayModal);
|
|
295
|
+
}
|
|
296
|
+
#offer = null;
|
|
297
|
+
#country = "";
|
|
298
|
+
#triggerElement = null;
|
|
299
|
+
#keyHandler = null;
|
|
300
|
+
set offer(value) {
|
|
301
|
+
this.#offer = value;
|
|
302
|
+
}
|
|
303
|
+
get country() {
|
|
304
|
+
return this.#country;
|
|
305
|
+
}
|
|
306
|
+
set country(value) {
|
|
307
|
+
this.#country = value;
|
|
308
|
+
}
|
|
309
|
+
connectedCallback() {
|
|
310
|
+
if (this.querySelector(".apm-overlay")) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
ensureApplePayLogoSymbol();
|
|
314
|
+
const template = document.createElement("template");
|
|
315
|
+
template.innerHTML = apple_pay_modal_template_default;
|
|
316
|
+
this.appendChild(template.content.cloneNode(true));
|
|
317
|
+
const offer = this.#offer;
|
|
318
|
+
const country = this.#country;
|
|
319
|
+
if (!offer) {
|
|
320
|
+
console.error("ApplePayModal: Missing or invalid offer data");
|
|
321
|
+
this.remove();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const offerNameElement = this.querySelector(
|
|
325
|
+
"#apple-pay-modal-offer-name"
|
|
326
|
+
);
|
|
327
|
+
const dueTodayElement = this.querySelector(
|
|
328
|
+
"#apple-pay-modal-due-today"
|
|
329
|
+
);
|
|
330
|
+
const termsElement = this.querySelector(
|
|
331
|
+
"#apple-pay-modal-terms"
|
|
332
|
+
);
|
|
333
|
+
if (offerNameElement) {
|
|
334
|
+
offerNameElement.textContent = getApplePayProductName(offer);
|
|
335
|
+
}
|
|
336
|
+
if (dueTodayElement) {
|
|
337
|
+
const taxSuffix = offer.price.purchase === 0 || offer.price.tax?.inclusive ? "" : "*";
|
|
338
|
+
const duePrice = formatPrice(
|
|
339
|
+
offer.price.purchase,
|
|
340
|
+
offer.price.currency_code
|
|
341
|
+
);
|
|
342
|
+
dueTodayElement.innerHTML = duePrice ? `Due today: <strong>${duePrice}${taxSuffix}</strong>` : "";
|
|
343
|
+
}
|
|
344
|
+
if (termsElement) {
|
|
345
|
+
termsElement.innerHTML = renderTermsHtml(
|
|
346
|
+
generateApplePayModalTerms(offer, country)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const isUS = country === "US";
|
|
350
|
+
const isLoggedIn = this.dataset.loggedIn === "true";
|
|
351
|
+
const isOfferNYT = offer.product?.offer_type === "bogof_nyt";
|
|
352
|
+
const isAutoRenewal = offer.subscription_options?.auto_renewal ?? true;
|
|
353
|
+
const checkboxTextElement = this.querySelector(
|
|
354
|
+
"#apple-pay-modal-checkbox-text"
|
|
355
|
+
);
|
|
356
|
+
if (checkboxTextElement) {
|
|
357
|
+
checkboxTextElement.innerHTML = getCheckboxTermsHtml(
|
|
358
|
+
isOfferNYT,
|
|
359
|
+
isLoggedIn,
|
|
360
|
+
isAutoRenewal
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const checkboxLabel = this.querySelector(
|
|
364
|
+
"#apple-pay-modal-checkbox-label"
|
|
365
|
+
);
|
|
366
|
+
const checkbox = this.querySelector(
|
|
367
|
+
"#apple-pay-modal-checkbox"
|
|
368
|
+
);
|
|
369
|
+
const cta = this.querySelector("#apple-pay-modal-cta");
|
|
370
|
+
const errorElement = this.querySelector(
|
|
371
|
+
"#apple-pay-modal-error"
|
|
372
|
+
);
|
|
373
|
+
const passiveTermsElement = this.querySelector(
|
|
374
|
+
"#apple-pay-modal-passive-terms"
|
|
375
|
+
);
|
|
376
|
+
if (!isUS) {
|
|
377
|
+
checkboxLabel?.remove();
|
|
378
|
+
errorElement?.remove();
|
|
379
|
+
if (passiveTermsElement) {
|
|
380
|
+
passiveTermsElement.innerHTML = getPassiveTermsHtml(
|
|
381
|
+
isOfferNYT,
|
|
382
|
+
isLoggedIn,
|
|
383
|
+
isAutoRenewal
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
passiveTermsElement?.remove();
|
|
388
|
+
checkbox?.addEventListener("change", () => {
|
|
389
|
+
if (checkbox.checked) {
|
|
390
|
+
errorElement?.classList.remove("apm__error--visible");
|
|
391
|
+
checkboxLabel?.classList.remove("apm__checkbox-label--error");
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
cta?.addEventListener("click", () => {
|
|
396
|
+
if (isUS && checkbox && !checkbox.checked) {
|
|
397
|
+
errorElement?.classList.add("apm__error--visible");
|
|
398
|
+
checkboxLabel?.classList.add("apm__checkbox-label--error");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.dispatchEvent(
|
|
402
|
+
new CustomEvent("apple-pay-confirmed", {
|
|
403
|
+
bubbles: true,
|
|
404
|
+
composed: true,
|
|
405
|
+
detail: { skuId: offer.sku_id, country }
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
this.close();
|
|
409
|
+
});
|
|
410
|
+
this.querySelector("#apple-pay-modal-close")?.addEventListener(
|
|
411
|
+
"click",
|
|
412
|
+
() => this.close()
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
open() {
|
|
416
|
+
this.#triggerElement = document.activeElement;
|
|
417
|
+
document.body.appendChild(this);
|
|
418
|
+
this.#keyHandler = (event) => {
|
|
419
|
+
if (event.key === "Escape") {
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
event.stopPropagation();
|
|
422
|
+
event.stopImmediatePropagation();
|
|
423
|
+
this.close();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
document.addEventListener("keydown", this.#keyHandler, { capture: true });
|
|
427
|
+
this.querySelector("#apple-pay-modal")?.focus();
|
|
428
|
+
}
|
|
429
|
+
close() {
|
|
430
|
+
if (this.#keyHandler) {
|
|
431
|
+
document.removeEventListener("keydown", this.#keyHandler, {
|
|
432
|
+
capture: true
|
|
433
|
+
});
|
|
434
|
+
this.#keyHandler = null;
|
|
435
|
+
}
|
|
436
|
+
this.remove();
|
|
437
|
+
this.#triggerElement?.focus();
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// src/templates/apple-pay-button.template.html
|
|
442
|
+
var apple_pay_button_template_default = '<style>\n /* Use CSS variables for layout so parent walls can easily override placement and sizing */\n teg-apple-pay-button {\n display: block;\n margin: var(--apple-pay-margin, 0);\n width: 100%;\n height: 100%;\n }\n\n teg-apple-pay-button .apple-pay-container {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n height: 100%;\n }\n\n teg-apple-pay-button .apple-pay-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.375rem;\n width: 100%;\n min-width: 0;\n height: 100%;\n min-height: var(--apple-pay-height, 2.75rem);\n background-color: #000;\n color: #fff;\n border: none;\n border-radius: var(--apple-pay-border-radius, 1.375rem);\n cursor: pointer;\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: 1.0625rem;\n font-weight: 500;\n }\n\n teg-apple-pay-button .apple-pay-btn svg {\n flex-shrink: 0;\n display: block;\n }\n</style>\n\n<div class="apple-pay-container" aria-label="Express checkout with Apple Pay">\n <button\n class="apple-pay-btn"\n type="button"\n aria-label="Pay with Apple Pay"\n data-analytics="paywall:apple-pay-checkout"\n >\n <svg width="51" height="22" aria-label="Apple Pay" role="img">\n <use href="#apple-pay-logo" />\n </svg>\n </button>\n</div>\n';
|
|
443
|
+
|
|
444
|
+
// src/clients/payment-checkout/config.ts
|
|
445
|
+
var runtimeConfig = null;
|
|
446
|
+
var getApplePayConfig = () => runtimeConfig;
|
|
447
|
+
|
|
448
|
+
// src/clients/payment-checkout/detects.ts
|
|
449
|
+
var isAppleDeviceOrSafari = () => {
|
|
450
|
+
const isIOSOrMacTouchDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
|
|
451
|
+
const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
|
|
452
|
+
return isIOSOrMacTouchDevice || isSafariBrowser;
|
|
453
|
+
};
|
|
454
|
+
var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
|
|
455
|
+
var isApplePayAvailable = async () => {
|
|
456
|
+
console.log("[web-apple-pay] isApplePayAvailable: checking availability");
|
|
457
|
+
if (bypassApplePayChecks()) {
|
|
458
|
+
console.log("[web-apple-pay] isApplePayAvailable: bypass active \u2192 true");
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
if (!isAppleDeviceOrSafari()) {
|
|
462
|
+
console.log("[web-apple-pay] isApplePayAvailable: not Apple device or Safari \u2192 false");
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const applePaySession = window.ApplePaySession;
|
|
466
|
+
if (!applePaySession) {
|
|
467
|
+
console.log("[web-apple-pay] isApplePayAvailable: ApplePaySession not found on window \u2192 false");
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
if (typeof applePaySession.canMakePayments !== "function") {
|
|
472
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePayments is not a function \u2192 false");
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (!applePaySession.canMakePayments()) {
|
|
476
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePayments() returned false \u2192 false");
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const merchantId = getApplePayConfig()?.merchantId;
|
|
480
|
+
if (!merchantId) {
|
|
481
|
+
console.warn(
|
|
482
|
+
"[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
|
|
483
|
+
);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
console.log("[web-apple-pay] isApplePayAvailable : calling canMakePaymentsWithActiveCard", { merchantId });
|
|
487
|
+
const result = await applePaySession.canMakePaymentsWithActiveCard(merchantId);
|
|
488
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePaymentsWithActiveCard \u2192", result);
|
|
489
|
+
return result;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error("Error checking Apple Pay active card status:", error);
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/clients/payment-checkout/types.ts
|
|
497
|
+
var ApiError = class extends Error {
|
|
498
|
+
status;
|
|
499
|
+
body;
|
|
500
|
+
constructor(status, body) {
|
|
501
|
+
super(`API request failed with status ${status}`);
|
|
502
|
+
this.name = "ApiError";
|
|
503
|
+
this.status = status;
|
|
504
|
+
this.body = body;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/clients/payment-checkout/index.ts
|
|
509
|
+
var getWalletApiBaseUrl = () => {
|
|
510
|
+
const config = getApplePayConfig();
|
|
511
|
+
if (!config?.walletApiBaseUrl) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return config.walletApiBaseUrl;
|
|
517
|
+
};
|
|
518
|
+
var apiFetch = async (path, options = {}) => {
|
|
519
|
+
const baseUrl = getWalletApiBaseUrl();
|
|
520
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
521
|
+
const hasBody = options.body !== void 0 && options.body !== null;
|
|
522
|
+
const headers = new Headers(options.headers);
|
|
523
|
+
if (!headers.has("Accept")) {
|
|
524
|
+
headers.set("Accept", "application/json");
|
|
525
|
+
}
|
|
526
|
+
if (!headers.has("Content-Type")) {
|
|
527
|
+
if (hasBody || method !== "GET" && method !== "HEAD") {
|
|
528
|
+
headers.set("Content-Type", "application/json");
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
532
|
+
...options,
|
|
533
|
+
headers
|
|
534
|
+
});
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
const contentType = response.headers.get("content-type") || "";
|
|
537
|
+
if (contentType.includes("application/json")) {
|
|
538
|
+
const errorBody = await response.json();
|
|
539
|
+
throw new ApiError(response.status, errorBody);
|
|
540
|
+
}
|
|
541
|
+
const errorText = await response.text();
|
|
542
|
+
throw new Error(
|
|
543
|
+
`API request failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return response.json();
|
|
547
|
+
};
|
|
548
|
+
var getPaymentOptions = (data) => {
|
|
549
|
+
const { sku, countryCode, currencyCode, checkoutUrl } = data;
|
|
550
|
+
const params = new URLSearchParams({ checkoutUrl });
|
|
551
|
+
return apiFetch(
|
|
552
|
+
`/v1/wallet/payment-options/sku/${sku}/${countryCode}/${currencyCode}?${params}`,
|
|
553
|
+
{ method: "GET" }
|
|
554
|
+
);
|
|
555
|
+
};
|
|
556
|
+
var validateAppleMerchant = (data) => {
|
|
557
|
+
return apiFetch("/v1/wallet/validate-merchant", {
|
|
558
|
+
method: "POST",
|
|
559
|
+
body: JSON.stringify(data)
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
var performPurchase = (data) => {
|
|
563
|
+
return apiFetch("/v1/wallet/purchase", {
|
|
564
|
+
method: "POST",
|
|
565
|
+
body: JSON.stringify(data),
|
|
566
|
+
credentials: "include"
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/clients/payment-checkout/session.ts
|
|
571
|
+
var getExpressCheckoutSession = () => {
|
|
572
|
+
return apiFetch("/v1/wallet/session", {
|
|
573
|
+
method: "GET",
|
|
574
|
+
credentials: "include"
|
|
575
|
+
});
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// src/utils/expressCheckoutRedirect.ts
|
|
579
|
+
var captureOriginUrl = () => window.location.href;
|
|
580
|
+
var buildQueryString = () => {
|
|
581
|
+
const countryCode = new URLSearchParams(window.location.search).get(
|
|
582
|
+
"country"
|
|
583
|
+
);
|
|
584
|
+
return countryCode ? `&country=${encodeURIComponent(countryCode)}` : "";
|
|
585
|
+
};
|
|
586
|
+
var decodeSku = (sku) => {
|
|
587
|
+
try {
|
|
588
|
+
return atob(sku.substring(sku.indexOf("-") + 1));
|
|
589
|
+
} catch {
|
|
590
|
+
return sku;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
var redirectToCheckoutComplete = (params) => {
|
|
594
|
+
const url = new URL("/checkout/complete", window.location.origin);
|
|
595
|
+
url.searchParams.set("basketId", params.basketId);
|
|
596
|
+
if (params.queryString) {
|
|
597
|
+
const qs = params.queryString.startsWith("?") ? params.queryString.slice(1) : params.queryString;
|
|
598
|
+
new URLSearchParams(qs).forEach((v, k) => url.searchParams.set(k, v));
|
|
599
|
+
}
|
|
600
|
+
url.searchParams.set("lmo_offer", params.skuId);
|
|
601
|
+
url.searchParams.set("status", params.status);
|
|
602
|
+
url.searchParams.set("redirectToCheckout", "true");
|
|
603
|
+
url.searchParams.set("returnUrl", params.returnUrl);
|
|
604
|
+
return url.pathname + url.search;
|
|
605
|
+
};
|
|
606
|
+
var redirectToCheckoutFallback = (params) => {
|
|
607
|
+
const url = new URL(window.location.href);
|
|
608
|
+
url.pathname = "/checkout";
|
|
609
|
+
url.searchParams.set("lmo_offer", params.skuId);
|
|
610
|
+
if (params.inlineError)
|
|
611
|
+
url.searchParams.set("inlineError", params.inlineError);
|
|
612
|
+
if (params.email) url.searchParams.set("email", params.email);
|
|
613
|
+
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
614
|
+
url.searchParams.set("returnUrl", params.returnUrl);
|
|
615
|
+
return url.pathname + url.search;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/utils/recaptcha.ts
|
|
619
|
+
var loadRecaptchaScript = (siteKey) => {
|
|
620
|
+
return new Promise((resolve, reject) => {
|
|
621
|
+
if (typeof grecaptcha !== "undefined") return resolve();
|
|
622
|
+
const existingScript = document.querySelector(
|
|
623
|
+
'script[src*="recaptcha/api.js"]'
|
|
624
|
+
);
|
|
625
|
+
if (existingScript) {
|
|
626
|
+
existingScript.addEventListener("load", () => resolve());
|
|
627
|
+
existingScript.addEventListener(
|
|
628
|
+
"error",
|
|
629
|
+
() => reject(new Error("Failed to load existing reCAPTCHA script"))
|
|
630
|
+
);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const script = document.createElement("script");
|
|
634
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
|
|
635
|
+
script.async = true;
|
|
636
|
+
script.defer = true;
|
|
637
|
+
script.onload = () => resolve();
|
|
638
|
+
script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
|
|
639
|
+
document.head.appendChild(script);
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
var getRecaptchaToken = async (action) => {
|
|
643
|
+
const siteKey = getApplePayConfig()?.recaptchaSiteKey;
|
|
644
|
+
if (!siteKey) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"[web-apple-pay] Missing recaptchaSiteKey. Call configureApplePay({ recaptchaSiteKey, ... }) before initiating Apple Pay."
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
await loadRecaptchaScript(siteKey);
|
|
650
|
+
return new Promise((resolve, reject) => {
|
|
651
|
+
grecaptcha.ready(() => {
|
|
652
|
+
grecaptcha.execute(siteKey, { action }).then(resolve).catch(reject);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
};
|
|
656
|
+
var getPurchaseRecaptchaTokens = async () => {
|
|
657
|
+
const [checkEligibility, newBasket, registerUser] = await Promise.all([
|
|
658
|
+
getRecaptchaToken("checkEligibility"),
|
|
659
|
+
getRecaptchaToken("newBasket"),
|
|
660
|
+
getRecaptchaToken("registerUser")
|
|
661
|
+
]);
|
|
662
|
+
return { checkEligibility, newBasket, registerUser };
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/utils/expressCheckoutTracking.ts
|
|
666
|
+
var EXPRESS_CHECKOUT_EVENTS = {
|
|
667
|
+
/** Wallet button displayed */
|
|
668
|
+
IMPRESSION: "express_wallet_impression",
|
|
669
|
+
/** Wallet button clicked */
|
|
670
|
+
BUTTON_CLICK: "express_wallet_click",
|
|
671
|
+
/** Apple Pay sheet opened */
|
|
672
|
+
SHEET_OPEN: "wallet_payment_sheet_open",
|
|
673
|
+
/** Payment authorised by the user in the Apple Pay sheet */
|
|
674
|
+
AUTHORISED: "wallet_payment_authorised",
|
|
675
|
+
/** Purchase completed — BFF returned success */
|
|
676
|
+
SUCCESS: "wallet_payment_success",
|
|
677
|
+
/** Payment failed — BFF returned error */
|
|
678
|
+
FAILURE: "wallet_payment_failure",
|
|
679
|
+
/** Redirect to standard checkout (any fallback path) */
|
|
680
|
+
FALLBACK_CHECKOUT: "fallback_checkout"
|
|
681
|
+
};
|
|
682
|
+
var getDevice = () => {
|
|
683
|
+
const ua = navigator.userAgent;
|
|
684
|
+
if (/tablet|ipad/i.test(ua)) return "tablet";
|
|
685
|
+
if (/mobile|iphone|android/i.test(ua)) return "mobile";
|
|
686
|
+
return "desktop";
|
|
687
|
+
};
|
|
688
|
+
var emitTedlEvent = (event) => {
|
|
689
|
+
window.tedl = window.tedl ?? {};
|
|
690
|
+
window.tedl.events = window.tedl.events ?? [];
|
|
691
|
+
window.tedl.events.push(event);
|
|
692
|
+
};
|
|
693
|
+
var track = (action, extra) => {
|
|
694
|
+
emitTedlEvent({
|
|
695
|
+
name: "user.journey",
|
|
696
|
+
type: "checkout",
|
|
697
|
+
action,
|
|
698
|
+
info: "applepay",
|
|
699
|
+
...extra
|
|
700
|
+
});
|
|
701
|
+
};
|
|
702
|
+
var trackExpressWalletImpression = (skuId, { entryPoint, country }) => {
|
|
703
|
+
track(EXPRESS_CHECKOUT_EVENTS.IMPRESSION, {
|
|
704
|
+
wallet_type: "apple_pay",
|
|
705
|
+
entry_point: entryPoint,
|
|
706
|
+
sku_id: skuId,
|
|
707
|
+
country,
|
|
708
|
+
device: getDevice()
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
var trackExpressWalletClick = (skuId) => {
|
|
712
|
+
track(EXPRESS_CHECKOUT_EVENTS.BUTTON_CLICK, { sku_id: skuId });
|
|
713
|
+
};
|
|
714
|
+
var trackWalletPaymentSheetOpen = (skuId) => {
|
|
715
|
+
track(EXPRESS_CHECKOUT_EVENTS.SHEET_OPEN, { sku_id: skuId });
|
|
716
|
+
};
|
|
717
|
+
var trackWalletPaymentAuthorised = (skuId) => {
|
|
718
|
+
track(EXPRESS_CHECKOUT_EVENTS.AUTHORISED, { sku_id: skuId });
|
|
719
|
+
};
|
|
720
|
+
var trackWalletPaymentSuccess = (skuId, basketId) => {
|
|
721
|
+
track(EXPRESS_CHECKOUT_EVENTS.SUCCESS, {
|
|
722
|
+
sku_id: skuId,
|
|
723
|
+
basket_id: basketId
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
var trackWalletPaymentFailure = (skuId, reason) => {
|
|
727
|
+
track(EXPRESS_CHECKOUT_EVENTS.FAILURE, {
|
|
728
|
+
sku_id: skuId,
|
|
729
|
+
failure_reason: reason
|
|
730
|
+
});
|
|
731
|
+
};
|
|
732
|
+
var trackFallbackCheckout = (skuId) => {
|
|
733
|
+
track(EXPRESS_CHECKOUT_EVENTS.FALLBACK_CHECKOUT, { sku_id: skuId });
|
|
734
|
+
};
|
|
735
|
+
var trackValidationFieldError = (field, errorType) => {
|
|
736
|
+
track(`validation_${field}_${errorType}`, {});
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/clients/payment-checkout/messages.ts
|
|
740
|
+
var MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
741
|
+
var MSG_EMAIL_WRONG_FORMAT = "That email doesn\u2019t look right. Please enter a valid email address.";
|
|
742
|
+
var MSG_EMAIL_TOO_LONG = "This field must be 80 characters or less.";
|
|
743
|
+
var MSG_UNSUPPORTED_BILLING_COUNTRY = "Billing country does not match offer price zone";
|
|
744
|
+
var MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment and billing details and try again or contact us.";
|
|
745
|
+
var MSG_RECAPTCHA_VALIDATION_FAILED = "Security check failed. Please try again.";
|
|
746
|
+
var MSG_EMAIL_COLLISION = "This email address is already linked to an account. Please log in, or try a different email address.";
|
|
747
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
748
|
+
|
|
749
|
+
// src/clients/payment-checkout/apple-session.ts
|
|
750
|
+
function startApplePaySession(config) {
|
|
751
|
+
console.log("[web-apple-pay] startApplePaySession", {
|
|
752
|
+
skuId: config.skuId,
|
|
753
|
+
countryCode: config.countryCode,
|
|
754
|
+
currencyCode: config.currencyCode,
|
|
755
|
+
totalAmount: config.totalAmount,
|
|
756
|
+
userLoggedIn: config.userLoggedIn
|
|
757
|
+
});
|
|
758
|
+
const paymentRequest = {
|
|
759
|
+
countryCode: config.countryCode,
|
|
760
|
+
currencyCode: config.currencyCode,
|
|
761
|
+
merchantCapabilities: ["supports3DS"],
|
|
762
|
+
supportedNetworks: config.supportedNetworks,
|
|
763
|
+
total: {
|
|
764
|
+
label: config.totalLabel,
|
|
765
|
+
amount: config.totalAmount
|
|
766
|
+
},
|
|
767
|
+
requiredBillingContactFields: ["postalAddress", "name"],
|
|
768
|
+
// Only request email from Apple Pay sheet if user is NOT logged in
|
|
769
|
+
...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
|
|
770
|
+
};
|
|
771
|
+
const session = new ApplePaySession(3, paymentRequest);
|
|
772
|
+
session.onvalidatemerchant = async (event) => {
|
|
773
|
+
console.log("[web-apple-pay] onvalidatemerchant: received", { validationURL: event.validationURL });
|
|
774
|
+
try {
|
|
775
|
+
const validationHost = new URL(event.validationURL).host;
|
|
776
|
+
if (!validationHost.endsWith("apple.com")) {
|
|
777
|
+
console.warn("[web-apple-pay] onvalidatemerchant: invalid host, aborting", { validationHost });
|
|
778
|
+
trackFallbackCheckout(config.skuId);
|
|
779
|
+
session.abort();
|
|
780
|
+
window.location.href = redirectToCheckoutFallback({
|
|
781
|
+
skuId: config.skuId,
|
|
782
|
+
returnUrl: config.returnUrl,
|
|
783
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
784
|
+
});
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const recaptchaToken = await getRecaptchaToken("verifyMerchant");
|
|
788
|
+
console.log("[web-apple-pay] onvalidatemerchant: calling validateAppleMerchant");
|
|
789
|
+
const response = await validateAppleMerchant({
|
|
790
|
+
validationURL: event.validationURL,
|
|
791
|
+
recaptchaToken
|
|
792
|
+
});
|
|
793
|
+
if (!response?.merchantSession) {
|
|
794
|
+
console.warn("[web-apple-pay] onvalidatemerchant: no merchantSession in response, aborting");
|
|
795
|
+
trackFallbackCheckout(config.skuId);
|
|
796
|
+
session.abort();
|
|
797
|
+
window.location.href = redirectToCheckoutFallback({
|
|
798
|
+
skuId: config.skuId,
|
|
799
|
+
returnUrl: config.returnUrl,
|
|
800
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
console.log("[web-apple-pay] onvalidatemerchant: merchant validated successfully");
|
|
805
|
+
session.completeMerchantValidation(response.merchantSession);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
console.error("[web-apple-pay] onvalidatemerchant: error during validation", err);
|
|
808
|
+
trackFallbackCheckout(config.skuId);
|
|
809
|
+
session.abort();
|
|
810
|
+
window.location.href = redirectToCheckoutFallback({
|
|
811
|
+
skuId: config.skuId,
|
|
812
|
+
returnUrl: config.returnUrl,
|
|
813
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
session.onpaymentauthorized = (event) => {
|
|
818
|
+
console.log("[web-apple-pay] onpaymentauthorized: payment authorized by user", { skuId: config.skuId });
|
|
819
|
+
trackWalletPaymentAuthorised(config.skuId);
|
|
820
|
+
config.onPaymentAuthorized(event, session);
|
|
821
|
+
};
|
|
822
|
+
session.oncancel = () => {
|
|
823
|
+
console.log("[web-apple-pay] oncancel: payment sheet cancelled by user", { skuId: config.skuId });
|
|
824
|
+
config.onCancel?.();
|
|
825
|
+
};
|
|
826
|
+
console.log("[web-apple-pay] startApplePaySession: beginning session");
|
|
827
|
+
trackWalletPaymentSheetOpen(config.skuId);
|
|
828
|
+
session.begin();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/clients/payment-checkout/contact-validation.ts
|
|
832
|
+
var pushFieldError = (errors, billingOrShippingContact = "billing", field, errorType, errorText) => {
|
|
833
|
+
const errorCode = billingOrShippingContact === "billing" ? "billingContactInvalid" : "shippingContactInvalid";
|
|
834
|
+
errors.push(new ApplePayError(errorCode, field, errorText));
|
|
835
|
+
trackValidationFieldError(field, errorType);
|
|
836
|
+
return "";
|
|
837
|
+
};
|
|
838
|
+
var pushRequiredFieldError = (errors, billingOrShippingContact = "billing", field) => {
|
|
839
|
+
return pushFieldError(
|
|
840
|
+
errors,
|
|
841
|
+
billingOrShippingContact,
|
|
842
|
+
field,
|
|
843
|
+
"required",
|
|
844
|
+
MSG_MISSING_REQUIRED_FIELD
|
|
845
|
+
);
|
|
846
|
+
};
|
|
847
|
+
var ContactValidationError = class extends Error {
|
|
848
|
+
constructor(errors) {
|
|
849
|
+
super(MSG_CONTACT_VALIDATION_INTERNAL);
|
|
850
|
+
this.errors = errors;
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// src/clients/payment-checkout/billing-contact-details.ts
|
|
855
|
+
var countriesWithoutCityField = ["KY", "CK", "NR", "TC"];
|
|
856
|
+
var countriesRequiringAllFields = ["US", "CA", "GB"];
|
|
857
|
+
var resolveCountryConfig = (country) => {
|
|
858
|
+
const hasCityField = !countriesWithoutCityField.includes(country);
|
|
859
|
+
const requiresStateAndPostcode = countriesRequiringAllFields.includes(country);
|
|
860
|
+
return {
|
|
861
|
+
hasCityField,
|
|
862
|
+
requiresStateAndPostcode,
|
|
863
|
+
// if there is no city field, we require state and use it in place of city
|
|
864
|
+
requiresStateField: !hasCityField || requiresStateAndPostcode
|
|
865
|
+
};
|
|
866
|
+
};
|
|
867
|
+
var validateAddressFields = (errors, contact, { hasCityField, requiresStateAndPostcode, requiresStateField }) => {
|
|
868
|
+
const locality = !hasCityField ? contact.administrativeArea : contact.locality;
|
|
869
|
+
if (!contact.addressLines?.at(0)?.trim()) {
|
|
870
|
+
pushRequiredFieldError(errors, "billing", "addressLines");
|
|
871
|
+
}
|
|
872
|
+
if (!locality?.trim() && hasCityField) {
|
|
873
|
+
pushRequiredFieldError(errors, "billing", "locality");
|
|
874
|
+
}
|
|
875
|
+
if (!contact.administrativeArea?.trim() && requiresStateField) {
|
|
876
|
+
pushRequiredFieldError(errors, "billing", "administrativeArea");
|
|
877
|
+
}
|
|
878
|
+
if (!contact.postalCode?.trim() && requiresStateAndPostcode) {
|
|
879
|
+
pushRequiredFieldError(errors, "billing", "postalCode");
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
var validateNameFields = (errors, contact) => {
|
|
883
|
+
const firstName = contact.givenName?.trim() || "";
|
|
884
|
+
const lastName = contact.familyName?.trim() || "";
|
|
885
|
+
if (!firstName || !lastName) {
|
|
886
|
+
pushRequiredFieldError(errors, "billing", "name");
|
|
887
|
+
}
|
|
888
|
+
return { firstName, lastName };
|
|
889
|
+
};
|
|
890
|
+
var validateCountrySupport = (errors, country, supportedBillingCountries) => {
|
|
891
|
+
if (!supportedBillingCountries.includes(country)) {
|
|
892
|
+
pushFieldError(
|
|
893
|
+
errors,
|
|
894
|
+
"billing",
|
|
895
|
+
"countryCode",
|
|
896
|
+
"not-supported",
|
|
897
|
+
MSG_UNSUPPORTED_BILLING_COUNTRY
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
var billingContactDetails = (contact, supportedBillingCountries) => {
|
|
902
|
+
const errors = [];
|
|
903
|
+
const country = contact.countryCode?.toUpperCase() || pushRequiredFieldError(errors, "billing", "country");
|
|
904
|
+
const config = resolveCountryConfig(country);
|
|
905
|
+
validateAddressFields(errors, contact, config);
|
|
906
|
+
const { firstName, lastName } = validateNameFields(errors, contact);
|
|
907
|
+
validateCountrySupport(errors, country, supportedBillingCountries);
|
|
908
|
+
if (errors.length > 0) {
|
|
909
|
+
throw new ContactValidationError(errors);
|
|
910
|
+
}
|
|
911
|
+
return { firstName, lastName };
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// src/clients/payment-checkout/shipping-contact-details.ts
|
|
915
|
+
var EMAIL_FORMAT_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
916
|
+
var shippingContactDetails = (shippingContact) => {
|
|
917
|
+
const errors = [];
|
|
918
|
+
let emailAddress = "";
|
|
919
|
+
if (shippingContact?.emailAddress === void 0 || shippingContact?.emailAddress === "") {
|
|
920
|
+
pushRequiredFieldError(errors, "shipping", "emailAddress");
|
|
921
|
+
} else {
|
|
922
|
+
emailAddress = shippingContact.emailAddress.trim();
|
|
923
|
+
if (emailAddress.length > 80) {
|
|
924
|
+
pushFieldError(
|
|
925
|
+
errors,
|
|
926
|
+
"shipping",
|
|
927
|
+
"emailAddress",
|
|
928
|
+
"format",
|
|
929
|
+
MSG_EMAIL_TOO_LONG
|
|
930
|
+
);
|
|
931
|
+
} else if (!EMAIL_FORMAT_REGEX.test(emailAddress)) {
|
|
932
|
+
pushFieldError(
|
|
933
|
+
errors,
|
|
934
|
+
"shipping",
|
|
935
|
+
"emailAddress",
|
|
936
|
+
"format",
|
|
937
|
+
MSG_EMAIL_WRONG_FORMAT
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (errors.length > 0) {
|
|
942
|
+
throw new ContactValidationError(errors);
|
|
943
|
+
}
|
|
944
|
+
return { emailAddress };
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// src/clients/payment-checkout/payment-handler.ts
|
|
948
|
+
var MSG_ELIGIBILITY = "Something went wrong, please try again";
|
|
949
|
+
var MSG_REGISTRATION = "We couldn't create your account. Please enter your email or try a different login method.";
|
|
950
|
+
var failApplePaySession = (session, errors = []) => {
|
|
951
|
+
session.completePayment({
|
|
952
|
+
status: ApplePaySession.STATUS_FAILURE,
|
|
953
|
+
errors
|
|
954
|
+
});
|
|
955
|
+
};
|
|
956
|
+
function parseWalletApiError(err) {
|
|
957
|
+
const body = err instanceof ApiError ? err.body : err;
|
|
958
|
+
if (body && typeof body === "object" && "errorType" in body && "message" in body) {
|
|
959
|
+
return body;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
964
|
+
billingContactDetails(
|
|
965
|
+
event.payment.billingContact,
|
|
966
|
+
supportedBillingCountries
|
|
967
|
+
);
|
|
968
|
+
if (!sessionData.loggedIn) {
|
|
969
|
+
return shippingContactDetails(event.payment.shippingContact).emailAddress;
|
|
970
|
+
}
|
|
971
|
+
return void 0;
|
|
972
|
+
}
|
|
973
|
+
function redirectFallback(skuId, returnUrl, options) {
|
|
974
|
+
trackFallbackCheckout(skuId);
|
|
975
|
+
window.location.href = redirectToCheckoutFallback({
|
|
976
|
+
skuId,
|
|
977
|
+
returnUrl,
|
|
978
|
+
...options
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
var ERROR_HANDLERS = {
|
|
982
|
+
EMAIL_COLLISION: ({ skuId, returnUrl, validatedEmail }) => redirectFallback(skuId, returnUrl, {
|
|
983
|
+
emailError: MSG_EMAIL_COLLISION,
|
|
984
|
+
email: validatedEmail
|
|
985
|
+
}),
|
|
986
|
+
PAYMENT_DECLINED: ({ skuId, returnUrl }) => (
|
|
987
|
+
// Matches sma-checkout catch-all: always redirect with generic inlineError.
|
|
988
|
+
redirectFallback(skuId, returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR })
|
|
989
|
+
),
|
|
990
|
+
ELIGIBILITY_REJECTED: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_ELIGIBILITY }),
|
|
991
|
+
REGISTRATION_FAILED: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_REGISTRATION }),
|
|
992
|
+
VALIDATION_ERROR: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_RECAPTCHA_VALIDATION_FAILED })
|
|
993
|
+
};
|
|
994
|
+
function handleStructuredError(ctx) {
|
|
995
|
+
const handler = ERROR_HANDLERS[ctx.walletError.errorType];
|
|
996
|
+
if (handler) {
|
|
997
|
+
handler(ctx);
|
|
998
|
+
} else {
|
|
999
|
+
redirectFallback(ctx.skuId, ctx.returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
1003
|
+
const { skuId, returnUrl, queryString } = config;
|
|
1004
|
+
let validatedEmail;
|
|
1005
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: starting", { skuId, userLoggedIn: sessionData.loggedIn });
|
|
1006
|
+
try {
|
|
1007
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: step 1 \u2014 validating contacts");
|
|
1008
|
+
validatedEmail = validateContacts(
|
|
1009
|
+
event,
|
|
1010
|
+
sessionData,
|
|
1011
|
+
config.supportedBillingCountries
|
|
1012
|
+
);
|
|
1013
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: contacts valid", { validatedEmail });
|
|
1014
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: step 2 \u2014 calling performPurchase", { skuId });
|
|
1015
|
+
const result = await performPurchase({
|
|
1016
|
+
applePayToken: event.payment.token,
|
|
1017
|
+
skuId: config.skuId,
|
|
1018
|
+
billingContact: event.payment.billingContact,
|
|
1019
|
+
...event.payment.shippingContact && {
|
|
1020
|
+
shippingContact: event.payment.shippingContact
|
|
1021
|
+
},
|
|
1022
|
+
recaptchaTokens: config.recaptchaTokens
|
|
1023
|
+
});
|
|
1024
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: performPurchase succeeded", { basketId: result.basketId });
|
|
1025
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: step 3 \u2014 completing payment and redirecting");
|
|
1026
|
+
trackWalletPaymentSuccess(skuId, result.basketId);
|
|
1027
|
+
session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
|
|
1028
|
+
const userStatus = sessionData.loggedIn ? "lex" : "nex";
|
|
1029
|
+
window.location.href = redirectToCheckoutComplete({
|
|
1030
|
+
basketId: result.basketId,
|
|
1031
|
+
status: userStatus,
|
|
1032
|
+
skuId: decodeSku(skuId),
|
|
1033
|
+
returnUrl,
|
|
1034
|
+
queryString
|
|
1035
|
+
});
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
if (err instanceof ContactValidationError) {
|
|
1038
|
+
console.warn("[web-apple-pay] handlePaymentAuthorized: contact validation failed", err.errors);
|
|
1039
|
+
failApplePaySession(session, err.errors);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
|
|
1043
|
+
const walletError = parseWalletApiError(err);
|
|
1044
|
+
console.error("[web-apple-pay] handlePaymentAuthorized: BFF/network error", {
|
|
1045
|
+
skuId,
|
|
1046
|
+
errorType: walletError?.errorType ?? "UNKNOWN",
|
|
1047
|
+
message: walletError?.message
|
|
1048
|
+
});
|
|
1049
|
+
trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
|
|
1050
|
+
failApplePaySession(session);
|
|
1051
|
+
if (!walletError) {
|
|
1052
|
+
console.warn("[web-apple-pay] handlePaymentAuthorized: unstructured error, using generic fallback");
|
|
1053
|
+
redirectFallback(skuId, returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
console.log("[web-apple-pay] handlePaymentAuthorized: dispatching structured error handler", { errorType: walletError.errorType });
|
|
1057
|
+
handleStructuredError({ skuId, returnUrl, validatedEmail, walletError });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/data/price-zones.json
|
|
1062
|
+
var price_zones_default = {
|
|
1063
|
+
"AS-AS": [
|
|
1064
|
+
"BD",
|
|
1065
|
+
"BT",
|
|
1066
|
+
"CK",
|
|
1067
|
+
"FJ",
|
|
1068
|
+
"ID",
|
|
1069
|
+
"KH",
|
|
1070
|
+
"KI",
|
|
1071
|
+
"LA",
|
|
1072
|
+
"LK",
|
|
1073
|
+
"MH",
|
|
1074
|
+
"MM",
|
|
1075
|
+
"MN",
|
|
1076
|
+
"MV",
|
|
1077
|
+
"NC",
|
|
1078
|
+
"NF",
|
|
1079
|
+
"NP",
|
|
1080
|
+
"NR",
|
|
1081
|
+
"PF",
|
|
1082
|
+
"PG",
|
|
1083
|
+
"PK",
|
|
1084
|
+
"PW",
|
|
1085
|
+
"SB",
|
|
1086
|
+
"TH",
|
|
1087
|
+
"TL",
|
|
1088
|
+
"TO",
|
|
1089
|
+
"TV",
|
|
1090
|
+
"VN",
|
|
1091
|
+
"VU",
|
|
1092
|
+
"WS"
|
|
1093
|
+
],
|
|
1094
|
+
"AS-AU": ["AU"],
|
|
1095
|
+
"AS-CN": ["CN"],
|
|
1096
|
+
"AS-HK": ["HK", "MO"],
|
|
1097
|
+
"AS-IN": ["IN"],
|
|
1098
|
+
"AS-JP": ["JP"],
|
|
1099
|
+
"AS-KR": ["KR"],
|
|
1100
|
+
"AS-MY": ["MY"],
|
|
1101
|
+
"AS-NZ": ["NZ"],
|
|
1102
|
+
"AS-PH": ["PH"],
|
|
1103
|
+
"AS-SG": ["BN", "SG"],
|
|
1104
|
+
"AS-TW": ["TW"],
|
|
1105
|
+
"CE-AF": [
|
|
1106
|
+
"DZ",
|
|
1107
|
+
"AO",
|
|
1108
|
+
"BJ",
|
|
1109
|
+
"BW",
|
|
1110
|
+
"BF",
|
|
1111
|
+
"BI",
|
|
1112
|
+
"CM",
|
|
1113
|
+
"CV",
|
|
1114
|
+
"CF",
|
|
1115
|
+
"TD",
|
|
1116
|
+
"CG",
|
|
1117
|
+
"CD",
|
|
1118
|
+
"CI",
|
|
1119
|
+
"DJ",
|
|
1120
|
+
"EG",
|
|
1121
|
+
"GQ",
|
|
1122
|
+
"ER",
|
|
1123
|
+
"ET",
|
|
1124
|
+
"GA",
|
|
1125
|
+
"GM",
|
|
1126
|
+
"GH",
|
|
1127
|
+
"GN",
|
|
1128
|
+
"GW",
|
|
1129
|
+
"IL",
|
|
1130
|
+
"JO",
|
|
1131
|
+
"KE",
|
|
1132
|
+
"LB",
|
|
1133
|
+
"LS",
|
|
1134
|
+
"LR",
|
|
1135
|
+
"LY",
|
|
1136
|
+
"MG",
|
|
1137
|
+
"MW",
|
|
1138
|
+
"ML",
|
|
1139
|
+
"MR",
|
|
1140
|
+
"MU",
|
|
1141
|
+
"MA",
|
|
1142
|
+
"MZ",
|
|
1143
|
+
"NA",
|
|
1144
|
+
"NE",
|
|
1145
|
+
"NG",
|
|
1146
|
+
"PS",
|
|
1147
|
+
"RE",
|
|
1148
|
+
"RW",
|
|
1149
|
+
"ST",
|
|
1150
|
+
"SN",
|
|
1151
|
+
"SC",
|
|
1152
|
+
"SL",
|
|
1153
|
+
"ZA",
|
|
1154
|
+
"SZ",
|
|
1155
|
+
"TZ",
|
|
1156
|
+
"TG",
|
|
1157
|
+
"TN",
|
|
1158
|
+
"UG",
|
|
1159
|
+
"ZM",
|
|
1160
|
+
"ZW"
|
|
1161
|
+
],
|
|
1162
|
+
"CE-CH": ["LI", "CH"],
|
|
1163
|
+
"CE-DK": ["FO", "GL", "DK"],
|
|
1164
|
+
"CE-EU": [
|
|
1165
|
+
"AL",
|
|
1166
|
+
"AD",
|
|
1167
|
+
"AM",
|
|
1168
|
+
"AT",
|
|
1169
|
+
"AZ",
|
|
1170
|
+
"BY",
|
|
1171
|
+
"BE",
|
|
1172
|
+
"BA",
|
|
1173
|
+
"BG",
|
|
1174
|
+
"HR",
|
|
1175
|
+
"CY",
|
|
1176
|
+
"CZ",
|
|
1177
|
+
"EE",
|
|
1178
|
+
"FI",
|
|
1179
|
+
"FR",
|
|
1180
|
+
"GE",
|
|
1181
|
+
"DE",
|
|
1182
|
+
"GI",
|
|
1183
|
+
"GR",
|
|
1184
|
+
"HU",
|
|
1185
|
+
"IS",
|
|
1186
|
+
"IE",
|
|
1187
|
+
"IT",
|
|
1188
|
+
"KZ",
|
|
1189
|
+
"XK",
|
|
1190
|
+
"KG",
|
|
1191
|
+
"LV",
|
|
1192
|
+
"LT",
|
|
1193
|
+
"LU",
|
|
1194
|
+
"MT",
|
|
1195
|
+
"MD",
|
|
1196
|
+
"MC",
|
|
1197
|
+
"ME",
|
|
1198
|
+
"NL",
|
|
1199
|
+
"PL",
|
|
1200
|
+
"PT",
|
|
1201
|
+
"MK",
|
|
1202
|
+
"RO",
|
|
1203
|
+
"RS",
|
|
1204
|
+
"SK",
|
|
1205
|
+
"SI",
|
|
1206
|
+
"ES",
|
|
1207
|
+
"TJ",
|
|
1208
|
+
"TR",
|
|
1209
|
+
"TM",
|
|
1210
|
+
"UA",
|
|
1211
|
+
"UZ"
|
|
1212
|
+
],
|
|
1213
|
+
"CE-ME": ["AE", "BH", "KW", "OM", "QA", "SA"],
|
|
1214
|
+
"CE-NO": ["NO"],
|
|
1215
|
+
"CE-SE": ["SE"],
|
|
1216
|
+
"LA-LA": [
|
|
1217
|
+
"AI",
|
|
1218
|
+
"AG",
|
|
1219
|
+
"AR",
|
|
1220
|
+
"AW",
|
|
1221
|
+
"BS",
|
|
1222
|
+
"BB",
|
|
1223
|
+
"BZ",
|
|
1224
|
+
"BM",
|
|
1225
|
+
"BO",
|
|
1226
|
+
"BR",
|
|
1227
|
+
"KY",
|
|
1228
|
+
"CL",
|
|
1229
|
+
"CO",
|
|
1230
|
+
"CR",
|
|
1231
|
+
"DM",
|
|
1232
|
+
"DO",
|
|
1233
|
+
"EC",
|
|
1234
|
+
"SV",
|
|
1235
|
+
"FK",
|
|
1236
|
+
"GF",
|
|
1237
|
+
"GD",
|
|
1238
|
+
"GT",
|
|
1239
|
+
"GY",
|
|
1240
|
+
"HT",
|
|
1241
|
+
"HN",
|
|
1242
|
+
"JM",
|
|
1243
|
+
"MQ",
|
|
1244
|
+
"MX",
|
|
1245
|
+
"MS",
|
|
1246
|
+
"NI",
|
|
1247
|
+
"MP",
|
|
1248
|
+
"PA",
|
|
1249
|
+
"PY",
|
|
1250
|
+
"PE",
|
|
1251
|
+
"KN",
|
|
1252
|
+
"LC",
|
|
1253
|
+
"VC",
|
|
1254
|
+
"SR",
|
|
1255
|
+
"TT",
|
|
1256
|
+
"TC",
|
|
1257
|
+
"UY",
|
|
1258
|
+
"VG",
|
|
1259
|
+
"VI"
|
|
1260
|
+
],
|
|
1261
|
+
"NA-CA": ["CA"],
|
|
1262
|
+
"NA-US": ["AS", "GU", "FM", "PR", "US"],
|
|
1263
|
+
"UK-UK": ["GB"]
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// src/data/eea-countries.json
|
|
1267
|
+
var eea_countries_default = [
|
|
1268
|
+
"AT",
|
|
1269
|
+
"BE",
|
|
1270
|
+
"BG",
|
|
1271
|
+
"HR",
|
|
1272
|
+
"CY",
|
|
1273
|
+
"CZ",
|
|
1274
|
+
"DK",
|
|
1275
|
+
"EE",
|
|
1276
|
+
"FO",
|
|
1277
|
+
"FI",
|
|
1278
|
+
"FR",
|
|
1279
|
+
"DE",
|
|
1280
|
+
"GR",
|
|
1281
|
+
"GL",
|
|
1282
|
+
"HU",
|
|
1283
|
+
"IS",
|
|
1284
|
+
"IE",
|
|
1285
|
+
"IT",
|
|
1286
|
+
"LV",
|
|
1287
|
+
"LI",
|
|
1288
|
+
"LT",
|
|
1289
|
+
"LU",
|
|
1290
|
+
"MT",
|
|
1291
|
+
"NL",
|
|
1292
|
+
"NO",
|
|
1293
|
+
"PL",
|
|
1294
|
+
"PT",
|
|
1295
|
+
"RO",
|
|
1296
|
+
"SK",
|
|
1297
|
+
"SI",
|
|
1298
|
+
"ES",
|
|
1299
|
+
"SE"
|
|
1300
|
+
];
|
|
1301
|
+
|
|
1302
|
+
// src/utils/billing-countries.ts
|
|
1303
|
+
var priceZones = price_zones_default;
|
|
1304
|
+
var eeaCountries = eea_countries_default;
|
|
1305
|
+
var ALL_COUNTRY_CODES = Object.values(priceZones).flat();
|
|
1306
|
+
var getZoneByCountry = (country) => Object.keys(priceZones).find((key) => priceZones[key].includes(country)) ?? null;
|
|
1307
|
+
var getSupportedBillingCountries = (offerCountry, offerVariant) => {
|
|
1308
|
+
if (offerVariant === "bundle" || offerVariant === "insider_print") {
|
|
1309
|
+
return [...ALL_COUNTRY_CODES];
|
|
1310
|
+
}
|
|
1311
|
+
const zone = getZoneByCountry(offerCountry);
|
|
1312
|
+
if (!zone) {
|
|
1313
|
+
return [...ALL_COUNTRY_CODES];
|
|
1314
|
+
}
|
|
1315
|
+
const zoneCountries = priceZones[zone];
|
|
1316
|
+
if (eeaCountries.includes(offerCountry)) {
|
|
1317
|
+
return Array.from(/* @__PURE__ */ new Set([...zoneCountries, ...eeaCountries]));
|
|
1318
|
+
}
|
|
1319
|
+
return [...zoneCountries];
|
|
1320
|
+
};
|
|
1321
|
+
|
|
1322
|
+
// src/apple-pay-button.ts
|
|
1323
|
+
var FEATURE_APPLE_PAY_EXPRESS_CHECKOUT = "FEATURE_APPLE_PAY_EXPRESS_CHECKOUT";
|
|
1324
|
+
var TOTAL_LABEL = "The Economist";
|
|
1325
|
+
var DEFAULT_CURRENCY = "USD";
|
|
1326
|
+
var DEFAULT_TOTAL = "0.00";
|
|
1327
|
+
var ERROR_INITIATE = "Failed to initiate Apple Pay session:";
|
|
1328
|
+
var SEL_CONTAINER = ".apple-pay-container";
|
|
1329
|
+
var SEL_BUTTON = ".apple-pay-btn";
|
|
1330
|
+
var APPLE_PAY_EXCLUDED_VARIANTS = [
|
|
1331
|
+
"bundle",
|
|
1332
|
+
"insider_print"
|
|
1333
|
+
];
|
|
1334
|
+
var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
1335
|
+
static tag = "teg-apple-pay-button";
|
|
1336
|
+
static define() {
|
|
1337
|
+
define(_ApplePayButton.tag, _ApplePayButton);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Creates and configures an ApplePayButton if the feature flag is present.
|
|
1341
|
+
*/
|
|
1342
|
+
static createIfEnabled(offer, country, entryPoint) {
|
|
1343
|
+
if (!offer) return null;
|
|
1344
|
+
if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant))
|
|
1345
|
+
return null;
|
|
1346
|
+
if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT))
|
|
1347
|
+
return null;
|
|
1348
|
+
_ApplePayButton.define();
|
|
1349
|
+
const button = document.createElement(_ApplePayButton.tag);
|
|
1350
|
+
button.offer = offer;
|
|
1351
|
+
button.country = country;
|
|
1352
|
+
button.entryPoint = entryPoint;
|
|
1353
|
+
return button;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Fallback session used when the BFF is unreachable at page load.
|
|
1357
|
+
* Treats the user as anonymous (not logged in, new-eligible).
|
|
1358
|
+
* Matches the behaviour of sma-checkout when session fetch fails.
|
|
1359
|
+
*/
|
|
1360
|
+
static #ANONYMOUS_SESSION = {
|
|
1361
|
+
loggedIn: false,
|
|
1362
|
+
userType: "new-eligible-user"
|
|
1363
|
+
};
|
|
1364
|
+
// ─── Private state ──────────────────────────────────────────────────────────
|
|
1365
|
+
#offer = null;
|
|
1366
|
+
#country = "";
|
|
1367
|
+
#rendering = false;
|
|
1368
|
+
#readyPromise = null;
|
|
1369
|
+
#sessionData = null;
|
|
1370
|
+
#paymentOptions = null;
|
|
1371
|
+
#returnUrl = "";
|
|
1372
|
+
#supportedBillingCountries = [];
|
|
1373
|
+
#entryPoint = "";
|
|
1374
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
1375
|
+
get offer() {
|
|
1376
|
+
return this.#offer;
|
|
1377
|
+
}
|
|
1378
|
+
set offer(value) {
|
|
1379
|
+
this.#offer = value;
|
|
1380
|
+
this.#updateDisabledState();
|
|
1381
|
+
}
|
|
1382
|
+
get country() {
|
|
1383
|
+
return this.#country;
|
|
1384
|
+
}
|
|
1385
|
+
set country(value) {
|
|
1386
|
+
this.#country = value;
|
|
1387
|
+
}
|
|
1388
|
+
get entryPoint() {
|
|
1389
|
+
return this.#entryPoint;
|
|
1390
|
+
}
|
|
1391
|
+
set entryPoint(value) {
|
|
1392
|
+
this.#entryPoint = value;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Awaitable contract for the component's current render lifecycle.
|
|
1396
|
+
*
|
|
1397
|
+
* - If called **after** the element has been appended to the DOM, resolves
|
|
1398
|
+
* once `connectedCallback` finishes — whether Apple Pay rendered
|
|
1399
|
+
* successfully or was hidden (unavailable / suppressed by session).
|
|
1400
|
+
* - If called **before** the element is appended (or if it has never been
|
|
1401
|
+
* connected), resolves immediately to `undefined`. It does **not** wait
|
|
1402
|
+
* for a future connection, so always `await whenReady()` after `appendChild`:
|
|
1403
|
+
*
|
|
1404
|
+
* ```ts
|
|
1405
|
+
* container.appendChild(btn); // browser fires connectedCallback
|
|
1406
|
+
* await btn.whenReady(); // waits for render to finish
|
|
1407
|
+
* if (btn.style.display !== 'none') { /* available *\/ }
|
|
1408
|
+
* ```
|
|
1409
|
+
*/
|
|
1410
|
+
whenReady() {
|
|
1411
|
+
return this.#readyPromise ?? Promise.resolve();
|
|
1412
|
+
}
|
|
1413
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
|
1414
|
+
async connectedCallback() {
|
|
1415
|
+
if (this.#rendering || this.querySelector(SEL_CONTAINER)) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.#rendering = true;
|
|
1419
|
+
this.#readyPromise = this.#run();
|
|
1420
|
+
await this.#readyPromise;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Resets all render state when the element is removed from the DOM.
|
|
1424
|
+
*
|
|
1425
|
+
* A shared library component cannot assume it won't be moved in the DOM
|
|
1426
|
+
* (e.g. lazy panels, SPA route changes). Resetting on disconnect means
|
|
1427
|
+
* reattach always triggers a fresh `#run()` — new session check, new payment
|
|
1428
|
+
* options fetch, fresh event listeners — preventing stale data from persisting
|
|
1429
|
+
* and eliminating the implicit reliance on the DOM as a render-state flag.
|
|
1430
|
+
*/
|
|
1431
|
+
disconnectedCallback() {
|
|
1432
|
+
this.#rendering = false;
|
|
1433
|
+
this.#readyPromise = null;
|
|
1434
|
+
this.#sessionData = null;
|
|
1435
|
+
this.#paymentOptions = null;
|
|
1436
|
+
this.#returnUrl = "";
|
|
1437
|
+
this.#supportedBillingCountries = [];
|
|
1438
|
+
this.querySelector(SEL_CONTAINER)?.remove();
|
|
1439
|
+
}
|
|
1440
|
+
// ─── Render pipeline ────────────────────────────────────────────────────────
|
|
1441
|
+
async #run() {
|
|
1442
|
+
try {
|
|
1443
|
+
if (await this.#checkAvailability()) return;
|
|
1444
|
+
if (await this.#checkSession()) return;
|
|
1445
|
+
if (this.#checkOffer()) return;
|
|
1446
|
+
this.#captureReturnUrl();
|
|
1447
|
+
this.#computeBillingCountries();
|
|
1448
|
+
if (await this.#fetchAndCacheOptions()) return;
|
|
1449
|
+
this.#renderTemplate();
|
|
1450
|
+
} catch {
|
|
1451
|
+
this.style.display = "none";
|
|
1452
|
+
} finally {
|
|
1453
|
+
this.#rendering = false;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
/** Returns `true` (hide) when Apple Pay is unavailable on this device/browser. */
|
|
1457
|
+
async #checkAvailability() {
|
|
1458
|
+
const available = await isApplePayAvailable();
|
|
1459
|
+
if (!available) {
|
|
1460
|
+
this.style.display = "none";
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Returns `true` (hide) when the user already has an active subscription or is B2B.
|
|
1467
|
+
* Session fetch errors are swallowed — the component continues as an anonymous user.
|
|
1468
|
+
*/
|
|
1469
|
+
async #checkSession() {
|
|
1470
|
+
this.#sessionData = await this.#fetchSession();
|
|
1471
|
+
if (this.#sessionData?.loggedIn && (this.#sessionData.userType === "active-subscription" || this.#sessionData.userType === "b2b-user")) {
|
|
1472
|
+
this.style.display = "none";
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
/** Returns `true` (hide) when no offer has been set before mount. */
|
|
1478
|
+
#checkOffer() {
|
|
1479
|
+
if (!this.#offer) {
|
|
1480
|
+
this.style.display = "none";
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
/** Captures the current page URL once at mount for reuse across the session lifetime. */
|
|
1486
|
+
#captureReturnUrl() {
|
|
1487
|
+
this.#returnUrl = captureOriginUrl();
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Computes supported billing countries once at mount.
|
|
1491
|
+
* Mirrors sma-checkout `billingCountryOptions()` — stable for the element's
|
|
1492
|
+
* connected lifetime; recalculated on reattach via `disconnectedCallback` reset.
|
|
1493
|
+
*/
|
|
1494
|
+
#computeBillingCountries() {
|
|
1495
|
+
this.#supportedBillingCountries = getSupportedBillingCountries(
|
|
1496
|
+
this.#country,
|
|
1497
|
+
this.#offer.product?.variant ?? ""
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Returns `true` (hide) when the wallet BFF reports Apple Pay is unavailable
|
|
1502
|
+
* for this offer. The response is cached and reused in `#initiateApplePay`.
|
|
1503
|
+
*/
|
|
1504
|
+
async #fetchAndCacheOptions() {
|
|
1505
|
+
this.#paymentOptions = await this.#fetchPaymentOptions();
|
|
1506
|
+
console.log("[web-apple-pay] #fetchAndCacheOptions: fetched payment options", { paymentOptions: this.#paymentOptions });
|
|
1507
|
+
if (!this.#paymentOptions?.options?.apple_pay) {
|
|
1508
|
+
this.style.display = "none";
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
/** Injects the button template and wires click and confirm event listeners. */
|
|
1514
|
+
#renderTemplate() {
|
|
1515
|
+
this.style.removeProperty("display");
|
|
1516
|
+
ensureApplePayLogoSymbol();
|
|
1517
|
+
const template = document.createElement("template");
|
|
1518
|
+
template.innerHTML = apple_pay_button_template_default;
|
|
1519
|
+
this.appendChild(template.content.cloneNode(true));
|
|
1520
|
+
this.#updateDisabledState();
|
|
1521
|
+
trackExpressWalletImpression(this.#offer.sku_id, {
|
|
1522
|
+
entryPoint: this.#entryPoint,
|
|
1523
|
+
country: this.#country
|
|
1524
|
+
});
|
|
1525
|
+
const button = this.querySelector(SEL_BUTTON);
|
|
1526
|
+
button.addEventListener("click", () => {
|
|
1527
|
+
this.#openModal();
|
|
1528
|
+
});
|
|
1529
|
+
this.addEventListener("apple-pay-confirmed", (event) => {
|
|
1530
|
+
const { skuId, country } = event.detail;
|
|
1531
|
+
this.#initiateApplePay(skuId, country);
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
1535
|
+
#updateDisabledState() {
|
|
1536
|
+
const button = this.querySelector(SEL_BUTTON);
|
|
1537
|
+
if (!button) return;
|
|
1538
|
+
if (this.#offer) {
|
|
1539
|
+
button.removeAttribute("disabled");
|
|
1540
|
+
button.removeAttribute("aria-disabled");
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
button.setAttribute("disabled", "");
|
|
1544
|
+
button.setAttribute("aria-disabled", "true");
|
|
1545
|
+
}
|
|
1546
|
+
#openModal() {
|
|
1547
|
+
if (!this.#offer || document.querySelector(ApplePayModal.tag)) return;
|
|
1548
|
+
trackExpressWalletClick(this.#offer.sku_id);
|
|
1549
|
+
ApplePayModal.define();
|
|
1550
|
+
const modal = document.createElement(ApplePayModal.tag);
|
|
1551
|
+
modal.offer = this.#offer;
|
|
1552
|
+
modal.country = this.#country;
|
|
1553
|
+
if (this.#sessionData?.loggedIn) {
|
|
1554
|
+
modal.dataset.loggedIn = "true";
|
|
1555
|
+
}
|
|
1556
|
+
modal.open();
|
|
1557
|
+
}
|
|
1558
|
+
async #fetchSession() {
|
|
1559
|
+
try {
|
|
1560
|
+
return await getExpressCheckoutSession();
|
|
1561
|
+
} catch {
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async #fetchPaymentOptions() {
|
|
1566
|
+
try {
|
|
1567
|
+
return await getPaymentOptions({
|
|
1568
|
+
sku: this.#offer.sku_id,
|
|
1569
|
+
countryCode: this.#country,
|
|
1570
|
+
currencyCode: this.#offer.price?.currency_code ?? DEFAULT_CURRENCY,
|
|
1571
|
+
checkoutUrl: this.#returnUrl
|
|
1572
|
+
});
|
|
1573
|
+
} catch {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
#buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
|
|
1578
|
+
const offer = this.#offer;
|
|
1579
|
+
const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
|
|
1580
|
+
return {
|
|
1581
|
+
countryCode: country,
|
|
1582
|
+
currencyCode: offer.price?.currency_code ?? DEFAULT_CURRENCY,
|
|
1583
|
+
supportedNetworks: paymentOptions.options.apple_pay.settings.supported_networks,
|
|
1584
|
+
totalAmount,
|
|
1585
|
+
totalLabel: TOTAL_LABEL,
|
|
1586
|
+
skuId,
|
|
1587
|
+
returnUrl: this.#returnUrl,
|
|
1588
|
+
userLoggedIn: sessionData.loggedIn,
|
|
1589
|
+
onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
|
|
1590
|
+
skuId,
|
|
1591
|
+
country,
|
|
1592
|
+
returnUrl: this.#returnUrl,
|
|
1593
|
+
queryString: buildQueryString(),
|
|
1594
|
+
supportedBillingCountries: this.#supportedBillingCountries,
|
|
1595
|
+
recaptchaTokens
|
|
1596
|
+
})
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async #initiateApplePay(skuId, country) {
|
|
1600
|
+
try {
|
|
1601
|
+
const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
|
|
1602
|
+
const paymentOptions = this.#paymentOptions;
|
|
1603
|
+
const recaptchaTokens = await getPurchaseRecaptchaTokens();
|
|
1604
|
+
startApplePaySession(
|
|
1605
|
+
this.#buildSessionParams(
|
|
1606
|
+
skuId,
|
|
1607
|
+
country,
|
|
1608
|
+
sessionData,
|
|
1609
|
+
paymentOptions,
|
|
1610
|
+
recaptchaTokens
|
|
1611
|
+
)
|
|
1612
|
+
);
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
console.error(ERROR_INITIATE, error);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
export {
|
|
1619
|
+
APPLE_PAY_EXCLUDED_VARIANTS,
|
|
1620
|
+
ApplePayButton
|
|
1621
|
+
};
|