@economist/web-apple-pay 1.7.8 → 1.7.10

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.
@@ -578,10 +578,9 @@ var loadRecaptchaScript = (siteKey) => {
578
578
  );
579
579
  if (existingScript) {
580
580
  existingScript.addEventListener("load", () => resolve());
581
- existingScript.addEventListener(
582
- "error",
583
- () => reject(new Error("Failed to load existing reCAPTCHA script"))
584
- );
581
+ existingScript.addEventListener("error", () => {
582
+ reject(new Error("Failed to load existing reCAPTCHA script"));
583
+ });
585
584
  return;
586
585
  }
587
586
  const script = document.createElement("script");
@@ -589,7 +588,9 @@ var loadRecaptchaScript = (siteKey) => {
589
588
  script.async = true;
590
589
  script.defer = true;
591
590
  script.onload = () => resolve();
592
- script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
591
+ script.onerror = () => {
592
+ reject(new Error("Failed to load reCAPTCHA script"));
593
+ };
593
594
  document.head.appendChild(script);
594
595
  });
595
596
  };
@@ -838,17 +839,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
838
839
  #country = "";
839
840
  #triggerElement = null;
840
841
  #keyHandler = null;
841
- #recaptchaTokens = null;
842
- #recaptchaTokensPromise = null;
842
+ #isClosed = false;
843
843
  /**
844
- * Synchronous callback invoked directly from the CTA click handler once the
845
- * user confirms. The caller (ApplePayButton) must call startApplePaySession
846
- * inside this callback to preserve the browser's user-gesture chain — Safari
847
- * requires new ApplePaySession() to execute within the original click stack.
848
- *
849
- * reCAPTCHA tokens are guaranteed non-null here: the CTA remains disabled
850
- * until the token pre-fetch resolves, so this callback is never reached
851
- * without valid tokens.
844
+ * Callback invoked synchronously from the CTA click handler.
845
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
846
+ * by the time this is called. The caller (ApplePayButton) must call
847
+ * new ApplePaySession() synchronously inside this callback to stay within
848
+ * Safari's user-gesture window. The token promise resolves before
849
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
852
850
  */
853
851
  onConfirm = null;
854
852
  set offer(value) {
@@ -945,28 +943,32 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
945
943
  }
946
944
  });
947
945
  }
948
- if (this.#recaptchaTokensPromise) {
949
- if (cta) {
950
- cta.setAttribute("disabled", "");
951
- cta.setAttribute("aria-disabled", "true");
952
- cta.setAttribute("aria-busy", "true");
946
+ cta?.addEventListener("click", () => {
947
+ if (cta.disabled) return;
948
+ if (isUS && checkbox && !checkbox.checked) {
949
+ errorElement?.classList.add("apm__error--visible");
950
+ checkboxLabel?.classList.add("apm__checkbox-label--error");
951
+ return;
953
952
  }
954
- this.#recaptchaTokensPromise.then((tokens) => {
955
- this.#recaptchaTokens = tokens;
956
- if (cta) {
957
- cta.removeAttribute("disabled");
958
- cta.removeAttribute("aria-disabled");
959
- cta.removeAttribute("aria-busy");
960
- }
961
- }).catch(() => {
962
- if (cta) {
963
- cta.removeAttribute("aria-busy");
964
- }
953
+ errorElement?.classList.remove("apm__error--visible");
954
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
955
+ cta.disabled = true;
956
+ cta.setAttribute("aria-busy", "true");
957
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
958
+ if (!this.#isClosed && this.onConfirm) {
959
+ this.onConfirm(recaptchaTokensPromise);
960
+ this.close();
961
+ }
962
+ recaptchaTokensPromise.catch(() => {
963
+ if (this.#isClosed) return;
964
+ cta.disabled = false;
965
+ cta.removeAttribute("aria-busy");
965
966
  const existingError = this.querySelector("#apple-pay-modal-error");
966
967
  if (existingError) {
967
968
  const icon = existingError.querySelector("svg");
968
969
  existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
969
970
  if (icon) existingError.prepend(icon);
971
+ existingError.setAttribute("role", "alert");
970
972
  existingError.classList.add("apm__error--visible");
971
973
  } else {
972
974
  const errorNotice = document.createElement("div");
@@ -976,25 +978,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
976
978
  cta?.insertAdjacentElement("beforebegin", errorNotice);
977
979
  }
978
980
  });
979
- }
980
- cta?.addEventListener("click", () => {
981
- if (isUS && checkbox && !checkbox.checked) {
982
- errorElement?.classList.add("apm__error--visible");
983
- checkboxLabel?.classList.add("apm__checkbox-label--error");
984
- return;
985
- }
986
- if (this.#recaptchaTokens) {
987
- this.onConfirm?.(this.#recaptchaTokens);
988
- this.close();
989
- }
990
981
  });
991
982
  this.querySelector("#apple-pay-modal-close")?.addEventListener(
992
983
  "click",
993
984
  () => this.close()
994
985
  );
995
986
  }
996
- open(recaptchaTokensPromise) {
997
- this.#recaptchaTokensPromise = recaptchaTokensPromise ?? getPurchaseRecaptchaTokens();
987
+ open() {
988
+ this.#isClosed = false;
998
989
  this.#triggerElement = document.activeElement;
999
990
  document.body.appendChild(this);
1000
991
  this.#keyHandler = (event) => {
@@ -1009,6 +1000,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
1009
1000
  this.querySelector("#apple-pay-modal")?.focus();
1010
1001
  }
1011
1002
  close() {
1003
+ this.#isClosed = true;
1012
1004
  if (this.#keyHandler) {
1013
1005
  document.removeEventListener("keydown", this.#keyHandler, {
1014
1006
  capture: true
@@ -1123,7 +1115,7 @@ var isAppleDeviceOrSafari = () => {
1123
1115
  const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
1124
1116
  return isIOSOrMacTouchDevice || isSafariBrowser;
1125
1117
  };
1126
- var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
1118
+ var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null;
1127
1119
  var isApplePayAvailable = async () => {
1128
1120
  if (bypassApplePayChecks()) {
1129
1121
  return true;
@@ -1421,7 +1413,10 @@ function startApplePaySession(config) {
1421
1413
  };
1422
1414
  session.onpaymentauthorized = (event) => {
1423
1415
  trackWalletPaymentAuthorised(config.skuId);
1424
- config.onPaymentAuthorized(event, session);
1416
+ config.onPaymentAuthorized(event, session).catch(() => {
1417
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1418
+ redirectToFallbackCheckout(config);
1419
+ });
1425
1420
  };
1426
1421
  session.oncancel = () => {
1427
1422
  config.onCancel?.();
@@ -1972,7 +1967,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1972
1967
  #returnUrl = "";
1973
1968
  #supportedBillingCountries = [];
1974
1969
  #entryPoint = "";
1975
- #recaptchaTokensPromise = null;
1976
1970
  // ─── Public API ─────────────────────────────────────────────────────────────
1977
1971
  get offer() {
1978
1972
  return this.#offer;
@@ -2037,15 +2031,14 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2037
2031
  this.#paymentOptions = null;
2038
2032
  this.#returnUrl = "";
2039
2033
  this.#supportedBillingCountries = [];
2040
- this.#recaptchaTokensPromise = null;
2041
2034
  this.querySelector(SEL_CONTAINER)?.remove();
2042
2035
  }
2043
2036
  // ─── Render pipeline ────────────────────────────────────────────────────────
2044
2037
  async #run() {
2045
2038
  try {
2039
+ this.#renderTemplate();
2046
2040
  if (await this.#checkAvailability()) return;
2047
2041
  if (this.#checkOffer()) return;
2048
- this.#renderTemplate();
2049
2042
  if (await this.#checkSession()) return;
2050
2043
  this.#captureReturnUrl();
2051
2044
  this.#computeBillingCountries();
@@ -2063,6 +2056,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2063
2056
  const available = await isApplePayAvailable();
2064
2057
  if (!available) {
2065
2058
  this.style.display = "none";
2059
+ this.querySelector(SEL_CONTAINER)?.remove();
2066
2060
  return true;
2067
2061
  }
2068
2062
  return false;
@@ -2084,6 +2078,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2084
2078
  #checkOffer() {
2085
2079
  if (!this.#offer) {
2086
2080
  this.style.display = "none";
2081
+ this.querySelector(SEL_CONTAINER)?.remove();
2087
2082
  return true;
2088
2083
  }
2089
2084
  return false;
@@ -2138,7 +2133,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2138
2133
  if (button) {
2139
2134
  button.classList.remove("wap-loading");
2140
2135
  this.#updateDisabledState();
2141
- this.#scheduleRecaptchaTokens();
2142
2136
  trackExpressWalletImpression(this.#offer.sku_id, {
2143
2137
  entryPoint: this.#entryPoint,
2144
2138
  country: this.#country
@@ -2175,27 +2169,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2175
2169
  }
2176
2170
  const skuId = this.#offer.sku_id;
2177
2171
  const country = this.#country;
2178
- modal.onConfirm = (recaptchaTokens) => {
2179
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2180
- this.#scheduleRecaptchaTokens();
2172
+ modal.onConfirm = (recaptchaTokensPromise) => {
2173
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2181
2174
  };
2182
- modal.open(this.#recaptchaTokensPromise);
2183
- }
2184
- /**
2185
- * Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
2186
- * If the fetch rejects, the field is cleared so the next `modal.open()` call
2187
- * falls back to a new attempt via the modal's own null-fallback path — preventing
2188
- * a single transient failure from permanently disabling the CTA on all retries.
2189
- * The identity guard prevents a late rejection from clobbering a newer attempt.
2190
- */
2191
- #scheduleRecaptchaTokens() {
2192
- const attempt = getPurchaseRecaptchaTokens();
2193
- attempt.catch(() => {
2194
- if (this.#recaptchaTokensPromise === attempt) {
2195
- this.#recaptchaTokensPromise = null;
2196
- }
2197
- });
2198
- this.#recaptchaTokensPromise = attempt;
2175
+ modal.open();
2199
2176
  }
2200
2177
  async #fetchSession() {
2201
2178
  try {
@@ -2216,7 +2193,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2216
2193
  return null;
2217
2194
  }
2218
2195
  }
2219
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2196
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2220
2197
  const offer = this.#offer;
2221
2198
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2222
2199
  return {
@@ -2228,30 +2205,41 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2228
2205
  skuId,
2229
2206
  returnUrl: this.#returnUrl,
2230
2207
  userLoggedIn: sessionData.loggedIn,
2231
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2232
- skuId,
2233
- country,
2234
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2235
- returnUrl: this.#returnUrl,
2236
- supportedBillingCountries: this.#supportedBillingCountries,
2237
- recaptchaTokens
2238
- })
2208
+ onPaymentAuthorized: async (event, session) => {
2209
+ const recaptchaTokens = await recaptchaTokensPromise;
2210
+ return handlePaymentAuthorized(event, session, sessionData, {
2211
+ skuId,
2212
+ country,
2213
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2214
+ returnUrl: this.#returnUrl,
2215
+ supportedBillingCountries: this.#supportedBillingCountries,
2216
+ recaptchaTokens
2217
+ });
2218
+ }
2239
2219
  };
2240
2220
  }
2241
- #initiateApplePay(skuId, country, recaptchaTokens) {
2221
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2242
2222
  try {
2243
2223
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2244
2224
  const paymentOptions = this.#paymentOptions;
2225
+ if (!paymentOptions) {
2226
+ return;
2227
+ }
2245
2228
  startApplePaySession(
2246
2229
  this.#buildSessionParams(
2247
2230
  skuId,
2248
2231
  country,
2249
2232
  sessionData,
2250
2233
  paymentOptions,
2251
- recaptchaTokens
2234
+ recaptchaTokensPromise
2252
2235
  )
2253
2236
  );
2254
2237
  } catch {
2238
+ redirectToFallbackCheckout({
2239
+ returnUrl: this.#returnUrl,
2240
+ skuId,
2241
+ countryCode: country
2242
+ });
2255
2243
  }
2256
2244
  }
2257
2245
  };
@@ -1,27 +1,22 @@
1
1
  import type { Offer } from './types/offer.types';
2
- import { getPurchaseRecaptchaTokens } from './utils/recaptcha';
3
- /** Resolved reCAPTCHA token set, mirroring the recaptcha utility return type. */
4
- type RecaptchaTokens = Awaited<ReturnType<typeof getPurchaseRecaptchaTokens>>;
2
+ import { type RecaptchaTokens } from './utils/recaptcha';
5
3
  export declare class ApplePayModal extends HTMLElement {
6
4
  #private;
7
5
  static readonly tag = "teg-apple-pay-modal";
8
6
  static define(): void;
9
7
  /**
10
- * Synchronous callback invoked directly from the CTA click handler once the
11
- * user confirms. The caller (ApplePayButton) must call startApplePaySession
12
- * inside this callback to preserve the browser's user-gesture chain — Safari
13
- * requires new ApplePaySession() to execute within the original click stack.
14
- *
15
- * reCAPTCHA tokens are guaranteed non-null here: the CTA remains disabled
16
- * until the token pre-fetch resolves, so this callback is never reached
17
- * without valid tokens.
8
+ * Callback invoked synchronously from the CTA click handler.
9
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
10
+ * by the time this is called. The caller (ApplePayButton) must call
11
+ * new ApplePaySession() synchronously inside this callback to stay within
12
+ * Safari's user-gesture window. The token promise resolves before
13
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
18
14
  */
19
- onConfirm: ((recaptchaTokens: RecaptchaTokens) => void) | null;
15
+ onConfirm: ((recaptchaTokensPromise: Promise<RecaptchaTokens>) => void) | null;
20
16
  set offer(value: Offer | null);
21
17
  get country(): string;
22
18
  set country(value: string);
23
19
  connectedCallback(): void;
24
- open(recaptchaTokensPromise?: Promise<RecaptchaTokens> | null): void;
20
+ open(): void;
25
21
  close(): void;
26
22
  }
27
- export {};
@@ -570,10 +570,9 @@ var loadRecaptchaScript = (siteKey) => {
570
570
  );
571
571
  if (existingScript) {
572
572
  existingScript.addEventListener("load", () => resolve());
573
- existingScript.addEventListener(
574
- "error",
575
- () => reject(new Error("Failed to load existing reCAPTCHA script"))
576
- );
573
+ existingScript.addEventListener("error", () => {
574
+ reject(new Error("Failed to load existing reCAPTCHA script"));
575
+ });
577
576
  return;
578
577
  }
579
578
  const script = document.createElement("script");
@@ -581,7 +580,9 @@ var loadRecaptchaScript = (siteKey) => {
581
580
  script.async = true;
582
581
  script.defer = true;
583
582
  script.onload = () => resolve();
584
- script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
583
+ script.onerror = () => {
584
+ reject(new Error("Failed to load reCAPTCHA script"));
585
+ };
585
586
  document.head.appendChild(script);
586
587
  });
587
588
  };
@@ -830,17 +831,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
830
831
  #country = "";
831
832
  #triggerElement = null;
832
833
  #keyHandler = null;
833
- #recaptchaTokens = null;
834
- #recaptchaTokensPromise = null;
834
+ #isClosed = false;
835
835
  /**
836
- * Synchronous callback invoked directly from the CTA click handler once the
837
- * user confirms. The caller (ApplePayButton) must call startApplePaySession
838
- * inside this callback to preserve the browser's user-gesture chain — Safari
839
- * requires new ApplePaySession() to execute within the original click stack.
840
- *
841
- * reCAPTCHA tokens are guaranteed non-null here: the CTA remains disabled
842
- * until the token pre-fetch resolves, so this callback is never reached
843
- * without valid tokens.
836
+ * Callback invoked synchronously from the CTA click handler.
837
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
838
+ * by the time this is called. The caller (ApplePayButton) must call
839
+ * new ApplePaySession() synchronously inside this callback to stay within
840
+ * Safari's user-gesture window. The token promise resolves before
841
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
844
842
  */
845
843
  onConfirm = null;
846
844
  set offer(value) {
@@ -937,28 +935,32 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
937
935
  }
938
936
  });
939
937
  }
940
- if (this.#recaptchaTokensPromise) {
941
- if (cta) {
942
- cta.setAttribute("disabled", "");
943
- cta.setAttribute("aria-disabled", "true");
944
- cta.setAttribute("aria-busy", "true");
938
+ cta?.addEventListener("click", () => {
939
+ if (cta.disabled) return;
940
+ if (isUS && checkbox && !checkbox.checked) {
941
+ errorElement?.classList.add("apm__error--visible");
942
+ checkboxLabel?.classList.add("apm__checkbox-label--error");
943
+ return;
945
944
  }
946
- this.#recaptchaTokensPromise.then((tokens) => {
947
- this.#recaptchaTokens = tokens;
948
- if (cta) {
949
- cta.removeAttribute("disabled");
950
- cta.removeAttribute("aria-disabled");
951
- cta.removeAttribute("aria-busy");
952
- }
953
- }).catch(() => {
954
- if (cta) {
955
- cta.removeAttribute("aria-busy");
956
- }
945
+ errorElement?.classList.remove("apm__error--visible");
946
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
947
+ cta.disabled = true;
948
+ cta.setAttribute("aria-busy", "true");
949
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
950
+ if (!this.#isClosed && this.onConfirm) {
951
+ this.onConfirm(recaptchaTokensPromise);
952
+ this.close();
953
+ }
954
+ recaptchaTokensPromise.catch(() => {
955
+ if (this.#isClosed) return;
956
+ cta.disabled = false;
957
+ cta.removeAttribute("aria-busy");
957
958
  const existingError = this.querySelector("#apple-pay-modal-error");
958
959
  if (existingError) {
959
960
  const icon = existingError.querySelector("svg");
960
961
  existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
961
962
  if (icon) existingError.prepend(icon);
963
+ existingError.setAttribute("role", "alert");
962
964
  existingError.classList.add("apm__error--visible");
963
965
  } else {
964
966
  const errorNotice = document.createElement("div");
@@ -968,25 +970,14 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
968
970
  cta?.insertAdjacentElement("beforebegin", errorNotice);
969
971
  }
970
972
  });
971
- }
972
- cta?.addEventListener("click", () => {
973
- if (isUS && checkbox && !checkbox.checked) {
974
- errorElement?.classList.add("apm__error--visible");
975
- checkboxLabel?.classList.add("apm__checkbox-label--error");
976
- return;
977
- }
978
- if (this.#recaptchaTokens) {
979
- this.onConfirm?.(this.#recaptchaTokens);
980
- this.close();
981
- }
982
973
  });
983
974
  this.querySelector("#apple-pay-modal-close")?.addEventListener(
984
975
  "click",
985
976
  () => this.close()
986
977
  );
987
978
  }
988
- open(recaptchaTokensPromise) {
989
- this.#recaptchaTokensPromise = recaptchaTokensPromise ?? getPurchaseRecaptchaTokens();
979
+ open() {
980
+ this.#isClosed = false;
990
981
  this.#triggerElement = document.activeElement;
991
982
  document.body.appendChild(this);
992
983
  this.#keyHandler = (event) => {
@@ -1001,6 +992,7 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
1001
992
  this.querySelector("#apple-pay-modal")?.focus();
1002
993
  }
1003
994
  close() {
995
+ this.#isClosed = true;
1004
996
  if (this.#keyHandler) {
1005
997
  document.removeEventListener("keydown", this.#keyHandler, {
1006
998
  capture: true