@economist/web-apple-pay 0.1.3-beta.44 → 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");
@@ -1381,22 +1382,28 @@ async function handleMerchantValidation(event, session, config) {
1381
1382
  try {
1382
1383
  const validationHost = new URL(event.validationURL).host;
1383
1384
  if (!validationHost.endsWith("apple.com")) {
1385
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
1384
1386
  session.abort();
1385
1387
  redirectToFallbackCheckout(config);
1386
1388
  return;
1387
1389
  }
1390
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
1388
1391
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
1392
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
1389
1393
  const response = await validateAppleMerchant({
1390
1394
  validationURL: event.validationURL,
1391
1395
  recaptchaToken
1392
1396
  });
1393
1397
  if (!response?.merchantSession) {
1398
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
1394
1399
  session.abort();
1395
1400
  redirectToFallbackCheckout(config);
1396
1401
  return;
1397
1402
  }
1403
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
1398
1404
  session.completeMerchantValidation(response.merchantSession);
1399
- } catch {
1405
+ } catch (error) {
1406
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
1400
1407
  session.abort();
1401
1408
  redirectToFallbackCheckout(config);
1402
1409
  }
@@ -1417,19 +1424,26 @@ function startApplePaySession(config) {
1417
1424
  };
1418
1425
  try {
1419
1426
  const session = new ApplePaySession(3, paymentRequest);
1427
+ console.info("[web-apple-pay] ApplePaySession created successfully");
1420
1428
  session.onvalidatemerchant = async (event) => {
1421
1429
  await handleMerchantValidation(event, session, config);
1422
1430
  };
1423
1431
  session.onpaymentauthorized = (event) => {
1424
1432
  trackWalletPaymentAuthorised(config.skuId);
1425
- 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
+ });
1426
1438
  };
1427
1439
  session.oncancel = () => {
1428
1440
  config.onCancel?.();
1429
1441
  };
1430
1442
  trackWalletPaymentSheetOpen(config.skuId);
1431
1443
  session.begin();
1432
- } catch {
1444
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
1445
+ } catch (error) {
1446
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
1433
1447
  redirectToFallbackCheckout(config);
1434
1448
  }
1435
1449
  }
@@ -2195,9 +2209,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2195
2209
  }
2196
2210
  const skuId = this.#offer.sku_id;
2197
2211
  const country = this.#country;
2198
- modal.onConfirm = (recaptchaTokens) => {
2212
+ modal.onConfirm = (recaptchaTokensPromise) => {
2199
2213
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2200
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2214
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2201
2215
  };
2202
2216
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2203
2217
  modal.open();
@@ -2221,7 +2235,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2221
2235
  return null;
2222
2236
  }
2223
2237
  }
2224
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2238
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2225
2239
  const offer = this.#offer;
2226
2240
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2227
2241
  return {
@@ -2233,31 +2247,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2233
2247
  skuId,
2234
2248
  returnUrl: this.#returnUrl,
2235
2249
  userLoggedIn: sessionData.loggedIn,
2236
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2237
- skuId,
2238
- country,
2239
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2240
- returnUrl: this.#returnUrl,
2241
- supportedBillingCountries: this.#supportedBillingCountries,
2242
- recaptchaTokens
2243
- })
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
+ }
2244
2261
  };
2245
2262
  }
2246
- #initiateApplePay(skuId, country, recaptchaTokens) {
2263
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2247
2264
  try {
2248
2265
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2249
2266
  const paymentOptions = this.#paymentOptions;
2267
+ if (!paymentOptions) {
2268
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2269
+ return;
2270
+ }
2250
2271
  console.info("[web-apple-pay] Starting Apple Pay session", {
2251
2272
  skuId,
2252
2273
  country,
2253
2274
  loggedIn: sessionData.loggedIn,
2254
2275
  userType: sessionData.userType,
2255
- hasPaymentOptions: !!paymentOptions,
2256
- recaptchaTokensPresent: {
2257
- checkEligibility: !!recaptchaTokens.checkEligibility,
2258
- newBasket: !!recaptchaTokens.newBasket,
2259
- registerUser: !!recaptchaTokens.registerUser
2260
- }
2276
+ hasPaymentOptions: true
2261
2277
  });
2262
2278
  startApplePaySession(
2263
2279
  this.#buildSessionParams(
@@ -2265,7 +2281,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2265
2281
  country,
2266
2282
  sessionData,
2267
2283
  paymentOptions,
2268
- recaptchaTokens
2284
+ recaptchaTokensPromise
2269
2285
  )
2270
2286
  );
2271
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");
@@ -1376,22 +1377,28 @@ async function handleMerchantValidation(event, session, config) {
1376
1377
  try {
1377
1378
  const validationHost = new URL(event.validationURL).host;
1378
1379
  if (!validationHost.endsWith("apple.com")) {
1380
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
1379
1381
  session.abort();
1380
1382
  redirectToFallbackCheckout(config);
1381
1383
  return;
1382
1384
  }
1385
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
1383
1386
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
1387
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
1384
1388
  const response = await validateAppleMerchant({
1385
1389
  validationURL: event.validationURL,
1386
1390
  recaptchaToken
1387
1391
  });
1388
1392
  if (!response?.merchantSession) {
1393
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
1389
1394
  session.abort();
1390
1395
  redirectToFallbackCheckout(config);
1391
1396
  return;
1392
1397
  }
1398
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
1393
1399
  session.completeMerchantValidation(response.merchantSession);
1394
- } catch {
1400
+ } catch (error) {
1401
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
1395
1402
  session.abort();
1396
1403
  redirectToFallbackCheckout(config);
1397
1404
  }
@@ -1412,19 +1419,26 @@ function startApplePaySession(config) {
1412
1419
  };
1413
1420
  try {
1414
1421
  const session = new ApplePaySession(3, paymentRequest);
1422
+ console.info("[web-apple-pay] ApplePaySession created successfully");
1415
1423
  session.onvalidatemerchant = async (event) => {
1416
1424
  await handleMerchantValidation(event, session, config);
1417
1425
  };
1418
1426
  session.onpaymentauthorized = (event) => {
1419
1427
  trackWalletPaymentAuthorised(config.skuId);
1420
- 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
+ });
1421
1433
  };
1422
1434
  session.oncancel = () => {
1423
1435
  config.onCancel?.();
1424
1436
  };
1425
1437
  trackWalletPaymentSheetOpen(config.skuId);
1426
1438
  session.begin();
1427
- } catch {
1439
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
1440
+ } catch (error) {
1441
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
1428
1442
  redirectToFallbackCheckout(config);
1429
1443
  }
1430
1444
  }
@@ -2190,9 +2204,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2190
2204
  }
2191
2205
  const skuId = this.#offer.sku_id;
2192
2206
  const country = this.#country;
2193
- modal.onConfirm = (recaptchaTokens) => {
2207
+ modal.onConfirm = (recaptchaTokensPromise) => {
2194
2208
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2195
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2209
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2196
2210
  };
2197
2211
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2198
2212
  modal.open();
@@ -2216,7 +2230,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2216
2230
  return null;
2217
2231
  }
2218
2232
  }
2219
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2233
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2220
2234
  const offer = this.#offer;
2221
2235
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2222
2236
  return {
@@ -2228,31 +2242,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2228
2242
  skuId,
2229
2243
  returnUrl: this.#returnUrl,
2230
2244
  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
- })
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
+ }
2239
2256
  };
2240
2257
  }
2241
- #initiateApplePay(skuId, country, recaptchaTokens) {
2258
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2242
2259
  try {
2243
2260
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2244
2261
  const paymentOptions = this.#paymentOptions;
2262
+ if (!paymentOptions) {
2263
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2264
+ return;
2265
+ }
2245
2266
  console.info("[web-apple-pay] Starting Apple Pay session", {
2246
2267
  skuId,
2247
2268
  country,
2248
2269
  loggedIn: sessionData.loggedIn,
2249
2270
  userType: sessionData.userType,
2250
- hasPaymentOptions: !!paymentOptions,
2251
- recaptchaTokensPresent: {
2252
- checkEligibility: !!recaptchaTokens.checkEligibility,
2253
- newBasket: !!recaptchaTokens.newBasket,
2254
- registerUser: !!recaptchaTokens.registerUser
2255
- }
2271
+ hasPaymentOptions: true
2256
2272
  });
2257
2273
  startApplePaySession(
2258
2274
  this.#buildSessionParams(
@@ -2260,7 +2276,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2260
2276
  country,
2261
2277
  sessionData,
2262
2278
  paymentOptions,
2263
- recaptchaTokens
2279
+ recaptchaTokensPromise
2264
2280
  )
2265
2281
  );
2266
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");
@@ -1384,22 +1385,28 @@ async function handleMerchantValidation(event, session, config) {
1384
1385
  try {
1385
1386
  const validationHost = new URL(event.validationURL).host;
1386
1387
  if (!validationHost.endsWith("apple.com")) {
1388
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
1387
1389
  session.abort();
1388
1390
  redirectToFallbackCheckout(config);
1389
1391
  return;
1390
1392
  }
1393
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
1391
1394
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
1395
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
1392
1396
  const response = await validateAppleMerchant({
1393
1397
  validationURL: event.validationURL,
1394
1398
  recaptchaToken
1395
1399
  });
1396
1400
  if (!response?.merchantSession) {
1401
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
1397
1402
  session.abort();
1398
1403
  redirectToFallbackCheckout(config);
1399
1404
  return;
1400
1405
  }
1406
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
1401
1407
  session.completeMerchantValidation(response.merchantSession);
1402
- } catch {
1408
+ } catch (error) {
1409
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
1403
1410
  session.abort();
1404
1411
  redirectToFallbackCheckout(config);
1405
1412
  }
@@ -1420,19 +1427,26 @@ function startApplePaySession(config) {
1420
1427
  };
1421
1428
  try {
1422
1429
  const session = new ApplePaySession(3, paymentRequest);
1430
+ console.info("[web-apple-pay] ApplePaySession created successfully");
1423
1431
  session.onvalidatemerchant = async (event) => {
1424
1432
  await handleMerchantValidation(event, session, config);
1425
1433
  };
1426
1434
  session.onpaymentauthorized = (event) => {
1427
1435
  trackWalletPaymentAuthorised(config.skuId);
1428
- 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
+ });
1429
1441
  };
1430
1442
  session.oncancel = () => {
1431
1443
  config.onCancel?.();
1432
1444
  };
1433
1445
  trackWalletPaymentSheetOpen(config.skuId);
1434
1446
  session.begin();
1435
- } catch {
1447
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
1448
+ } catch (error) {
1449
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
1436
1450
  redirectToFallbackCheckout(config);
1437
1451
  }
1438
1452
  }
@@ -2198,9 +2212,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2198
2212
  }
2199
2213
  const skuId = this.#offer.sku_id;
2200
2214
  const country = this.#country;
2201
- modal.onConfirm = (recaptchaTokens) => {
2215
+ modal.onConfirm = (recaptchaTokensPromise) => {
2202
2216
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2203
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2217
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2204
2218
  };
2205
2219
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2206
2220
  modal.open();
@@ -2224,7 +2238,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2224
2238
  return null;
2225
2239
  }
2226
2240
  }
2227
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2241
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2228
2242
  const offer = this.#offer;
2229
2243
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2230
2244
  return {
@@ -2236,31 +2250,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2236
2250
  skuId,
2237
2251
  returnUrl: this.#returnUrl,
2238
2252
  userLoggedIn: sessionData.loggedIn,
2239
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2240
- skuId,
2241
- country,
2242
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2243
- returnUrl: this.#returnUrl,
2244
- supportedBillingCountries: this.#supportedBillingCountries,
2245
- recaptchaTokens
2246
- })
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
+ }
2247
2264
  };
2248
2265
  }
2249
- #initiateApplePay(skuId, country, recaptchaTokens) {
2266
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2250
2267
  try {
2251
2268
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2252
2269
  const paymentOptions = this.#paymentOptions;
2270
+ if (!paymentOptions) {
2271
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2272
+ return;
2273
+ }
2253
2274
  console.info("[web-apple-pay] Starting Apple Pay session", {
2254
2275
  skuId,
2255
2276
  country,
2256
2277
  loggedIn: sessionData.loggedIn,
2257
2278
  userType: sessionData.userType,
2258
- hasPaymentOptions: !!paymentOptions,
2259
- recaptchaTokensPresent: {
2260
- checkEligibility: !!recaptchaTokens.checkEligibility,
2261
- newBasket: !!recaptchaTokens.newBasket,
2262
- registerUser: !!recaptchaTokens.registerUser
2263
- }
2279
+ hasPaymentOptions: true
2264
2280
  });
2265
2281
  startApplePaySession(
2266
2282
  this.#buildSessionParams(
@@ -2268,7 +2284,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2268
2284
  country,
2269
2285
  sessionData,
2270
2286
  paymentOptions,
2271
- recaptchaTokens
2287
+ recaptchaTokensPromise
2272
2288
  )
2273
2289
  );
2274
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");
@@ -1376,22 +1377,28 @@ async function handleMerchantValidation(event, session, config) {
1376
1377
  try {
1377
1378
  const validationHost = new URL(event.validationURL).host;
1378
1379
  if (!validationHost.endsWith("apple.com")) {
1380
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
1379
1381
  session.abort();
1380
1382
  redirectToFallbackCheckout(config);
1381
1383
  return;
1382
1384
  }
1385
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
1383
1386
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
1387
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
1384
1388
  const response = await validateAppleMerchant({
1385
1389
  validationURL: event.validationURL,
1386
1390
  recaptchaToken
1387
1391
  });
1388
1392
  if (!response?.merchantSession) {
1393
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
1389
1394
  session.abort();
1390
1395
  redirectToFallbackCheckout(config);
1391
1396
  return;
1392
1397
  }
1398
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
1393
1399
  session.completeMerchantValidation(response.merchantSession);
1394
- } catch {
1400
+ } catch (error) {
1401
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
1395
1402
  session.abort();
1396
1403
  redirectToFallbackCheckout(config);
1397
1404
  }
@@ -1412,19 +1419,26 @@ function startApplePaySession(config) {
1412
1419
  };
1413
1420
  try {
1414
1421
  const session = new ApplePaySession(3, paymentRequest);
1422
+ console.info("[web-apple-pay] ApplePaySession created successfully");
1415
1423
  session.onvalidatemerchant = async (event) => {
1416
1424
  await handleMerchantValidation(event, session, config);
1417
1425
  };
1418
1426
  session.onpaymentauthorized = (event) => {
1419
1427
  trackWalletPaymentAuthorised(config.skuId);
1420
- 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
+ });
1421
1433
  };
1422
1434
  session.oncancel = () => {
1423
1435
  config.onCancel?.();
1424
1436
  };
1425
1437
  trackWalletPaymentSheetOpen(config.skuId);
1426
1438
  session.begin();
1427
- } catch {
1439
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
1440
+ } catch (error) {
1441
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
1428
1442
  redirectToFallbackCheckout(config);
1429
1443
  }
1430
1444
  }
@@ -2190,9 +2204,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2190
2204
  }
2191
2205
  const skuId = this.#offer.sku_id;
2192
2206
  const country = this.#country;
2193
- modal.onConfirm = (recaptchaTokens) => {
2207
+ modal.onConfirm = (recaptchaTokensPromise) => {
2194
2208
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2195
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2209
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2196
2210
  };
2197
2211
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2198
2212
  modal.open();
@@ -2216,7 +2230,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2216
2230
  return null;
2217
2231
  }
2218
2232
  }
2219
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2233
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2220
2234
  const offer = this.#offer;
2221
2235
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2222
2236
  return {
@@ -2228,31 +2242,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2228
2242
  skuId,
2229
2243
  returnUrl: this.#returnUrl,
2230
2244
  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
- })
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
+ }
2239
2256
  };
2240
2257
  }
2241
- #initiateApplePay(skuId, country, recaptchaTokens) {
2258
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2242
2259
  try {
2243
2260
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2244
2261
  const paymentOptions = this.#paymentOptions;
2262
+ if (!paymentOptions) {
2263
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2264
+ return;
2265
+ }
2245
2266
  console.info("[web-apple-pay] Starting Apple Pay session", {
2246
2267
  skuId,
2247
2268
  country,
2248
2269
  loggedIn: sessionData.loggedIn,
2249
2270
  userType: sessionData.userType,
2250
- hasPaymentOptions: !!paymentOptions,
2251
- recaptchaTokensPresent: {
2252
- checkEligibility: !!recaptchaTokens.checkEligibility,
2253
- newBasket: !!recaptchaTokens.newBasket,
2254
- registerUser: !!recaptchaTokens.registerUser
2255
- }
2271
+ hasPaymentOptions: true
2256
2272
  });
2257
2273
  startApplePaySession(
2258
2274
  this.#buildSessionParams(
@@ -2260,7 +2276,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2260
2276
  country,
2261
2277
  sessionData,
2262
2278
  paymentOptions,
2263
- recaptchaTokens
2279
+ recaptchaTokensPromise
2264
2280
  )
2265
2281
  );
2266
2282
  } catch (error) {
@@ -201,22 +201,28 @@ async function handleMerchantValidation(event, session, config) {
201
201
  try {
202
202
  const validationHost = new URL(event.validationURL).host;
203
203
  if (!validationHost.endsWith("apple.com")) {
204
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
204
205
  session.abort();
205
206
  redirectToFallbackCheckout(config);
206
207
  return;
207
208
  }
209
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
208
210
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
211
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
209
212
  const response = await validateAppleMerchant({
210
213
  validationURL: event.validationURL,
211
214
  recaptchaToken
212
215
  });
213
216
  if (!response?.merchantSession) {
217
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
214
218
  session.abort();
215
219
  redirectToFallbackCheckout(config);
216
220
  return;
217
221
  }
222
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
218
223
  session.completeMerchantValidation(response.merchantSession);
219
- } catch {
224
+ } catch (error) {
225
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
220
226
  session.abort();
221
227
  redirectToFallbackCheckout(config);
222
228
  }
@@ -237,19 +243,26 @@ function startApplePaySession(config) {
237
243
  };
238
244
  try {
239
245
  const session = new ApplePaySession(3, paymentRequest);
246
+ console.info("[web-apple-pay] ApplePaySession created successfully");
240
247
  session.onvalidatemerchant = async (event) => {
241
248
  await handleMerchantValidation(event, session, config);
242
249
  };
243
250
  session.onpaymentauthorized = (event) => {
244
251
  trackWalletPaymentAuthorised(config.skuId);
245
- 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
+ });
246
257
  };
247
258
  session.oncancel = () => {
248
259
  config.onCancel?.();
249
260
  };
250
261
  trackWalletPaymentSheetOpen(config.skuId);
251
262
  session.begin();
252
- } catch {
263
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
264
+ } catch (error) {
265
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
253
266
  redirectToFallbackCheckout(config);
254
267
  }
255
268
  }
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");
@@ -1381,22 +1382,28 @@ async function handleMerchantValidation(event, session, config) {
1381
1382
  try {
1382
1383
  const validationHost = new URL(event.validationURL).host;
1383
1384
  if (!validationHost.endsWith("apple.com")) {
1385
+ console.error("[web-apple-pay] Merchant validation URL is not from apple.com \u2014 aborting session", { validationURL: event.validationURL });
1384
1386
  session.abort();
1385
1387
  redirectToFallbackCheckout(config);
1386
1388
  return;
1387
1389
  }
1390
+ console.info("[web-apple-pay] Merchant validation started", { validationHost });
1388
1391
  const recaptchaToken = await getRecaptchaToken("verifyMerchant");
1392
+ console.info("[web-apple-pay] verifyMerchant reCAPTCHA token acquired \u2014 calling validateAppleMerchant");
1389
1393
  const response = await validateAppleMerchant({
1390
1394
  validationURL: event.validationURL,
1391
1395
  recaptchaToken
1392
1396
  });
1393
1397
  if (!response?.merchantSession) {
1398
+ console.error("[web-apple-pay] Merchant validation response missing merchantSession \u2014 aborting session", { response });
1394
1399
  session.abort();
1395
1400
  redirectToFallbackCheckout(config);
1396
1401
  return;
1397
1402
  }
1403
+ console.info("[web-apple-pay] Merchant validation succeeded \u2014 completing merchant validation");
1398
1404
  session.completeMerchantValidation(response.merchantSession);
1399
- } catch {
1405
+ } catch (error) {
1406
+ console.error("[web-apple-pay] Merchant validation threw an error \u2014 aborting session", error);
1400
1407
  session.abort();
1401
1408
  redirectToFallbackCheckout(config);
1402
1409
  }
@@ -1417,19 +1424,26 @@ function startApplePaySession(config) {
1417
1424
  };
1418
1425
  try {
1419
1426
  const session = new ApplePaySession(3, paymentRequest);
1427
+ console.info("[web-apple-pay] ApplePaySession created successfully");
1420
1428
  session.onvalidatemerchant = async (event) => {
1421
1429
  await handleMerchantValidation(event, session, config);
1422
1430
  };
1423
1431
  session.onpaymentauthorized = (event) => {
1424
1432
  trackWalletPaymentAuthorised(config.skuId);
1425
- 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
+ });
1426
1438
  };
1427
1439
  session.oncancel = () => {
1428
1440
  config.onCancel?.();
1429
1441
  };
1430
1442
  trackWalletPaymentSheetOpen(config.skuId);
1431
1443
  session.begin();
1432
- } catch {
1444
+ console.info("[web-apple-pay] ApplePaySession.begin() called \u2014 payment sheet should appear");
1445
+ } catch (error) {
1446
+ console.error("[web-apple-pay] ApplePaySession creation or begin() threw \u2014 redirecting to fallback checkout", error);
1433
1447
  redirectToFallbackCheckout(config);
1434
1448
  }
1435
1449
  }
@@ -2195,9 +2209,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2195
2209
  }
2196
2210
  const skuId = this.#offer.sku_id;
2197
2211
  const country = this.#country;
2198
- modal.onConfirm = (recaptchaTokens) => {
2212
+ modal.onConfirm = (recaptchaTokensPromise) => {
2199
2213
  console.info("[web-apple-pay] onConfirm received from modal \u2014 initiating Apple Pay session", { skuId, country });
2200
- this.#initiateApplePay(skuId, country, recaptchaTokens);
2214
+ this.#initiateApplePay(skuId, country, recaptchaTokensPromise);
2201
2215
  };
2202
2216
  console.info("[web-apple-pay] Opening Apple Pay modal", { skuId, country });
2203
2217
  modal.open();
@@ -2221,7 +2235,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2221
2235
  return null;
2222
2236
  }
2223
2237
  }
2224
- #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokens) {
2238
+ #buildSessionParams(skuId, country, sessionData, paymentOptions, recaptchaTokensPromise) {
2225
2239
  const offer = this.#offer;
2226
2240
  const totalAmount = offer.price?.purchase ? `${offer.price.purchase}` : DEFAULT_TOTAL;
2227
2241
  return {
@@ -2233,31 +2247,33 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2233
2247
  skuId,
2234
2248
  returnUrl: this.#returnUrl,
2235
2249
  userLoggedIn: sessionData.loggedIn,
2236
- onPaymentAuthorized: (event, session) => handlePaymentAuthorized(event, session, sessionData, {
2237
- skuId,
2238
- country,
2239
- currency: offer.price?.currency_code ?? DEFAULT_CURRENCY,
2240
- returnUrl: this.#returnUrl,
2241
- supportedBillingCountries: this.#supportedBillingCountries,
2242
- recaptchaTokens
2243
- })
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
+ }
2244
2261
  };
2245
2262
  }
2246
- #initiateApplePay(skuId, country, recaptchaTokens) {
2263
+ #initiateApplePay(skuId, country, recaptchaTokensPromise) {
2247
2264
  try {
2248
2265
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
2249
2266
  const paymentOptions = this.#paymentOptions;
2267
+ if (!paymentOptions) {
2268
+ console.error("[web-apple-pay] #initiateApplePay called before payment options were fetched");
2269
+ return;
2270
+ }
2250
2271
  console.info("[web-apple-pay] Starting Apple Pay session", {
2251
2272
  skuId,
2252
2273
  country,
2253
2274
  loggedIn: sessionData.loggedIn,
2254
2275
  userType: sessionData.userType,
2255
- hasPaymentOptions: !!paymentOptions,
2256
- recaptchaTokensPresent: {
2257
- checkEligibility: !!recaptchaTokens.checkEligibility,
2258
- newBasket: !!recaptchaTokens.newBasket,
2259
- registerUser: !!recaptchaTokens.registerUser
2260
- }
2276
+ hasPaymentOptions: true
2261
2277
  });
2262
2278
  startApplePaySession(
2263
2279
  this.#buildSessionParams(
@@ -2265,7 +2281,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2265
2281
  country,
2266
2282
  sessionData,
2267
2283
  paymentOptions,
2268
- recaptchaTokens
2284
+ recaptchaTokensPromise
2269
2285
  )
2270
2286
  );
2271
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.44",
4
+ "version": "0.1.3-beta.46",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",