@economist/web-apple-pay 0.1.3-beta.6 → 0.1.3-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -78,6 +78,62 @@ function ensureApplePayLogoSymbol() {
78
78
  // src/templates/apple-pay-modal.template.html
79
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
80
 
81
+ // src/clients/payment-checkout/config.ts
82
+ var runtimeConfig = null;
83
+ var configureApplePay = (config) => {
84
+ runtimeConfig = {
85
+ ...config
86
+ };
87
+ };
88
+ var getApplePayConfig = () => runtimeConfig;
89
+
90
+ // src/utils/recaptcha.ts
91
+ var loadRecaptchaScript = (siteKey) => {
92
+ return new Promise((resolve, reject) => {
93
+ if (typeof grecaptcha !== "undefined") return resolve();
94
+ const existingScript = document.querySelector(
95
+ 'script[src*="recaptcha/api.js"]'
96
+ );
97
+ if (existingScript) {
98
+ existingScript.addEventListener("load", () => resolve());
99
+ existingScript.addEventListener(
100
+ "error",
101
+ () => reject(new Error("Failed to load existing reCAPTCHA script"))
102
+ );
103
+ return;
104
+ }
105
+ const script = document.createElement("script");
106
+ script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
107
+ script.async = true;
108
+ script.defer = true;
109
+ script.onload = () => resolve();
110
+ script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
111
+ document.head.appendChild(script);
112
+ });
113
+ };
114
+ var getRecaptchaToken = async (action) => {
115
+ const siteKey = getApplePayConfig()?.recaptchaSiteKey;
116
+ if (!siteKey) {
117
+ throw new Error(
118
+ "[web-apple-pay] Missing recaptchaSiteKey. Call configureApplePay({ recaptchaSiteKey, ... }) before initiating Apple Pay."
119
+ );
120
+ }
121
+ await loadRecaptchaScript(siteKey);
122
+ return new Promise((resolve, reject) => {
123
+ grecaptcha.ready(() => {
124
+ grecaptcha.execute(siteKey, { action }).then(resolve).catch(reject);
125
+ });
126
+ });
127
+ };
128
+ var getPurchaseRecaptchaTokens = async () => {
129
+ const [checkEligibility, newBasket, registerUser] = await Promise.all([
130
+ getRecaptchaToken("checkEligibility"),
131
+ getRecaptchaToken("newBasket"),
132
+ getRecaptchaToken("registerUser")
133
+ ]);
134
+ return { checkEligibility, newBasket, registerUser };
135
+ };
136
+
81
137
  // src/apple-pay-modal.ts
82
138
  function getRenewalDateText(unit, duration) {
83
139
  const now = /* @__PURE__ */ new Date();
@@ -297,6 +353,8 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
297
353
  #country = "";
298
354
  #triggerElement = null;
299
355
  #keyHandler = null;
356
+ #recaptchaTokens = null;
357
+ #recaptchaTokensPromise = null;
300
358
  set offer(value) {
301
359
  this.#offer = value;
302
360
  }
@@ -317,7 +375,6 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
317
375
  const offer = this.#offer;
318
376
  const country = this.#country;
319
377
  if (!offer) {
320
- console.error("ApplePayModal: Missing or invalid offer data");
321
378
  this.remove();
322
379
  return;
323
380
  }
@@ -392,6 +449,23 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
392
449
  }
393
450
  });
394
451
  }
452
+ if (this.#recaptchaTokensPromise) {
453
+ if (cta) {
454
+ cta.setAttribute("disabled", "");
455
+ cta.setAttribute("aria-disabled", "true");
456
+ cta.setAttribute("aria-busy", "true");
457
+ }
458
+ this.#recaptchaTokensPromise.then((tokens) => {
459
+ this.#recaptchaTokens = tokens;
460
+ if (cta) {
461
+ cta.removeAttribute("disabled");
462
+ cta.removeAttribute("aria-disabled");
463
+ cta.removeAttribute("aria-busy");
464
+ }
465
+ }).catch(() => {
466
+ console.error("[web-apple-pay] apple-pay-modal: failed to pre-fetch reCAPTCHA tokens; CTA remains disabled");
467
+ });
468
+ }
395
469
  cta?.addEventListener("click", () => {
396
470
  if (isUS && checkbox && !checkbox.checked) {
397
471
  errorElement?.classList.add("apm__error--visible");
@@ -402,7 +476,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
402
476
  new CustomEvent("apple-pay-confirmed", {
403
477
  bubbles: true,
404
478
  composed: true,
405
- detail: { skuId: offer.sku_id, country }
479
+ detail: { skuId: offer.sku_id, country, recaptchaTokens: this.#recaptchaTokens }
406
480
  })
407
481
  );
408
482
  this.close();
@@ -413,6 +487,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
413
487
  );
414
488
  }
415
489
  open() {
490
+ this.#recaptchaTokensPromise = getPurchaseRecaptchaTokens();
416
491
  this.#triggerElement = document.activeElement;
417
492
  document.body.appendChild(this);
418
493
  this.#keyHandler = (event) => {
@@ -439,16 +514,91 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
439
514
  };
440
515
 
441
516
  // 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';
517
+ var apple_pay_button_template_default = `<style>
518
+ /* Use CSS variables for layout so parent walls can easily override placement and sizing */
519
+ teg-apple-pay-button {
520
+ display: block;
521
+ margin: var(--apple-pay-margin, 0);
522
+ width: 100%;
523
+ height: 100%;
524
+ }
443
525
 
444
- // src/clients/payment-checkout/config.ts
445
- var runtimeConfig = null;
446
- var configureApplePay = (config) => {
447
- runtimeConfig = {
448
- ...config
449
- };
450
- };
451
- var getApplePayConfig = () => runtimeConfig;
526
+ teg-apple-pay-button .apple-pay-container {
527
+ display: flex;
528
+ flex-direction: column;
529
+ align-items: stretch;
530
+ width: 100%;
531
+ height: 100%;
532
+ }
533
+
534
+ teg-apple-pay-button .apple-pay-btn {
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ gap: 0.375rem;
539
+ width: 100%;
540
+ min-width: 0;
541
+ height: 100%;
542
+ min-height: var(--apple-pay-height, 2.75rem);
543
+ background-color: #000;
544
+ color: #fff;
545
+ border: none;
546
+ border-radius: var(--apple-pay-border-radius, 1.375rem);
547
+ cursor: pointer;
548
+ font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));
549
+ font-size: 1.0625rem;
550
+ font-weight: 500;
551
+ }
552
+
553
+ teg-apple-pay-button .apple-pay-btn svg {
554
+ flex-shrink: 0;
555
+ display: block;
556
+ }
557
+
558
+ teg-apple-pay-button .apple-pay-btn.wap-loading {
559
+ position: relative;
560
+ cursor: default;
561
+ }
562
+
563
+ teg-apple-pay-button .apple-pay-btn.wap-loading svg {
564
+ visibility: hidden;
565
+ }
566
+
567
+ teg-apple-pay-button .apple-pay-btn.wap-loading::after {
568
+ content: '';
569
+ position: absolute;
570
+ left: 50%;
571
+ top: 50%;
572
+ margin-left: -0.625rem;
573
+ margin-top: -0.625rem;
574
+ width: 1.25rem;
575
+ height: 1.25rem;
576
+ border: 2px solid rgba(255, 255, 255, 0.2);
577
+ border-top-color: #fff;
578
+ border-radius: 50%;
579
+ animation: wap-button-spinner 0.8s linear infinite;
580
+ }
581
+
582
+ @keyframes wap-button-spinner {
583
+ to {
584
+ transform: rotate(360deg);
585
+ }
586
+ }
587
+ </style>
588
+
589
+ <div class="apple-pay-container" aria-label="Express checkout with Apple Pay">
590
+ <button
591
+ class="apple-pay-btn"
592
+ type="button"
593
+ aria-label="Pay with Apple Pay"
594
+ data-analytics="paywall:apple-pay-checkout"
595
+ >
596
+ <svg width="51" height="22" aria-label="Apple Pay" role="img">
597
+ <use href="#apple-pay-logo" />
598
+ </svg>
599
+ </button>
600
+ </div>
601
+ `;
452
602
 
453
603
  // src/clients/payment-checkout/detects.ts
454
604
  var isAppleDeviceOrSafari = () => {
@@ -459,20 +609,25 @@ var isAppleDeviceOrSafari = () => {
459
609
  var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
460
610
  var isApplePayAvailable = async () => {
461
611
  if (bypassApplePayChecks()) {
612
+ console.debug("[web-apple-pay] isApplePayAvailable: bypass enabled, returning true");
462
613
  return true;
463
614
  }
464
615
  if (!isAppleDeviceOrSafari()) {
616
+ console.debug("[web-apple-pay] isApplePayAvailable: not an Apple device or Safari", { userAgent: navigator.userAgent });
465
617
  return false;
466
618
  }
467
619
  const applePaySession = window.ApplePaySession;
468
620
  if (!applePaySession) {
621
+ console.debug("[web-apple-pay] isApplePayAvailable: ApplePaySession not present on window");
469
622
  return false;
470
623
  }
471
624
  try {
472
625
  if (typeof applePaySession.canMakePayments !== "function") {
626
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePayments is not a function");
473
627
  return false;
474
628
  }
475
629
  if (!applePaySession.canMakePayments()) {
630
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePayments() returned false");
476
631
  return false;
477
632
  }
478
633
  const merchantId = getApplePayConfig()?.merchantId;
@@ -482,7 +637,9 @@ var isApplePayAvailable = async () => {
482
637
  );
483
638
  return false;
484
639
  }
485
- return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
640
+ const hasActiveCard = await applePaySession.canMakePaymentsWithActiveCard(merchantId);
641
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePaymentsWithActiveCard =", hasActiveCard, { merchantId });
642
+ return hasActiveCard;
486
643
  } catch (error) {
487
644
  console.error("Error checking Apple Pay active card status:", error);
488
645
  return false;
@@ -612,53 +769,6 @@ var redirectToCheckoutFallback = (params) => {
612
769
  return url.pathname + url.search;
613
770
  };
614
771
 
615
- // src/utils/recaptcha.ts
616
- var loadRecaptchaScript = (siteKey) => {
617
- return new Promise((resolve, reject) => {
618
- if (typeof grecaptcha !== "undefined") return resolve();
619
- const existingScript = document.querySelector(
620
- 'script[src*="recaptcha/api.js"]'
621
- );
622
- if (existingScript) {
623
- existingScript.addEventListener("load", () => resolve());
624
- existingScript.addEventListener(
625
- "error",
626
- () => reject(new Error("Failed to load existing reCAPTCHA script"))
627
- );
628
- return;
629
- }
630
- const script = document.createElement("script");
631
- script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
632
- script.async = true;
633
- script.defer = true;
634
- script.onload = () => resolve();
635
- script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
636
- document.head.appendChild(script);
637
- });
638
- };
639
- var getRecaptchaToken = async (action) => {
640
- const siteKey = getApplePayConfig()?.recaptchaSiteKey;
641
- if (!siteKey) {
642
- throw new Error(
643
- "[web-apple-pay] Missing recaptchaSiteKey. Call configureApplePay({ recaptchaSiteKey, ... }) before initiating Apple Pay."
644
- );
645
- }
646
- await loadRecaptchaScript(siteKey);
647
- return new Promise((resolve, reject) => {
648
- grecaptcha.ready(() => {
649
- grecaptcha.execute(siteKey, { action }).then(resolve).catch(reject);
650
- });
651
- });
652
- };
653
- var getPurchaseRecaptchaTokens = async () => {
654
- const [checkEligibility, newBasket, registerUser] = await Promise.all([
655
- getRecaptchaToken("checkEligibility"),
656
- getRecaptchaToken("newBasket"),
657
- getRecaptchaToken("registerUser")
658
- ]);
659
- return { checkEligibility, newBasket, registerUser };
660
- };
661
-
662
772
  // src/utils/expressCheckoutTracking.ts
663
773
  var EXPRESS_CHECKOUT_EVENTS = {
664
774
  /** Wallet button displayed */
@@ -744,7 +854,52 @@ var MSG_EMAIL_COLLISION = "This email address is already linked to an account. P
744
854
  var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
745
855
 
746
856
  // src/clients/payment-checkout/apple-session.ts
857
+ function redirectToFallbackCheckout(config) {
858
+ trackFallbackCheckout(config.skuId);
859
+ window.location.href = redirectToCheckoutFallback({
860
+ skuId: config.skuId,
861
+ returnUrl: config.returnUrl,
862
+ inlineError: MSG_PAYMENT_GENERIC_ERROR
863
+ });
864
+ }
865
+ async function handleMerchantValidation(event, session, config) {
866
+ console.debug("[web-apple-pay] onvalidatemerchant: validating", { validationURL: event.validationURL });
867
+ try {
868
+ const validationHost = new URL(event.validationURL).host;
869
+ if (!validationHost.endsWith("apple.com")) {
870
+ console.debug("[web-apple-pay] onvalidatemerchant: invalid host, aborting", { validationHost });
871
+ session.abort();
872
+ redirectToFallbackCheckout(config);
873
+ return;
874
+ }
875
+ const recaptchaToken = await getRecaptchaToken("verifyMerchant");
876
+ const response = await validateAppleMerchant({
877
+ validationURL: event.validationURL,
878
+ recaptchaToken
879
+ });
880
+ if (!response?.merchantSession) {
881
+ console.debug("[web-apple-pay] onvalidatemerchant: no merchantSession in response, aborting");
882
+ session.abort();
883
+ redirectToFallbackCheckout(config);
884
+ return;
885
+ }
886
+ console.debug("[web-apple-pay] onvalidatemerchant: merchant validated successfully");
887
+ session.completeMerchantValidation(response.merchantSession);
888
+ } catch (err) {
889
+ console.error("[web-apple-pay] onvalidatemerchant: error during validation", err);
890
+ session.abort();
891
+ redirectToFallbackCheckout(config);
892
+ }
893
+ }
747
894
  function startApplePaySession(config) {
895
+ console.debug("[web-apple-pay] startApplePaySession: starting session", {
896
+ skuId: config.skuId,
897
+ countryCode: config.countryCode,
898
+ currencyCode: config.currencyCode,
899
+ supportedNetworks: config.supportedNetworks,
900
+ totalAmount: config.totalAmount,
901
+ userLoggedIn: config.userLoggedIn
902
+ });
748
903
  const paymentRequest = {
749
904
  countryCode: config.countryCode,
750
905
  currencyCode: config.currencyCode,
@@ -758,55 +913,27 @@ function startApplePaySession(config) {
758
913
  // Only request email from Apple Pay sheet if user is NOT logged in
759
914
  ...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
760
915
  };
761
- const session = new ApplePaySession(3, paymentRequest);
762
- session.onvalidatemerchant = async (event) => {
763
- try {
764
- const validationHost = new URL(event.validationURL).host;
765
- if (!validationHost.endsWith("apple.com")) {
766
- trackFallbackCheckout(config.skuId);
767
- session.abort();
768
- window.location.href = redirectToCheckoutFallback({
769
- skuId: config.skuId,
770
- returnUrl: config.returnUrl,
771
- inlineError: MSG_PAYMENT_GENERIC_ERROR
772
- });
773
- return;
774
- }
775
- const recaptchaToken = await getRecaptchaToken("verifyMerchant");
776
- const response = await validateAppleMerchant({
777
- validationURL: event.validationURL,
778
- recaptchaToken
779
- });
780
- if (!response?.merchantSession) {
781
- trackFallbackCheckout(config.skuId);
782
- session.abort();
783
- window.location.href = redirectToCheckoutFallback({
784
- skuId: config.skuId,
785
- returnUrl: config.returnUrl,
786
- inlineError: MSG_PAYMENT_GENERIC_ERROR
787
- });
788
- return;
789
- }
790
- session.completeMerchantValidation(response.merchantSession);
791
- } catch {
792
- trackFallbackCheckout(config.skuId);
793
- session.abort();
794
- window.location.href = redirectToCheckoutFallback({
795
- skuId: config.skuId,
796
- returnUrl: config.returnUrl,
797
- inlineError: MSG_PAYMENT_GENERIC_ERROR
798
- });
799
- }
800
- };
801
- session.onpaymentauthorized = (event) => {
802
- trackWalletPaymentAuthorised(config.skuId);
803
- config.onPaymentAuthorized(event, session);
804
- };
805
- session.oncancel = () => {
806
- config.onCancel?.();
807
- };
808
- trackWalletPaymentSheetOpen(config.skuId);
809
- session.begin();
916
+ try {
917
+ const session = new ApplePaySession(3, paymentRequest);
918
+ session.onvalidatemerchant = async (event) => {
919
+ await handleMerchantValidation(event, session, config);
920
+ };
921
+ session.onpaymentauthorized = (event) => {
922
+ console.debug("[web-apple-pay] onpaymentauthorized: payment authorised");
923
+ trackWalletPaymentAuthorised(config.skuId);
924
+ config.onPaymentAuthorized(event, session);
925
+ };
926
+ session.oncancel = () => {
927
+ console.debug("[web-apple-pay] oncancel: user cancelled Apple Pay sheet");
928
+ config.onCancel?.();
929
+ };
930
+ console.debug("[web-apple-pay] startApplePaySession: session.begin()");
931
+ trackWalletPaymentSheetOpen(config.skuId);
932
+ session.begin();
933
+ } catch (err) {
934
+ console.error("[web-apple-pay] startApplePaySession: failed to create session", err);
935
+ redirectToFallbackCheckout(config);
936
+ }
810
937
  }
811
938
 
812
939
  // src/clients/payment-checkout/contact-validation.ts
@@ -1309,11 +1436,15 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1309
1436
  * Creates and configures an ApplePayButton if the feature flag is present.
1310
1437
  */
1311
1438
  static createIfEnabled(offer, country, entryPoint) {
1312
- if (!offer) return null;
1313
- if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant))
1439
+ if (!offer) {
1440
+ return null;
1441
+ }
1442
+ if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant)) {
1314
1443
  return null;
1315
- if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT))
1444
+ }
1445
+ if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT)) {
1316
1446
  return null;
1447
+ }
1317
1448
  _ApplePayButton.define();
1318
1449
  const button = document.createElement(_ApplePayButton.tag);
1319
1450
  button.offer = offer;
@@ -1340,6 +1471,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1340
1471
  #returnUrl = "";
1341
1472
  #supportedBillingCountries = [];
1342
1473
  #entryPoint = "";
1474
+ #applePayConfirmedHandler = null;
1343
1475
  // ─── Public API ─────────────────────────────────────────────────────────────
1344
1476
  get offer() {
1345
1477
  return this.#offer;
@@ -1404,20 +1536,28 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1404
1536
  this.#paymentOptions = null;
1405
1537
  this.#returnUrl = "";
1406
1538
  this.#supportedBillingCountries = [];
1539
+ if (this.#applePayConfirmedHandler) {
1540
+ document.removeEventListener("apple-pay-confirmed", this.#applePayConfirmedHandler);
1541
+ this.#applePayConfirmedHandler = null;
1542
+ }
1407
1543
  this.querySelector(SEL_CONTAINER)?.remove();
1408
1544
  }
1409
1545
  // ─── Render pipeline ────────────────────────────────────────────────────────
1410
1546
  async #run() {
1547
+ console.debug("[web-apple-pay] #run: start");
1411
1548
  try {
1412
1549
  if (await this.#checkAvailability()) return;
1413
- if (await this.#checkSession()) return;
1414
1550
  if (this.#checkOffer()) return;
1551
+ this.#renderTemplate();
1552
+ if (await this.#checkSession()) return;
1415
1553
  this.#captureReturnUrl();
1416
1554
  this.#computeBillingCountries();
1417
1555
  if (await this.#fetchAndCacheOptions()) return;
1418
- this.#renderTemplate();
1419
- } catch {
1556
+ this.#finishLoading();
1557
+ } catch (err) {
1558
+ console.error("[web-apple-pay] #run: unexpected error, hiding button", err);
1420
1559
  this.style.display = "none";
1560
+ this.querySelector(SEL_CONTAINER)?.remove();
1421
1561
  } finally {
1422
1562
  this.#rendering = false;
1423
1563
  }
@@ -1426,9 +1566,11 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1426
1566
  async #checkAvailability() {
1427
1567
  const available = await isApplePayAvailable();
1428
1568
  if (!available) {
1569
+ console.debug("[web-apple-pay] #checkAvailability: not available on this device/browser");
1429
1570
  this.style.display = "none";
1430
1571
  return true;
1431
1572
  }
1573
+ console.debug("[web-apple-pay] #checkAvailability: ok");
1432
1574
  return false;
1433
1575
  }
1434
1576
  /**
@@ -1437,18 +1579,24 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1437
1579
  */
1438
1580
  async #checkSession() {
1439
1581
  this.#sessionData = await this.#fetchSession();
1582
+ console.debug("[web-apple-pay] #checkSession: session data", { loggedIn: this.#sessionData?.loggedIn, userType: this.#sessionData?.userType });
1440
1583
  if (this.#sessionData?.loggedIn && (this.#sessionData.userType === "active-subscription" || this.#sessionData.userType === "b2b-user")) {
1584
+ console.debug("[web-apple-pay] #checkSession: ineligible user, hiding button", { userType: this.#sessionData.userType });
1441
1585
  this.style.display = "none";
1586
+ this.querySelector(SEL_CONTAINER)?.remove();
1442
1587
  return true;
1443
1588
  }
1589
+ console.debug("[web-apple-pay] #checkSession: ok");
1444
1590
  return false;
1445
1591
  }
1446
1592
  /** Returns `true` (hide) when no offer has been set before mount. */
1447
1593
  #checkOffer() {
1448
1594
  if (!this.#offer) {
1595
+ console.debug("[web-apple-pay] #checkOffer: no offer set, hiding button");
1449
1596
  this.style.display = "none";
1450
1597
  return true;
1451
1598
  }
1599
+ console.debug("[web-apple-pay] #checkOffer: ok", { skuId: this.#offer.sku_id });
1452
1600
  return false;
1453
1601
  }
1454
1602
  /** Captures the current page URL once at mount for reuse across the session lifetime. */
@@ -1472,32 +1620,51 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1472
1620
  */
1473
1621
  async #fetchAndCacheOptions() {
1474
1622
  this.#paymentOptions = await this.#fetchPaymentOptions();
1475
- if (!this.#paymentOptions?.apple_pay) {
1623
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: payment options", this.#paymentOptions);
1624
+ if (!this.#paymentOptions?.options?.apple_pay) {
1625
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: apple_pay absent in options, hiding button");
1476
1626
  this.style.display = "none";
1627
+ this.querySelector(SEL_CONTAINER)?.remove();
1477
1628
  return true;
1478
1629
  }
1630
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: ok", this.#paymentOptions.options.apple_pay);
1479
1631
  return false;
1480
1632
  }
1481
1633
  /** Injects the button template and wires click and confirm event listeners. */
1482
1634
  #renderTemplate() {
1635
+ console.debug("[web-apple-pay] #renderTemplate: injecting button template");
1483
1636
  this.style.removeProperty("display");
1484
1637
  ensureApplePayLogoSymbol();
1485
1638
  const template = document.createElement("template");
1486
1639
  template.innerHTML = apple_pay_button_template_default;
1487
1640
  this.appendChild(template.content.cloneNode(true));
1488
- this.#updateDisabledState();
1489
- trackExpressWalletImpression(this.#offer.sku_id, {
1490
- entryPoint: this.#entryPoint,
1491
- country: this.#country
1492
- });
1493
1641
  const button = this.querySelector(SEL_BUTTON);
1642
+ button.classList.add("wap-loading");
1643
+ button.setAttribute("disabled", "");
1644
+ button.setAttribute("aria-disabled", "true");
1645
+ button.setAttribute("aria-busy", "true");
1494
1646
  button.addEventListener("click", () => {
1647
+ if (button.classList.contains("wap-loading")) return;
1495
1648
  this.#openModal();
1496
1649
  });
1497
- this.addEventListener("apple-pay-confirmed", (event) => {
1498
- const { skuId, country } = event.detail;
1499
- this.#initiateApplePay(skuId, country);
1500
- });
1650
+ this.#applePayConfirmedHandler = (event) => {
1651
+ const { skuId, country, recaptchaTokens } = event.detail;
1652
+ this.#initiateApplePay(skuId, country, recaptchaTokens);
1653
+ this.#applePayConfirmedHandler = null;
1654
+ };
1655
+ document.addEventListener("apple-pay-confirmed", this.#applePayConfirmedHandler, { once: true });
1656
+ }
1657
+ #finishLoading() {
1658
+ console.debug("[web-apple-pay] #finishLoading: all checks passed, enabling button");
1659
+ const button = this.querySelector(SEL_BUTTON);
1660
+ if (button) {
1661
+ button.classList.remove("wap-loading");
1662
+ this.#updateDisabledState();
1663
+ trackExpressWalletImpression(this.#offer.sku_id, {
1664
+ entryPoint: this.#entryPoint,
1665
+ country: this.#country
1666
+ });
1667
+ }
1501
1668
  }
1502
1669
  // ─── Private helpers ────────────────────────────────────────────────────────
1503
1670
  #updateDisabledState() {
@@ -1506,13 +1673,19 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1506
1673
  if (this.#offer) {
1507
1674
  button.removeAttribute("disabled");
1508
1675
  button.removeAttribute("aria-disabled");
1676
+ button.removeAttribute("aria-busy");
1509
1677
  return;
1510
1678
  }
1511
1679
  button.setAttribute("disabled", "");
1512
1680
  button.setAttribute("aria-disabled", "true");
1513
1681
  }
1514
1682
  #openModal() {
1515
- if (!this.#offer || document.querySelector(ApplePayModal.tag)) return;
1683
+ if (!this.#offer) {
1684
+ return;
1685
+ }
1686
+ if (document.querySelector(ApplePayModal.tag)) {
1687
+ return;
1688
+ }
1516
1689
  trackExpressWalletClick(this.#offer.sku_id);
1517
1690
  ApplePayModal.define();
1518
1691
  const modal = document.createElement(ApplePayModal.tag);
@@ -1548,7 +1721,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1548
1721
  return {
1549
1722
  countryCode: country,
1550
1723
  currencyCode: offer.price?.currency_code ?? DEFAULT_CURRENCY,
1551
- supportedNetworks: paymentOptions.apple_pay.settings.supported_networks,
1724
+ supportedNetworks: paymentOptions.options.apple_pay.settings.supported_networks,
1552
1725
  totalAmount,
1553
1726
  totalLabel: TOTAL_LABEL,
1554
1727
  skuId,
@@ -1565,11 +1738,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1565
1738
  })
1566
1739
  };
1567
1740
  }
1568
- async #initiateApplePay(skuId, country) {
1741
+ #initiateApplePay(skuId, country, recaptchaTokens) {
1569
1742
  try {
1570
1743
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
1571
1744
  const paymentOptions = this.#paymentOptions;
1572
- const recaptchaTokens = await getPurchaseRecaptchaTokens();
1573
1745
  startApplePaySession(
1574
1746
  this.#buildSessionParams(
1575
1747
  skuId,