@economist/web-apple-pay 0.1.3-beta.20 → 0.1.3-beta.21

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.
@@ -516,11 +516,13 @@ var apple_pay_modal_template_default = `<style>
516
516
  </svg>
517
517
  Please accept the terms to continue
518
518
  </div>
519
- <button class="apm__cta" id="apple-pay-modal-cta">
520
- Continue with
521
- <svg width="51" height="22" aria-label="Apple Pay" role="img">
522
- <use href="#apple-pay-logo" />
523
- </svg>
519
+ <button class="apm__cta" id="apple-pay-modal-cta" aria-label="Continue with Apple Pay">
520
+ <span class="apm__cta-content">
521
+ Continue with
522
+ <svg width="51" height="22" aria-hidden="true" role="img">
523
+ <use href="#apple-pay-logo" />
524
+ </svg>
525
+ </span>
524
526
  </button>
525
527
  </div>
526
528
  </div>
@@ -636,6 +638,7 @@ function getOfferTypeText(offerType) {
636
638
  }
637
639
  var TERMS_LINK = '<a href="https://www.economistgroup.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>';
638
640
  var PRIVACY_LINK = '<a href="https://www.economistgroup.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>';
641
+ var MSG_RECAPTCHA_LOAD_ERROR = "Something went wrong. Please close and try again.";
639
642
  function getCheckboxTermsHtml(isNYT, isLoggedIn, autoRenewal) {
640
643
  const autoRenewalClause = autoRenewal ? " and auto-renewal terms above," : "";
641
644
  if (isNYT) {
@@ -927,6 +930,22 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
927
930
  }
928
931
  }).catch(() => {
929
932
  console.error("[web-apple-pay] apple-pay-modal: failed to pre-fetch reCAPTCHA tokens; CTA remains disabled");
933
+ if (cta) {
934
+ cta.removeAttribute("aria-busy");
935
+ }
936
+ const existingError = this.querySelector("#apple-pay-modal-error");
937
+ if (existingError) {
938
+ const icon = existingError.querySelector("svg");
939
+ existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
940
+ if (icon) existingError.prepend(icon);
941
+ existingError.classList.add("apm__error--visible");
942
+ } else {
943
+ const errorNotice = document.createElement("div");
944
+ errorNotice.setAttribute("role", "alert");
945
+ errorNotice.className = "apm__error apm__error--visible";
946
+ errorNotice.textContent = MSG_RECAPTCHA_LOAD_ERROR;
947
+ cta?.insertAdjacentElement("beforebegin", errorNotice);
948
+ }
930
949
  });
931
950
  }
932
951
  cta?.addEventListener("click", () => {
@@ -937,8 +956,8 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
937
956
  }
938
957
  if (this.#recaptchaTokens) {
939
958
  this.onConfirm?.(this.#recaptchaTokens);
959
+ this.close();
940
960
  }
941
- this.close();
942
961
  });
943
962
  this.querySelector("#apple-pay-modal-close")?.addEventListener(
944
963
  "click",
@@ -1140,11 +1159,18 @@ var apiFetch = async (path, options = {}) => {
1140
1159
  });
1141
1160
  if (!response.ok) {
1142
1161
  const contentType = response.headers.get("content-type") || "";
1162
+ console.warn("[web-apple-pay] apiFetch: non-2xx response", {
1163
+ path,
1164
+ status: response.status,
1165
+ contentType
1166
+ });
1143
1167
  if (contentType.includes("application/json")) {
1144
1168
  const errorBody = await response.json();
1169
+ console.warn("[web-apple-pay] apiFetch: parsed JSON error body", { errorBody });
1145
1170
  throw new ApiError(response.status, errorBody);
1146
1171
  }
1147
1172
  const errorText = await response.text();
1173
+ console.warn("[web-apple-pay] apiFetch: non-JSON error body", { errorText });
1148
1174
  throw new Error(
1149
1175
  `API request failed: ${response.status} ${response.statusText} - ${errorText}`
1150
1176
  );
@@ -1182,6 +1208,22 @@ var getExpressCheckoutSession = () => {
1182
1208
  };
1183
1209
 
1184
1210
  // src/utils/expressCheckoutRedirect.ts
1211
+ var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
1212
+ var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
1213
+ var writeCheckoutRedirect = (originUrl) => {
1214
+ if (originUrl.startsWith("/")) {
1215
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
1216
+ return;
1217
+ }
1218
+ try {
1219
+ const { hostname } = new URL(originUrl);
1220
+ const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
1221
+ if (isAllowed) {
1222
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
1223
+ }
1224
+ } catch {
1225
+ }
1226
+ };
1185
1227
  var captureOriginUrl = () => window.location.href;
1186
1228
  var buildQueryString = () => {
1187
1229
  const countryCode = new URLSearchParams(window.location.search).get(
@@ -1191,11 +1233,8 @@ var buildQueryString = () => {
1191
1233
  };
1192
1234
  var decodeSku = (sku) => {
1193
1235
  try {
1194
- const decoded = atob(sku.substring(sku.indexOf("-") + 1));
1195
- console.log("[web-apple-pay] decodeSku", { input: sku, decoded });
1196
- return decoded;
1197
- } catch (err) {
1198
- console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
1236
+ return atob(sku.substring(sku.indexOf("-") + 1));
1237
+ } catch {
1199
1238
  return sku;
1200
1239
  }
1201
1240
  };
@@ -1209,24 +1248,16 @@ var redirectToCheckoutComplete = (params) => {
1209
1248
  url.searchParams.set("lmo_offer", params.skuId);
1210
1249
  url.searchParams.set("status", params.status);
1211
1250
  url.searchParams.set("redirectToCheckout", "true");
1212
- url.searchParams.set("returnUrl", params.returnUrl);
1213
1251
  return url.pathname + url.search;
1214
1252
  };
1215
1253
  var redirectToCheckoutFallback = (params) => {
1216
- const decodedOffer = decodeSku(params.skuId);
1217
- console.log("[web-apple-pay] redirectToCheckoutFallback", {
1218
- rawSkuId: params.skuId,
1219
- decodedOffer
1220
- });
1221
1254
  const url = new URL(window.location.href);
1222
1255
  url.pathname = "/checkout";
1223
- url.searchParams.set("lmo_offer", decodedOffer);
1256
+ url.searchParams.set("lmo_offer", decodeSku(params.skuId));
1224
1257
  if (params.inlineError)
1225
1258
  url.searchParams.set("inlineError", params.inlineError);
1226
1259
  if (params.email) url.searchParams.set("email", params.email);
1227
1260
  if (params.emailError) url.searchParams.set("emailError", params.emailError);
1228
- url.searchParams.set("returnUrl", params.returnUrl);
1229
- console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
1230
1261
  return url.pathname + url.search;
1231
1262
  };
1232
1263
 
@@ -1316,10 +1347,10 @@ var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
1316
1347
 
1317
1348
  // src/clients/payment-checkout/apple-session.ts
1318
1349
  function redirectToFallbackCheckout(config) {
1350
+ writeCheckoutRedirect(config.returnUrl);
1319
1351
  trackFallbackCheckout(config.skuId);
1320
1352
  window.location.href = redirectToCheckoutFallback({
1321
1353
  skuId: config.skuId,
1322
- returnUrl: config.returnUrl,
1323
1354
  inlineError: MSG_PAYMENT_GENERIC_ERROR
1324
1355
  });
1325
1356
  }
@@ -1509,9 +1540,15 @@ var failApplePaySession = (session, errors = []) => {
1509
1540
  };
1510
1541
  function parseWalletApiError(err) {
1511
1542
  const body = err instanceof ApiError ? err.body : err;
1543
+ console.warn("[web-apple-pay] parseWalletApiError: raw error", {
1544
+ isApiError: err instanceof ApiError,
1545
+ body
1546
+ });
1512
1547
  if (body && typeof body === "object" && "errorType" in body && "message" in body) {
1548
+ console.warn("[web-apple-pay] parseWalletApiError: structured BFF error", body);
1513
1549
  return body;
1514
1550
  }
1551
+ console.warn("[web-apple-pay] parseWalletApiError: unstructured error \u2014 no errorType found");
1515
1552
  return null;
1516
1553
  }
1517
1554
  function validateContacts(event, sessionData, supportedBillingCountries) {
@@ -1525,11 +1562,10 @@ function validateContacts(event, sessionData, supportedBillingCountries) {
1525
1562
  return void 0;
1526
1563
  }
1527
1564
  function redirectFallback(skuId, returnUrl, options) {
1528
- console.log("[web-apple-pay] redirectFallback called", { skuId, returnUrl, options });
1565
+ writeCheckoutRedirect(returnUrl);
1529
1566
  trackFallbackCheckout(skuId);
1530
1567
  window.location.href = redirectToCheckoutFallback({
1531
1568
  skuId,
1532
- returnUrl,
1533
1569
  ...options
1534
1570
  });
1535
1571
  }
@@ -1576,11 +1612,11 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1576
1612
  trackWalletPaymentSuccess(skuId, result.basketId);
1577
1613
  session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
1578
1614
  const userStatus = sessionData.loggedIn ? "lex" : "nex";
1615
+ writeCheckoutRedirect(returnUrl);
1579
1616
  window.location.href = redirectToCheckoutComplete({
1580
1617
  basketId: result.basketId,
1581
1618
  status: userStatus,
1582
1619
  skuId: decodeSku(skuId),
1583
- returnUrl,
1584
1620
  queryString
1585
1621
  });
1586
1622
  } catch (err) {
@@ -1590,6 +1626,10 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1590
1626
  }
1591
1627
  console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
1592
1628
  const walletError = parseWalletApiError(err);
1629
+ console.warn("[web-apple-pay] handlePaymentAuthorized: walletError result", {
1630
+ walletError,
1631
+ validatedEmail
1632
+ });
1593
1633
  trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
1594
1634
  failApplePaySession(session);
1595
1635
  if (!walletError) {
@@ -2084,7 +2124,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2084
2124
  if (button) {
2085
2125
  button.classList.remove("wap-loading");
2086
2126
  this.#updateDisabledState();
2087
- this.#recaptchaTokensPromise = getPurchaseRecaptchaTokens();
2127
+ this.#scheduleRecaptchaTokens();
2088
2128
  trackExpressWalletImpression(this.#offer.sku_id, {
2089
2129
  entryPoint: this.#entryPoint,
2090
2130
  country: this.#country
@@ -2123,10 +2163,27 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2123
2163
  const country = this.#country;
2124
2164
  modal.onConfirm = (recaptchaTokens) => {
2125
2165
  this.#initiateApplePay(skuId, country, recaptchaTokens);
2126
- this.#recaptchaTokensPromise = getPurchaseRecaptchaTokens();
2166
+ this.#scheduleRecaptchaTokens();
2127
2167
  };
2128
2168
  modal.open(this.#recaptchaTokensPromise);
2129
2169
  }
2170
+ /**
2171
+ * Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
2172
+ * If the fetch rejects, the field is cleared so the next `modal.open()` call
2173
+ * falls back to a new attempt via the modal's own null-fallback path — preventing
2174
+ * a single transient failure from permanently disabling the CTA on all retries.
2175
+ * The identity guard prevents a late rejection from clobbering a newer attempt.
2176
+ */
2177
+ #scheduleRecaptchaTokens() {
2178
+ const attempt = getPurchaseRecaptchaTokens();
2179
+ attempt.catch((err) => {
2180
+ console.error("[web-apple-pay] reCAPTCHA token pre-fetch failed; will retry on modal open", err);
2181
+ if (this.#recaptchaTokensPromise === attempt) {
2182
+ this.#recaptchaTokensPromise = null;
2183
+ }
2184
+ });
2185
+ this.#recaptchaTokensPromise = attempt;
2186
+ }
2130
2187
  async #fetchSession() {
2131
2188
  try {
2132
2189
  return await getExpressCheckoutSession();
@@ -2243,6 +2300,24 @@ var createFetchMock = (sessionResponse, sessionStatus = 200) => {
2243
2300
  headers: { "content-type": "application/json" }
2244
2301
  });
2245
2302
  }
2303
+ if (requestUrl.includes("/v1/wallet/payment-options/sku/")) {
2304
+ return new Response(
2305
+ JSON.stringify({
2306
+ options: {
2307
+ apple_pay: {
2308
+ provider: "apple-pay",
2309
+ settings: {
2310
+ supported_networks: ["visa", "masterCard", "amex"]
2311
+ }
2312
+ }
2313
+ }
2314
+ }),
2315
+ {
2316
+ status: 200,
2317
+ headers: { "content-type": "application/json" }
2318
+ }
2319
+ );
2320
+ }
2246
2321
  return new Response("Not Found", {
2247
2322
  status: 404,
2248
2323
  statusText: "Not Found"
@@ -516,11 +516,13 @@ var apple_pay_modal_template_default = `<style>
516
516
  </svg>
517
517
  Please accept the terms to continue
518
518
  </div>
519
- <button class="apm__cta" id="apple-pay-modal-cta">
520
- Continue with
521
- <svg width="51" height="22" aria-label="Apple Pay" role="img">
522
- <use href="#apple-pay-logo" />
523
- </svg>
519
+ <button class="apm__cta" id="apple-pay-modal-cta" aria-label="Continue with Apple Pay">
520
+ <span class="apm__cta-content">
521
+ Continue with
522
+ <svg width="51" height="22" aria-hidden="true" role="img">
523
+ <use href="#apple-pay-logo" />
524
+ </svg>
525
+ </span>
524
526
  </button>
525
527
  </div>
526
528
  </div>
@@ -628,6 +630,7 @@ function getOfferTypeText(offerType) {
628
630
  }
629
631
  var TERMS_LINK = '<a href="https://www.economistgroup.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>';
630
632
  var PRIVACY_LINK = '<a href="https://www.economistgroup.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>';
633
+ var MSG_RECAPTCHA_LOAD_ERROR = "Something went wrong. Please close and try again.";
631
634
  function getCheckboxTermsHtml(isNYT, isLoggedIn, autoRenewal) {
632
635
  const autoRenewalClause = autoRenewal ? " and auto-renewal terms above," : "";
633
636
  if (isNYT) {
@@ -919,6 +922,22 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
919
922
  }
920
923
  }).catch(() => {
921
924
  console.error("[web-apple-pay] apple-pay-modal: failed to pre-fetch reCAPTCHA tokens; CTA remains disabled");
925
+ if (cta) {
926
+ cta.removeAttribute("aria-busy");
927
+ }
928
+ const existingError = this.querySelector("#apple-pay-modal-error");
929
+ if (existingError) {
930
+ const icon = existingError.querySelector("svg");
931
+ existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
932
+ if (icon) existingError.prepend(icon);
933
+ existingError.classList.add("apm__error--visible");
934
+ } else {
935
+ const errorNotice = document.createElement("div");
936
+ errorNotice.setAttribute("role", "alert");
937
+ errorNotice.className = "apm__error apm__error--visible";
938
+ errorNotice.textContent = MSG_RECAPTCHA_LOAD_ERROR;
939
+ cta?.insertAdjacentElement("beforebegin", errorNotice);
940
+ }
922
941
  });
923
942
  }
924
943
  cta?.addEventListener("click", () => {
@@ -929,8 +948,8 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
929
948
  }
930
949
  if (this.#recaptchaTokens) {
931
950
  this.onConfirm?.(this.#recaptchaTokens);
951
+ this.close();
932
952
  }
933
- this.close();
934
953
  });
935
954
  this.querySelector("#apple-pay-modal-close")?.addEventListener(
936
955
  "click",
@@ -516,11 +516,13 @@ var apple_pay_modal_template_default = `<style>
516
516
  </svg>
517
517
  Please accept the terms to continue
518
518
  </div>
519
- <button class="apm__cta" id="apple-pay-modal-cta">
520
- Continue with
521
- <svg width="51" height="22" aria-label="Apple Pay" role="img">
522
- <use href="#apple-pay-logo" />
523
- </svg>
519
+ <button class="apm__cta" id="apple-pay-modal-cta" aria-label="Continue with Apple Pay">
520
+ <span class="apm__cta-content">
521
+ Continue with
522
+ <svg width="51" height="22" aria-hidden="true" role="img">
523
+ <use href="#apple-pay-logo" />
524
+ </svg>
525
+ </span>
524
526
  </button>
525
527
  </div>
526
528
  </div>
@@ -628,6 +630,7 @@ function getOfferTypeText(offerType) {
628
630
  }
629
631
  var TERMS_LINK = '<a href="https://www.economistgroup.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>';
630
632
  var PRIVACY_LINK = '<a href="https://www.economistgroup.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>';
633
+ var MSG_RECAPTCHA_LOAD_ERROR = "Something went wrong. Please close and try again.";
631
634
  function getCheckboxTermsHtml(isNYT, isLoggedIn, autoRenewal) {
632
635
  const autoRenewalClause = autoRenewal ? " and auto-renewal terms above," : "";
633
636
  if (isNYT) {
@@ -919,6 +922,22 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
919
922
  }
920
923
  }).catch(() => {
921
924
  console.error("[web-apple-pay] apple-pay-modal: failed to pre-fetch reCAPTCHA tokens; CTA remains disabled");
925
+ if (cta) {
926
+ cta.removeAttribute("aria-busy");
927
+ }
928
+ const existingError = this.querySelector("#apple-pay-modal-error");
929
+ if (existingError) {
930
+ const icon = existingError.querySelector("svg");
931
+ existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
932
+ if (icon) existingError.prepend(icon);
933
+ existingError.classList.add("apm__error--visible");
934
+ } else {
935
+ const errorNotice = document.createElement("div");
936
+ errorNotice.setAttribute("role", "alert");
937
+ errorNotice.className = "apm__error apm__error--visible";
938
+ errorNotice.textContent = MSG_RECAPTCHA_LOAD_ERROR;
939
+ cta?.insertAdjacentElement("beforebegin", errorNotice);
940
+ }
922
941
  });
923
942
  }
924
943
  cta?.addEventListener("click", () => {
@@ -929,8 +948,8 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
929
948
  }
930
949
  if (this.#recaptchaTokens) {
931
950
  this.onConfirm?.(this.#recaptchaTokens);
951
+ this.close();
932
952
  }
933
- this.close();
934
953
  });
935
954
  this.querySelector("#apple-pay-modal-close")?.addEventListener(
936
955
  "click",
@@ -1132,11 +1151,18 @@ var apiFetch = async (path, options = {}) => {
1132
1151
  });
1133
1152
  if (!response.ok) {
1134
1153
  const contentType = response.headers.get("content-type") || "";
1154
+ console.warn("[web-apple-pay] apiFetch: non-2xx response", {
1155
+ path,
1156
+ status: response.status,
1157
+ contentType
1158
+ });
1135
1159
  if (contentType.includes("application/json")) {
1136
1160
  const errorBody = await response.json();
1161
+ console.warn("[web-apple-pay] apiFetch: parsed JSON error body", { errorBody });
1137
1162
  throw new ApiError(response.status, errorBody);
1138
1163
  }
1139
1164
  const errorText = await response.text();
1165
+ console.warn("[web-apple-pay] apiFetch: non-JSON error body", { errorText });
1140
1166
  throw new Error(
1141
1167
  `API request failed: ${response.status} ${response.statusText} - ${errorText}`
1142
1168
  );
@@ -1174,6 +1200,22 @@ var getExpressCheckoutSession = () => {
1174
1200
  };
1175
1201
 
1176
1202
  // src/utils/expressCheckoutRedirect.ts
1203
+ var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
1204
+ var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
1205
+ var writeCheckoutRedirect = (originUrl) => {
1206
+ if (originUrl.startsWith("/")) {
1207
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
1208
+ return;
1209
+ }
1210
+ try {
1211
+ const { hostname } = new URL(originUrl);
1212
+ const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
1213
+ if (isAllowed) {
1214
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
1215
+ }
1216
+ } catch {
1217
+ }
1218
+ };
1177
1219
  var captureOriginUrl = () => window.location.href;
1178
1220
  var buildQueryString = () => {
1179
1221
  const countryCode = new URLSearchParams(window.location.search).get(
@@ -1183,11 +1225,8 @@ var buildQueryString = () => {
1183
1225
  };
1184
1226
  var decodeSku = (sku) => {
1185
1227
  try {
1186
- const decoded = atob(sku.substring(sku.indexOf("-") + 1));
1187
- console.log("[web-apple-pay] decodeSku", { input: sku, decoded });
1188
- return decoded;
1189
- } catch (err) {
1190
- console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
1228
+ return atob(sku.substring(sku.indexOf("-") + 1));
1229
+ } catch {
1191
1230
  return sku;
1192
1231
  }
1193
1232
  };
@@ -1201,24 +1240,16 @@ var redirectToCheckoutComplete = (params) => {
1201
1240
  url.searchParams.set("lmo_offer", params.skuId);
1202
1241
  url.searchParams.set("status", params.status);
1203
1242
  url.searchParams.set("redirectToCheckout", "true");
1204
- url.searchParams.set("returnUrl", params.returnUrl);
1205
1243
  return url.pathname + url.search;
1206
1244
  };
1207
1245
  var redirectToCheckoutFallback = (params) => {
1208
- const decodedOffer = decodeSku(params.skuId);
1209
- console.log("[web-apple-pay] redirectToCheckoutFallback", {
1210
- rawSkuId: params.skuId,
1211
- decodedOffer
1212
- });
1213
1246
  const url = new URL(window.location.href);
1214
1247
  url.pathname = "/checkout";
1215
- url.searchParams.set("lmo_offer", decodedOffer);
1248
+ url.searchParams.set("lmo_offer", decodeSku(params.skuId));
1216
1249
  if (params.inlineError)
1217
1250
  url.searchParams.set("inlineError", params.inlineError);
1218
1251
  if (params.email) url.searchParams.set("email", params.email);
1219
1252
  if (params.emailError) url.searchParams.set("emailError", params.emailError);
1220
- url.searchParams.set("returnUrl", params.returnUrl);
1221
- console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
1222
1253
  return url.pathname + url.search;
1223
1254
  };
1224
1255
 
@@ -1308,10 +1339,10 @@ var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
1308
1339
 
1309
1340
  // src/clients/payment-checkout/apple-session.ts
1310
1341
  function redirectToFallbackCheckout(config) {
1342
+ writeCheckoutRedirect(config.returnUrl);
1311
1343
  trackFallbackCheckout(config.skuId);
1312
1344
  window.location.href = redirectToCheckoutFallback({
1313
1345
  skuId: config.skuId,
1314
- returnUrl: config.returnUrl,
1315
1346
  inlineError: MSG_PAYMENT_GENERIC_ERROR
1316
1347
  });
1317
1348
  }
@@ -1501,9 +1532,15 @@ var failApplePaySession = (session, errors = []) => {
1501
1532
  };
1502
1533
  function parseWalletApiError(err) {
1503
1534
  const body = err instanceof ApiError ? err.body : err;
1535
+ console.warn("[web-apple-pay] parseWalletApiError: raw error", {
1536
+ isApiError: err instanceof ApiError,
1537
+ body
1538
+ });
1504
1539
  if (body && typeof body === "object" && "errorType" in body && "message" in body) {
1540
+ console.warn("[web-apple-pay] parseWalletApiError: structured BFF error", body);
1505
1541
  return body;
1506
1542
  }
1543
+ console.warn("[web-apple-pay] parseWalletApiError: unstructured error \u2014 no errorType found");
1507
1544
  return null;
1508
1545
  }
1509
1546
  function validateContacts(event, sessionData, supportedBillingCountries) {
@@ -1517,11 +1554,10 @@ function validateContacts(event, sessionData, supportedBillingCountries) {
1517
1554
  return void 0;
1518
1555
  }
1519
1556
  function redirectFallback(skuId, returnUrl, options) {
1520
- console.log("[web-apple-pay] redirectFallback called", { skuId, returnUrl, options });
1557
+ writeCheckoutRedirect(returnUrl);
1521
1558
  trackFallbackCheckout(skuId);
1522
1559
  window.location.href = redirectToCheckoutFallback({
1523
1560
  skuId,
1524
- returnUrl,
1525
1561
  ...options
1526
1562
  });
1527
1563
  }
@@ -1568,11 +1604,11 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1568
1604
  trackWalletPaymentSuccess(skuId, result.basketId);
1569
1605
  session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
1570
1606
  const userStatus = sessionData.loggedIn ? "lex" : "nex";
1607
+ writeCheckoutRedirect(returnUrl);
1571
1608
  window.location.href = redirectToCheckoutComplete({
1572
1609
  basketId: result.basketId,
1573
1610
  status: userStatus,
1574
1611
  skuId: decodeSku(skuId),
1575
- returnUrl,
1576
1612
  queryString
1577
1613
  });
1578
1614
  } catch (err) {
@@ -1582,6 +1618,10 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1582
1618
  }
1583
1619
  console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
1584
1620
  const walletError = parseWalletApiError(err);
1621
+ console.warn("[web-apple-pay] handlePaymentAuthorized: walletError result", {
1622
+ walletError,
1623
+ validatedEmail
1624
+ });
1585
1625
  trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
1586
1626
  failApplePaySession(session);
1587
1627
  if (!walletError) {
@@ -2076,7 +2116,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2076
2116
  if (button) {
2077
2117
  button.classList.remove("wap-loading");
2078
2118
  this.#updateDisabledState();
2079
- this.#recaptchaTokensPromise = getPurchaseRecaptchaTokens();
2119
+ this.#scheduleRecaptchaTokens();
2080
2120
  trackExpressWalletImpression(this.#offer.sku_id, {
2081
2121
  entryPoint: this.#entryPoint,
2082
2122
  country: this.#country
@@ -2115,10 +2155,27 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
2115
2155
  const country = this.#country;
2116
2156
  modal.onConfirm = (recaptchaTokens) => {
2117
2157
  this.#initiateApplePay(skuId, country, recaptchaTokens);
2118
- this.#recaptchaTokensPromise = getPurchaseRecaptchaTokens();
2158
+ this.#scheduleRecaptchaTokens();
2119
2159
  };
2120
2160
  modal.open(this.#recaptchaTokensPromise);
2121
2161
  }
2162
+ /**
2163
+ * Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
2164
+ * If the fetch rejects, the field is cleared so the next `modal.open()` call
2165
+ * falls back to a new attempt via the modal's own null-fallback path — preventing
2166
+ * a single transient failure from permanently disabling the CTA on all retries.
2167
+ * The identity guard prevents a late rejection from clobbering a newer attempt.
2168
+ */
2169
+ #scheduleRecaptchaTokens() {
2170
+ const attempt = getPurchaseRecaptchaTokens();
2171
+ attempt.catch((err) => {
2172
+ console.error("[web-apple-pay] reCAPTCHA token pre-fetch failed; will retry on modal open", err);
2173
+ if (this.#recaptchaTokensPromise === attempt) {
2174
+ this.#recaptchaTokensPromise = null;
2175
+ }
2176
+ });
2177
+ this.#recaptchaTokensPromise = attempt;
2178
+ }
2122
2179
  async #fetchSession() {
2123
2180
  try {
2124
2181
  return await getExpressCheckoutSession();
@@ -43,11 +43,18 @@ var apiFetch = async (path, options = {}) => {
43
43
  });
44
44
  if (!response.ok) {
45
45
  const contentType = response.headers.get("content-type") || "";
46
+ console.warn("[web-apple-pay] apiFetch: non-2xx response", {
47
+ path,
48
+ status: response.status,
49
+ contentType
50
+ });
46
51
  if (contentType.includes("application/json")) {
47
52
  const errorBody = await response.json();
53
+ console.warn("[web-apple-pay] apiFetch: parsed JSON error body", { errorBody });
48
54
  throw new ApiError(response.status, errorBody);
49
55
  }
50
56
  const errorText = await response.text();
57
+ console.warn("[web-apple-pay] apiFetch: non-JSON error body", { errorText });
51
58
  throw new Error(
52
59
  `API request failed: ${response.status} ${response.statusText} - ${errorText}`
53
60
  );
@@ -62,31 +69,37 @@ var validateAppleMerchant = (data) => {
62
69
  };
63
70
 
64
71
  // src/utils/expressCheckoutRedirect.ts
72
+ var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
73
+ var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
74
+ var writeCheckoutRedirect = (originUrl) => {
75
+ if (originUrl.startsWith("/")) {
76
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
77
+ return;
78
+ }
79
+ try {
80
+ const { hostname } = new URL(originUrl);
81
+ const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
82
+ if (isAllowed) {
83
+ localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
84
+ }
85
+ } catch {
86
+ }
87
+ };
65
88
  var decodeSku = (sku) => {
66
89
  try {
67
- const decoded = atob(sku.substring(sku.indexOf("-") + 1));
68
- console.log("[web-apple-pay] decodeSku", { input: sku, decoded });
69
- return decoded;
70
- } catch (err) {
71
- console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
90
+ return atob(sku.substring(sku.indexOf("-") + 1));
91
+ } catch {
72
92
  return sku;
73
93
  }
74
94
  };
75
95
  var redirectToCheckoutFallback = (params) => {
76
- const decodedOffer = decodeSku(params.skuId);
77
- console.log("[web-apple-pay] redirectToCheckoutFallback", {
78
- rawSkuId: params.skuId,
79
- decodedOffer
80
- });
81
96
  const url = new URL(window.location.href);
82
97
  url.pathname = "/checkout";
83
- url.searchParams.set("lmo_offer", decodedOffer);
98
+ url.searchParams.set("lmo_offer", decodeSku(params.skuId));
84
99
  if (params.inlineError)
85
100
  url.searchParams.set("inlineError", params.inlineError);
86
101
  if (params.email) url.searchParams.set("email", params.email);
87
102
  if (params.emailError) url.searchParams.set("emailError", params.emailError);
88
- url.searchParams.set("returnUrl", params.returnUrl);
89
- console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
90
103
  return url.pathname + url.search;
91
104
  };
92
105
 
@@ -175,10 +188,10 @@ var MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment
175
188
 
176
189
  // src/clients/payment-checkout/apple-session.ts
177
190
  function redirectToFallbackCheckout(config) {
191
+ writeCheckoutRedirect(config.returnUrl);
178
192
  trackFallbackCheckout(config.skuId);
179
193
  window.location.href = redirectToCheckoutFallback({
180
194
  skuId: config.skuId,
181
- returnUrl: config.returnUrl,
182
195
  inlineError: MSG_PAYMENT_GENERIC_ERROR
183
196
  });
184
197
  }
@@ -43,11 +43,18 @@ var apiFetch = async (path, options = {}) => {
43
43
  });
44
44
  if (!response.ok) {
45
45
  const contentType = response.headers.get("content-type") || "";
46
+ console.warn("[web-apple-pay] apiFetch: non-2xx response", {
47
+ path,
48
+ status: response.status,
49
+ contentType
50
+ });
46
51
  if (contentType.includes("application/json")) {
47
52
  const errorBody = await response.json();
53
+ console.warn("[web-apple-pay] apiFetch: parsed JSON error body", { errorBody });
48
54
  throw new ApiError(response.status, errorBody);
49
55
  }
50
56
  const errorText = await response.text();
57
+ console.warn("[web-apple-pay] apiFetch: non-JSON error body", { errorText });
51
58
  throw new Error(
52
59
  `API request failed: ${response.status} ${response.statusText} - ${errorText}`
53
60
  );