@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.
- package/dist/index.js +83 -26
- package/dist/src/apple-pay-button.js +83 -26
- package/dist/src/apple-pay-button.stories.js +101 -26
- package/dist/src/apple-pay-modal.js +25 -6
- package/dist/src/apple-pay-modal.stories.js +83 -26
- package/dist/src/clients/payment-checkout/apple-session.js +27 -14
- package/dist/src/clients/payment-checkout/index.js +7 -0
- package/dist/src/clients/payment-checkout/payment-handler.js +38 -17
- package/dist/src/clients/payment-checkout/session.js +7 -0
- package/dist/src/index.js +83 -26
- package/dist/src/stories/utils/story-env.js +18 -0
- package/dist/src/utils/expressCheckoutRedirect.d.ts +27 -6
- package/dist/src/utils/expressCheckoutRedirect.js +21 -15
- package/package.json +1 -1
|
@@ -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
|
-
|
|
521
|
-
|
|
522
|
-
<
|
|
523
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
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",
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
<
|
|
523
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
<
|
|
523
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
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",
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
68
|
-
|
|
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",
|
|
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
|
);
|