@economist/web-apple-pay 1.7.9 → 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
@@ -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,7 +2031,6 @@ 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 ────────────────────────────────────────────────────────
@@ -2140,7 +2133,6 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2140
2133
  if (button) {
2141
2134
  button.classList.remove("wap-loading");
2142
2135
  this.#updateDisabledState();
2143
- this.#scheduleRecaptchaTokens();
2144
2136
  trackExpressWalletImpression(this.#offer.sku_id, {
2145
2137
  entryPoint: this.#entryPoint,
2146
2138
  country: this.#country
@@ -2177,27 +2169,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2177
2169
  }
2178
2170
  const skuId = this.#offer.sku_id;
2179
2171
  const country = this.#country;
2180
- modal.onConfirm = (recaptchaTokens) => {
2181
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2182
- this.#scheduleRecaptchaTokens();
2172
+ modal.onConfirm = (recaptchaTokensPromise) => {
2173
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2183
2174
  };
2184
- modal.open(this.#recaptchaTokensPromise);
2185
- }
2186
- /**
2187
- * Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
2188
- * If the fetch rejects, the field is cleared so the next `modal.open()` call
2189
- * falls back to a new attempt via the modal's own null-fallback path — preventing
2190
- * a single transient failure from permanently disabling the CTA on all retries.
2191
- * The identity guard prevents a late rejection from clobbering a newer attempt.
2192
- */
2193
- #scheduleRecaptchaTokens() {
2194
- const attempt = getPurchaseRecaptchaTokens();
2195
- attempt.catch(() => {
2196
- if (this.#recaptchaTokensPromise === attempt) {
2197
- this.#recaptchaTokensPromise = null;
2198
- }
2199
- });
2200
- this.#recaptchaTokensPromise = attempt;
2175
+ modal.open();
2201
2176
  }
2202
2177
  async #fetchSession() {
2203
2178
  try {
@@ -2218,7 +2193,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2218
2193
  return null;
2219
2194
  }
2220
2195
  }
2221
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2196
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2222
2197
  const offer = this.#offer;
2223
2198
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2224
2199
  return {
@@ -2230,30 +2205,41 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2230
2205
  skuId,
2231
2206
  returnUrl: this.#returnUrl,
2232
2207
  userLoggedIn: sessionData.loggedIn,
2233
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2234
- skuId,
2235
- country,
2236
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2237
- returnUrl: this.#returnUrl,
2238
- supportedBillingCountries: this.#supportedBillingCountries,
2239
- recaptchaTokens
2240
- })
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
+ }
2241
2219
  };
2242
2220
  }
2243
- #initiateApplePay(skuId, country, recaptchaTokens) {
2221
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2244
2222
  try {
2245
2223
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2246
2224
  const paymentOptions = this.#paymentOptions;
2225
+ if (!paymentOptions) {
2226
+ return;
2227
+ }
2247
2228
  startApplePaySession(
2248
2229
  this.#buildSessionParams(
2249
2230
  skuId,
2250
2231
  country,
2251
2232
  sessionData,
2252
2233
  paymentOptions,
2253
- recaptchaTokens
2234
+ recaptchaTokensPromise
2254
2235
  )
2255
2236
  );
2256
2237
  } catch {
2238
+ redirectToFallbackCheckout({
2239
+ returnUrl: this.#returnUrl,
2240
+ skuId,
2241
+ countryCode: country
2242
+ });
2257
2243
  }
2258
2244
  }
2259
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