@economist/web-apple-pay 0.1.3-beta.45 → 0.1.3-beta.46

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
@@ -849,10 +849,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
849
849
  #keyHandler = null;
850
850
  #isClosed = false;
851
851
  /**
852
- * Callback invoked from the CTA click handler once the user confirms.
853
- * The caller (ApplePayButton) must call startApplePaySession inside this
854
- * callback to preserve the browser's user-gesture chain — Safari requires
855
- * new ApplePaySession() to execute within the original click stack.
852
+ * Callback invoked synchronously from the CTA click handler.
853
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
854
+ * by the time this is called. The caller (ApplePayButton) must call
855
+ * new ApplePaySession() synchronously inside this callback to stay within
856
+ * Safari's user-gesture window. The token promise resolves before
857
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
856
858
  */
857
859
  onConfirm = null;
858
860
  set offer(value) {
@@ -957,20 +959,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
957
959
  return;
958
960
  }
959
961
  errorElement?.classList.remove("apm__error--visible");
962
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
960
963
  cta.disabled = true;
961
964
  cta.setAttribute("aria-busy", "true");
962
965
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
963
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
964
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
965
- if (this.#isClosed) {
966
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
967
- return;
968
- }
969
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
970
- this.onConfirm?.(recaptchaTokens);
966
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
967
+ if (!this.#isClosed && this.onConfirm) {
968
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
969
+ this.onConfirm(recaptchaTokensPromise);
971
970
  this.close();
972
- }).catch((error) => {
971
+ }
972
+ recaptchaTokensPromise.catch((error) => {
973
973
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
974
+ if (this.#isClosed) return;
974
975
  cta.disabled = false;
975
976
  cta.removeAttribute("aria-busy");
976
977
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -1429,7 +1430,11 @@ function startApplePaySession(config) {
1429
1430
  };
1430
1431
  session.onpaymentauthorized = (event) => {
1431
1432
  trackWalletPaymentAuthorised(config.skuId);
1432
- config.onPaymentAuthorized(event, session);
1433
+ config.onPaymentAuthorized(event, session).catch((err) => {
1434
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
1435
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1436
+ redirectToFallbackCheckout(config);
1437
+ });
1433
1438
  };
1434
1439
  session.oncancel = () => {
1435
1440
  config.onCancel?.();
@@ -2204,9 +2209,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2204
2209
  }
2205
2210
  const skuId = this.#offer.sku_id;
2206
2211
  const country = this.#country;
2207
- modal.onConfirm = (recaptchaTokens) => {
2212
+ modal.onConfirm = (recaptchaTokensPromise) => {
2208
2213
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2209
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2214
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2210
2215
  };
2211
2216
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2212
2217
  modal.open();
@@ -2230,7 +2235,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2230
2235
  return null;
2231
2236
  }
2232
2237
  }
2233
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2238
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2234
2239
  const offer = this.#offer;
2235
2240
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2236
2241
  return {
@@ -2242,31 +2247,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2242
2247
  skuId,
2243
2248
  returnUrl: this.#returnUrl,
2244
2249
  userLoggedIn: sessionData.loggedIn,
2245
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2246
- skuId,
2247
- country,
2248
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2249
- returnUrl: this.#returnUrl,
2250
- supportedBillingCountries: this.#supportedBillingCountries,
2251
- recaptchaTokens
2252
- })
2250
+ onPaymentAuthorized: async (event, session) => {
2251
+ const recaptchaTokens = await recaptchaTokensPromise;
2252
+ return handlePaymentAuthorized(event, session, sessionData, {
2253
+ skuId,
2254
+ country,
2255
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2256
+ returnUrl: this.#returnUrl,
2257
+ supportedBillingCountries: this.#supportedBillingCountries,
2258
+ recaptchaTokens
2259
+ });
2260
+ }
2253
2261
  };
2254
2262
  }
2255
- #initiateApplePay(skuId, country, recaptchaTokens) {
2263
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2256
2264
  try {
2257
2265
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2258
2266
  const paymentOptions = this.#paymentOptions;
2267
+ if (!paymentOptions) {
2268
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2269
+ return;
2270
+ }
2259
2271
  console.info("[web-apple-pay] Starting Apple Pay session", {
2260
2272
  skuId,
2261
2273
  country,
2262
2274
  loggedIn: sessionData.loggedIn,
2263
2275
  userType: sessionData.userType,
2264
- hasPaymentOptions: !!paymentOptions,
2265
- recaptchaTokensPresent: {
2266
- checkEligibility: !!recaptchaTokens.checkEligibility,
2267
- newBasket: !!recaptchaTokens.newBasket,
2268
- registerUser: !!recaptchaTokens.registerUser
2269
- }
2276
+ hasPaymentOptions: true
2270
2277
  });
2271
2278
  startApplePaySession(
2272
2279
  this.#buildSessionParams(
@@ -2274,7 +2281,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2274
2281
  country,
2275
2282
  sessionData,
2276
2283
  paymentOptions,
2277
- recaptchaTokens
2284
+ recaptchaTokensPromise
2278
2285
  )
2279
2286
  );
2280
2287
  } catch (error) {
@@ -844,10 +844,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
844
844
  #keyHandler = null;
845
845
  #isClosed = false;
846
846
  /**
847
- * Callback invoked from the CTA click handler once the user confirms.
848
- * The caller (ApplePayButton) must call startApplePaySession inside this
849
- * callback to preserve the browser's user-gesture chain — Safari requires
850
- * new ApplePaySession() to execute within the original click stack.
847
+ * Callback invoked synchronously from the CTA click handler.
848
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
849
+ * by the time this is called. The caller (ApplePayButton) must call
850
+ * new ApplePaySession() synchronously inside this callback to stay within
851
+ * Safari's user-gesture window. The token promise resolves before
852
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
851
853
  */
852
854
  onConfirm = null;
853
855
  set offer(value) {
@@ -952,20 +954,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
952
954
  return;
953
955
  }
954
956
  errorElement?.classList.remove("apm__error--visible");
957
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
955
958
  cta.disabled = true;
956
959
  cta.setAttribute("aria-busy", "true");
957
960
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
958
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
959
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
960
- if (this.#isClosed) {
961
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
962
- return;
963
- }
964
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
965
- this.onConfirm?.(recaptchaTokens);
961
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
962
+ if (!this.#isClosed && this.onConfirm) {
963
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
964
+ this.onConfirm(recaptchaTokensPromise);
966
965
  this.close();
967
- }).catch((error) => {
966
+ }
967
+ recaptchaTokensPromise.catch((error) => {
968
968
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
969
+ if (this.#isClosed) return;
969
970
  cta.disabled = false;
970
971
  cta.removeAttribute("aria-busy");
971
972
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -1424,7 +1425,11 @@ function startApplePaySession(config) {
1424
1425
  };
1425
1426
  session.onpaymentauthorized = (event) => {
1426
1427
  trackWalletPaymentAuthorised(config.skuId);
1427
- config.onPaymentAuthorized(event, session);
1428
+ config.onPaymentAuthorized(event, session).catch((err) => {
1429
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
1430
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1431
+ redirectToFallbackCheckout(config);
1432
+ });
1428
1433
  };
1429
1434
  session.oncancel = () => {
1430
1435
  config.onCancel?.();
@@ -2199,9 +2204,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2199
2204
  }
2200
2205
  const skuId = this.#offer.sku_id;
2201
2206
  const country = this.#country;
2202
- modal.onConfirm = (recaptchaTokens) => {
2207
+ modal.onConfirm = (recaptchaTokensPromise) => {
2203
2208
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2204
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2209
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2205
2210
  };
2206
2211
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2207
2212
  modal.open();
@@ -2225,7 +2230,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2225
2230
  return null;
2226
2231
  }
2227
2232
  }
2228
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2233
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2229
2234
  const offer = this.#offer;
2230
2235
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2231
2236
  return {
@@ -2237,31 +2242,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2237
2242
  skuId,
2238
2243
  returnUrl: this.#returnUrl,
2239
2244
  userLoggedIn: sessionData.loggedIn,
2240
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2241
- skuId,
2242
- country,
2243
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2244
- returnUrl: this.#returnUrl,
2245
- supportedBillingCountries: this.#supportedBillingCountries,
2246
- recaptchaTokens
2247
- })
2245
+ onPaymentAuthorized: async (event, session) => {
2246
+ const recaptchaTokens = await recaptchaTokensPromise;
2247
+ return handlePaymentAuthorized(event, session, sessionData, {
2248
+ skuId,
2249
+ country,
2250
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2251
+ returnUrl: this.#returnUrl,
2252
+ supportedBillingCountries: this.#supportedBillingCountries,
2253
+ recaptchaTokens
2254
+ });
2255
+ }
2248
2256
  };
2249
2257
  }
2250
- #initiateApplePay(skuId, country, recaptchaTokens) {
2258
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2251
2259
  try {
2252
2260
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2253
2261
  const paymentOptions = this.#paymentOptions;
2262
+ if (!paymentOptions) {
2263
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2264
+ return;
2265
+ }
2254
2266
  console.info("[web-apple-pay] Starting Apple Pay session", {
2255
2267
  skuId,
2256
2268
  country,
2257
2269
  loggedIn: sessionData.loggedIn,
2258
2270
  userType: sessionData.userType,
2259
- hasPaymentOptions: !!paymentOptions,
2260
- recaptchaTokensPresent: {
2261
- checkEligibility: !!recaptchaTokens.checkEligibility,
2262
- newBasket: !!recaptchaTokens.newBasket,
2263
- registerUser: !!recaptchaTokens.registerUser
2264
- }
2271
+ hasPaymentOptions: true
2265
2272
  });
2266
2273
  startApplePaySession(
2267
2274
  this.#buildSessionParams(
@@ -2269,7 +2276,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2269
2276
  country,
2270
2277
  sessionData,
2271
2278
  paymentOptions,
2272
- recaptchaTokens
2279
+ recaptchaTokensPromise
2273
2280
  )
2274
2281
  );
2275
2282
  } catch (error) {
@@ -852,10 +852,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
852
852
  #keyHandler = null;
853
853
  #isClosed = false;
854
854
  /**
855
- * Callback invoked from the CTA click handler once the user confirms.
856
- * The caller (ApplePayButton) must call startApplePaySession inside this
857
- * callback to preserve the browser's user-gesture chain — Safari requires
858
- * new ApplePaySession() to execute within the original click stack.
855
+ * Callback invoked synchronously from the CTA click handler.
856
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
857
+ * by the time this is called. The caller (ApplePayButton) must call
858
+ * new ApplePaySession() synchronously inside this callback to stay within
859
+ * Safari's user-gesture window. The token promise resolves before
860
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
859
861
  */
860
862
  onConfirm = null;
861
863
  set offer(value) {
@@ -960,20 +962,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
960
962
  return;
961
963
  }
962
964
  errorElement?.classList.remove("apm__error--visible");
965
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
963
966
  cta.disabled = true;
964
967
  cta.setAttribute("aria-busy", "true");
965
968
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
966
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
967
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
968
- if (this.#isClosed) {
969
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
970
- return;
971
- }
972
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
973
- this.onConfirm?.(recaptchaTokens);
969
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
970
+ if (!this.#isClosed && this.onConfirm) {
971
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
972
+ this.onConfirm(recaptchaTokensPromise);
974
973
  this.close();
975
- }).catch((error) => {
974
+ }
975
+ recaptchaTokensPromise.catch((error) => {
976
976
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
977
+ if (this.#isClosed) return;
977
978
  cta.disabled = false;
978
979
  cta.removeAttribute("aria-busy");
979
980
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -1432,7 +1433,11 @@ function startApplePaySession(config) {
1432
1433
  };
1433
1434
  session.onpaymentauthorized = (event) => {
1434
1435
  trackWalletPaymentAuthorised(config.skuId);
1435
- config.onPaymentAuthorized(event, session);
1436
+ config.onPaymentAuthorized(event, session).catch((err) => {
1437
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
1438
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1439
+ redirectToFallbackCheckout(config);
1440
+ });
1436
1441
  };
1437
1442
  session.oncancel = () => {
1438
1443
  config.onCancel?.();
@@ -2207,9 +2212,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2207
2212
  }
2208
2213
  const skuId = this.#offer.sku_id;
2209
2214
  const country = this.#country;
2210
- modal.onConfirm = (recaptchaTokens) => {
2215
+ modal.onConfirm = (recaptchaTokensPromise) => {
2211
2216
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2212
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2217
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2213
2218
  };
2214
2219
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2215
2220
  modal.open();
@@ -2233,7 +2238,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2233
2238
  return null;
2234
2239
  }
2235
2240
  }
2236
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2241
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2237
2242
  const offer = this.#offer;
2238
2243
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2239
2244
  return {
@@ -2245,31 +2250,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2245
2250
  skuId,
2246
2251
  returnUrl: this.#returnUrl,
2247
2252
  userLoggedIn: sessionData.loggedIn,
2248
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2249
- skuId,
2250
- country,
2251
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2252
- returnUrl: this.#returnUrl,
2253
- supportedBillingCountries: this.#supportedBillingCountries,
2254
- recaptchaTokens
2255
- })
2253
+ onPaymentAuthorized: async (event, session) => {
2254
+ const recaptchaTokens = await recaptchaTokensPromise;
2255
+ return handlePaymentAuthorized(event, session, sessionData, {
2256
+ skuId,
2257
+ country,
2258
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2259
+ returnUrl: this.#returnUrl,
2260
+ supportedBillingCountries: this.#supportedBillingCountries,
2261
+ recaptchaTokens
2262
+ });
2263
+ }
2256
2264
  };
2257
2265
  }
2258
- #initiateApplePay(skuId, country, recaptchaTokens) {
2266
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2259
2267
  try {
2260
2268
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2261
2269
  const paymentOptions = this.#paymentOptions;
2270
+ if (!paymentOptions) {
2271
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2272
+ return;
2273
+ }
2262
2274
  console.info("[web-apple-pay] Starting Apple Pay session", {
2263
2275
  skuId,
2264
2276
  country,
2265
2277
  loggedIn: sessionData.loggedIn,
2266
2278
  userType: sessionData.userType,
2267
- hasPaymentOptions: !!paymentOptions,
2268
- recaptchaTokensPresent: {
2269
- checkEligibility: !!recaptchaTokens.checkEligibility,
2270
- newBasket: !!recaptchaTokens.newBasket,
2271
- registerUser: !!recaptchaTokens.registerUser
2272
- }
2279
+ hasPaymentOptions: true
2273
2280
  });
2274
2281
  startApplePaySession(
2275
2282
  this.#buildSessionParams(
@@ -2277,7 +2284,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2277
2284
  country,
2278
2285
  sessionData,
2279
2286
  paymentOptions,
2280
- recaptchaTokens
2287
+ recaptchaTokensPromise
2281
2288
  )
2282
2289
  );
2283
2290
  } catch (error) {
@@ -7,12 +7,14 @@ export declare class ApplePayModal extends HTMLElement {
7
7
  static readonly tag = "teg-apple-pay-modal";
8
8
  static define(): void;
9
9
  /**
10
- * Callback invoked from the CTA click handler once the user confirms.
11
- * The caller (ApplePayButton) must call startApplePaySession inside this
12
- * callback to preserve the browser's user-gesture chain — Safari requires
13
- * new ApplePaySession() to execute within the original click stack.
10
+ * Callback invoked synchronously from the CTA click handler.
11
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
12
+ * by the time this is called. The caller (ApplePayButton) must call
13
+ * new ApplePaySession() synchronously inside this callback to stay within
14
+ * Safari's user-gesture window. The token promise resolves before
15
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
14
16
  */
15
- onConfirm: ((recaptchaTokens: RecaptchaTokens) => void) | null;
17
+ onConfirm: ((recaptchaTokensPromise: Promise<RecaptchaTokens>) => void) | null;
16
18
  set offer(value: Offer | null);
17
19
  get country(): string;
18
20
  set country(value: string);
@@ -844,10 +844,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
844
844
  #keyHandler = null;
845
845
  #isClosed = false;
846
846
  /**
847
- * Callback invoked from the CTA click handler once the user confirms.
848
- * The caller (ApplePayButton) must call startApplePaySession inside this
849
- * callback to preserve the browser's user-gesture chain — Safari requires
850
- * new ApplePaySession() to execute within the original click stack.
847
+ * Callback invoked synchronously from the CTA click handler.
848
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
849
+ * by the time this is called. The caller (ApplePayButton) must call
850
+ * new ApplePaySession() synchronously inside this callback to stay within
851
+ * Safari's user-gesture window. The token promise resolves before
852
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
851
853
  */
852
854
  onConfirm = null;
853
855
  set offer(value) {
@@ -952,20 +954,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
952
954
  return;
953
955
  }
954
956
  errorElement?.classList.remove("apm__error--visible");
957
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
955
958
  cta.disabled = true;
956
959
  cta.setAttribute("aria-busy", "true");
957
960
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
958
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
959
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
960
- if (this.#isClosed) {
961
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
962
- return;
963
- }
964
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
965
- this.onConfirm?.(recaptchaTokens);
961
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
962
+ if (!this.#isClosed && this.onConfirm) {
963
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
964
+ this.onConfirm(recaptchaTokensPromise);
966
965
  this.close();
967
- }).catch((error) => {
966
+ }
967
+ recaptchaTokensPromise.catch((error) => {
968
968
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
969
+ if (this.#isClosed) return;
969
970
  cta.disabled = false;
970
971
  cta.removeAttribute("aria-busy");
971
972
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -844,10 +844,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
844
844
  #keyHandler = null;
845
845
  #isClosed = false;
846
846
  /**
847
- * Callback invoked from the CTA click handler once the user confirms.
848
- * The caller (ApplePayButton) must call startApplePaySession inside this
849
- * callback to preserve the browser's user-gesture chain — Safari requires
850
- * new ApplePaySession() to execute within the original click stack.
847
+ * Callback invoked synchronously from the CTA click handler.
848
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
849
+ * by the time this is called. The caller (ApplePayButton) must call
850
+ * new ApplePaySession() synchronously inside this callback to stay within
851
+ * Safari's user-gesture window. The token promise resolves before
852
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
851
853
  */
852
854
  onConfirm = null;
853
855
  set offer(value) {
@@ -952,20 +954,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
952
954
  return;
953
955
  }
954
956
  errorElement?.classList.remove("apm__error--visible");
957
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
955
958
  cta.disabled = true;
956
959
  cta.setAttribute("aria-busy", "true");
957
960
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
958
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
959
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
960
- if (this.#isClosed) {
961
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
962
- return;
963
- }
964
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
965
- this.onConfirm?.(recaptchaTokens);
961
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
962
+ if (!this.#isClosed && this.onConfirm) {
963
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
964
+ this.onConfirm(recaptchaTokensPromise);
966
965
  this.close();
967
- }).catch((error) => {
966
+ }
967
+ recaptchaTokensPromise.catch((error) => {
968
968
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
969
+ if (this.#isClosed) return;
969
970
  cta.disabled = false;
970
971
  cta.removeAttribute("aria-busy");
971
972
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -1424,7 +1425,11 @@ function startApplePaySession(config) {
1424
1425
  };
1425
1426
  session.onpaymentauthorized = (event) => {
1426
1427
  trackWalletPaymentAuthorised(config.skuId);
1427
- config.onPaymentAuthorized(event, session);
1428
+ config.onPaymentAuthorized(event, session).catch((err) => {
1429
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
1430
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1431
+ redirectToFallbackCheckout(config);
1432
+ });
1428
1433
  };
1429
1434
  session.oncancel = () => {
1430
1435
  config.onCancel?.();
@@ -2199,9 +2204,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2199
2204
  }
2200
2205
  const skuId = this.#offer.sku_id;
2201
2206
  const country = this.#country;
2202
- modal.onConfirm = (recaptchaTokens) => {
2207
+ modal.onConfirm = (recaptchaTokensPromise) => {
2203
2208
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2204
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2209
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2205
2210
  };
2206
2211
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2207
2212
  modal.open();
@@ -2225,7 +2230,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2225
2230
  return null;
2226
2231
  }
2227
2232
  }
2228
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2233
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2229
2234
  const offer = this.#offer;
2230
2235
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2231
2236
  return {
@@ -2237,31 +2242,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2237
2242
  skuId,
2238
2243
  returnUrl: this.#returnUrl,
2239
2244
  userLoggedIn: sessionData.loggedIn,
2240
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2241
- skuId,
2242
- country,
2243
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2244
- returnUrl: this.#returnUrl,
2245
- supportedBillingCountries: this.#supportedBillingCountries,
2246
- recaptchaTokens
2247
- })
2245
+ onPaymentAuthorized: async (event, session) => {
2246
+ const recaptchaTokens = await recaptchaTokensPromise;
2247
+ return handlePaymentAuthorized(event, session, sessionData, {
2248
+ skuId,
2249
+ country,
2250
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2251
+ returnUrl: this.#returnUrl,
2252
+ supportedBillingCountries: this.#supportedBillingCountries,
2253
+ recaptchaTokens
2254
+ });
2255
+ }
2248
2256
  };
2249
2257
  }
2250
- #initiateApplePay(skuId, country, recaptchaTokens) {
2258
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2251
2259
  try {
2252
2260
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2253
2261
  const paymentOptions = this.#paymentOptions;
2262
+ if (!paymentOptions) {
2263
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2264
+ return;
2265
+ }
2254
2266
  console.info("[web-apple-pay] Starting Apple Pay session", {
2255
2267
  skuId,
2256
2268
  country,
2257
2269
  loggedIn: sessionData.loggedIn,
2258
2270
  userType: sessionData.userType,
2259
- hasPaymentOptions: !!paymentOptions,
2260
- recaptchaTokensPresent: {
2261
- checkEligibility: !!recaptchaTokens.checkEligibility,
2262
- newBasket: !!recaptchaTokens.newBasket,
2263
- registerUser: !!recaptchaTokens.registerUser
2264
- }
2271
+ hasPaymentOptions: true
2265
2272
  });
2266
2273
  startApplePaySession(
2267
2274
  this.#buildSessionParams(
@@ -2269,7 +2276,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2269
2276
  country,
2270
2277
  sessionData,
2271
2278
  paymentOptions,
2272
- recaptchaTokens
2279
+ recaptchaTokensPromise
2273
2280
  )
2274
2281
  );
2275
2282
  } catch (error) {
@@ -249,7 +249,11 @@ function startApplePaySession(config) {
249
249
  };
250
250
  session.onpaymentauthorized = (event) => {
251
251
  trackWalletPaymentAuthorised(config.skuId);
252
- config.onPaymentAuthorized(event, session);
252
+ config.onPaymentAuthorized(event, session).catch((err) => {
253
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
254
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
255
+ redirectToFallbackCheckout(config);
256
+ });
253
257
  };
254
258
  session.oncancel = () => {
255
259
  config.onCancel?.();
package/dist/src/index.js CHANGED
@@ -849,10 +849,12 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
849
849
  #keyHandler = null;
850
850
  #isClosed = false;
851
851
  /**
852
- * Callback invoked from the CTA click handler once the user confirms.
853
- * The caller (ApplePayButton) must call startApplePaySession inside this
854
- * callback to preserve the browser's user-gesture chain — Safari requires
855
- * new ApplePaySession() to execute within the original click stack.
852
+ * Callback invoked synchronously from the CTA click handler.
853
+ * Receives a Promise of reCAPTCHA tokens — the promise is already in-flight
854
+ * by the time this is called. The caller (ApplePayButton) must call
855
+ * new ApplePaySession() synchronously inside this callback to stay within
856
+ * Safari's user-gesture window. The token promise resolves before
857
+ * onPaymentAuthorized fires (user still needs to authenticate on the sheet).
856
858
  */
857
859
  onConfirm = null;
858
860
  set offer(value) {
@@ -957,20 +959,19 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
957
959
  return;
958
960
  }
959
961
  errorElement?.classList.remove("apm__error--visible");
962
+ this.querySelector('[role="alert"]:not(#apple-pay-modal-error)')?.remove();
960
963
  cta.disabled = true;
961
964
  cta.setAttribute("aria-busy", "true");
962
965
  console.info("[web-apple-pay] CTA clicked \u2014 fetching reCAPTCHA tokens");
963
- getPurchaseRecaptchaTokens().then((recaptchaTokens) => {
964
- console.info("[web-apple-pay] reCAPTCHA tokens resolved successfully");
965
- if (this.#isClosed) {
966
- console.warn("[web-apple-pay] Modal was closed before reCAPTCHA resolved \u2014 aborting Apple Pay initiation");
967
- return;
968
- }
969
- console.info("[web-apple-pay] Calling onConfirm to initiate Apple Pay session");
970
- this.onConfirm?.(recaptchaTokens);
966
+ const recaptchaTokensPromise = getPurchaseRecaptchaTokens();
967
+ if (!this.#isClosed && this.onConfirm) {
968
+ console.info("[web-apple-pay] Calling onConfirm synchronously to initiate Apple Pay session");
969
+ this.onConfirm(recaptchaTokensPromise);
971
970
  this.close();
972
- }).catch((error) => {
971
+ }
972
+ recaptchaTokensPromise.catch((error) => {
973
973
  console.error("[web-apple-pay] reCAPTCHA token fetch failed", error);
974
+ if (this.#isClosed) return;
974
975
  cta.disabled = false;
975
976
  cta.removeAttribute("aria-busy");
976
977
  const existingError = this.querySelector("#apple-pay-modal-error");
@@ -1429,7 +1430,11 @@ function startApplePaySession(config) {
1429
1430
  };
1430
1431
  session.onpaymentauthorized = (event) => {
1431
1432
  trackWalletPaymentAuthorised(config.skuId);
1432
- config.onPaymentAuthorized(event, session);
1433
+ config.onPaymentAuthorized(event, session).catch((err) => {
1434
+ console.error("[web-apple-pay] onPaymentAuthorized threw an unhandled error \u2014 aborting session", err);
1435
+ session.completePayment({ status: ApplePaySession.STATUS_FAILURE });
1436
+ redirectToFallbackCheckout(config);
1437
+ });
1433
1438
  };
1434
1439
  session.oncancel = () => {
1435
1440
  config.onCancel?.();
@@ -2204,9 +2209,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2204
2209
  }
2205
2210
  const skuId = this.#offer.sku_id;
2206
2211
  const country = this.#country;
2207
- modal.onConfirm = (recaptchaTokens) => {
2212
+ modal.onConfirm = (recaptchaTokensPromise) => {
2208
2213
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2209
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2214
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2210
2215
  };
2211
2216
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2212
2217
  modal.open();
@@ -2230,7 +2235,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2230
2235
  return null;
2231
2236
  }
2232
2237
  }
2233
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2238
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2234
2239
  const offer = this.#offer;
2235
2240
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2236
2241
  return {
@@ -2242,31 +2247,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2242
2247
  skuId,
2243
2248
  returnUrl: this.#returnUrl,
2244
2249
  userLoggedIn: sessionData.loggedIn,
2245
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2246
- skuId,
2247
- country,
2248
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2249
- returnUrl: this.#returnUrl,
2250
- supportedBillingCountries: this.#supportedBillingCountries,
2251
- recaptchaTokens
2252
- })
2250
+ onPaymentAuthorized: async (event, session) => {
2251
+ const recaptchaTokens = await recaptchaTokensPromise;
2252
+ return handlePaymentAuthorized(event, session, sessionData, {
2253
+ skuId,
2254
+ country,
2255
+ currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2256
+ returnUrl: this.#returnUrl,
2257
+ supportedBillingCountries: this.#supportedBillingCountries,
2258
+ recaptchaTokens
2259
+ });
2260
+ }
2253
2261
  };
2254
2262
  }
2255
- #initiateApplePay(skuId, country, recaptchaTokens) {
2263
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2256
2264
  try {
2257
2265
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2258
2266
  const paymentOptions = this.#paymentOptions;
2267
+ if (!paymentOptions) {
2268
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2269
+ return;
2270
+ }
2259
2271
  console.info("[web-apple-pay] Starting Apple Pay session", {
2260
2272
  skuId,
2261
2273
  country,
2262
2274
  loggedIn: sessionData.loggedIn,
2263
2275
  userType: sessionData.userType,
2264
- hasPaymentOptions: !!paymentOptions,
2265
- recaptchaTokensPresent: {
2266
- checkEligibility: !!recaptchaTokens.checkEligibility,
2267
- newBasket: !!recaptchaTokens.newBasket,
2268
- registerUser: !!recaptchaTokens.registerUser
2269
- }
2276
+ hasPaymentOptions: true
2270
2277
  });
2271
2278
  startApplePaySession(
2272
2279
  this.#buildSessionParams(
@@ -2274,7 +2281,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2274
2281
  country,
2275
2282
  sessionData,
2276
2283
  paymentOptions,
2277
- recaptchaTokens
2284
+ recaptchaTokensPromise
2278
2285
  )
2279
2286
  );
2280
2287
  } catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@economist/web-apple-pay",
3
3
  "description": "Web component package for Apple Pay checkout UI",
4
- "version": "0.1.3-beta.45",
4
+ "version": "0.1.3-beta.46",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",