@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
|
@@ -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
|
);
|
|
@@ -239,13 +246,26 @@ var shippingContactDetails = (shippingContact) => {
|
|
|
239
246
|
};
|
|
240
247
|
|
|
241
248
|
// src/utils/expressCheckoutRedirect.ts
|
|
249
|
+
var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
|
|
250
|
+
var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
|
|
251
|
+
var writeCheckoutRedirect = (originUrl) => {
|
|
252
|
+
if (originUrl.startsWith("/")) {
|
|
253
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const { hostname } = new URL(originUrl);
|
|
258
|
+
const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
|
|
259
|
+
if (isAllowed) {
|
|
260
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
};
|
|
242
265
|
var decodeSku = (sku) => {
|
|
243
266
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return decoded;
|
|
247
|
-
} catch (err) {
|
|
248
|
-
console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
|
|
267
|
+
return atob(sku.substring(sku.indexOf("-") + 1));
|
|
268
|
+
} catch {
|
|
249
269
|
return sku;
|
|
250
270
|
}
|
|
251
271
|
};
|
|
@@ -259,24 +279,16 @@ var redirectToCheckoutComplete = (params) => {
|
|
|
259
279
|
url.searchParams.set("lmo_offer", params.skuId);
|
|
260
280
|
url.searchParams.set("status", params.status);
|
|
261
281
|
url.searchParams.set("redirectToCheckout", "true");
|
|
262
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
263
282
|
return url.pathname + url.search;
|
|
264
283
|
};
|
|
265
284
|
var redirectToCheckoutFallback = (params) => {
|
|
266
|
-
const decodedOffer = decodeSku(params.skuId);
|
|
267
|
-
console.log("[web-apple-pay] redirectToCheckoutFallback", {
|
|
268
|
-
rawSkuId: params.skuId,
|
|
269
|
-
decodedOffer
|
|
270
|
-
});
|
|
271
285
|
const url = new URL(window.location.href);
|
|
272
286
|
url.pathname = "/checkout";
|
|
273
|
-
url.searchParams.set("lmo_offer",
|
|
287
|
+
url.searchParams.set("lmo_offer", decodeSku(params.skuId));
|
|
274
288
|
if (params.inlineError)
|
|
275
289
|
url.searchParams.set("inlineError", params.inlineError);
|
|
276
290
|
if (params.email) url.searchParams.set("email", params.email);
|
|
277
291
|
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
278
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
279
|
-
console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
|
|
280
292
|
return url.pathname + url.search;
|
|
281
293
|
};
|
|
282
294
|
|
|
@@ -291,9 +303,15 @@ var failApplePaySession = (session, errors = []) => {
|
|
|
291
303
|
};
|
|
292
304
|
function parseWalletApiError(err) {
|
|
293
305
|
const body = err instanceof ApiError ? err.body : err;
|
|
306
|
+
console.warn("[web-apple-pay] parseWalletApiError: raw error", {
|
|
307
|
+
isApiError: err instanceof ApiError,
|
|
308
|
+
body
|
|
309
|
+
});
|
|
294
310
|
if (body && typeof body === "object" && "errorType" in body && "message" in body) {
|
|
311
|
+
console.warn("[web-apple-pay] parseWalletApiError: structured BFF error", body);
|
|
295
312
|
return body;
|
|
296
313
|
}
|
|
314
|
+
console.warn("[web-apple-pay] parseWalletApiError: unstructured error \u2014 no errorType found");
|
|
297
315
|
return null;
|
|
298
316
|
}
|
|
299
317
|
function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
@@ -307,11 +325,10 @@ function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
|
307
325
|
return void 0;
|
|
308
326
|
}
|
|
309
327
|
function redirectFallback(skuId, returnUrl, options) {
|
|
310
|
-
|
|
328
|
+
writeCheckoutRedirect(returnUrl);
|
|
311
329
|
trackFallbackCheckout(skuId);
|
|
312
330
|
window.location.href = redirectToCheckoutFallback({
|
|
313
331
|
skuId,
|
|
314
|
-
returnUrl,
|
|
315
332
|
...options
|
|
316
333
|
});
|
|
317
334
|
}
|
|
@@ -358,11 +375,11 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
|
358
375
|
trackWalletPaymentSuccess(skuId, result.basketId);
|
|
359
376
|
session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
|
|
360
377
|
const userStatus = sessionData.loggedIn ? "lex" : "nex";
|
|
378
|
+
writeCheckoutRedirect(returnUrl);
|
|
361
379
|
window.location.href = redirectToCheckoutComplete({
|
|
362
380
|
basketId: result.basketId,
|
|
363
381
|
status: userStatus,
|
|
364
382
|
skuId: decodeSku(skuId),
|
|
365
|
-
returnUrl,
|
|
366
383
|
queryString
|
|
367
384
|
});
|
|
368
385
|
} catch (err) {
|
|
@@ -372,6 +389,10 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
|
372
389
|
}
|
|
373
390
|
console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
|
|
374
391
|
const walletError = parseWalletApiError(err);
|
|
392
|
+
console.warn("[web-apple-pay] handlePaymentAuthorized: walletError result", {
|
|
393
|
+
walletError,
|
|
394
|
+
validatedEmail
|
|
395
|
+
});
|
|
375
396
|
trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
|
|
376
397
|
failApplePaySession(session);
|
|
377
398
|
if (!walletError) {
|
|
@@ -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
|
);
|
package/dist/src/index.js
CHANGED
|
@@ -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>
|
|
@@ -633,6 +635,7 @@ function getOfferTypeText(offerType) {
|
|
|
633
635
|
}
|
|
634
636
|
var TERMS_LINK = '<a href="https://www.economistgroup.com/terms-of-use" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>';
|
|
635
637
|
var PRIVACY_LINK = '<a href="https://www.economistgroup.com/privacy-policy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>';
|
|
638
|
+
var MSG_RECAPTCHA_LOAD_ERROR = "Something went wrong. Please close and try again.";
|
|
636
639
|
function getCheckboxTermsHtml(isNYT, isLoggedIn, autoRenewal) {
|
|
637
640
|
const autoRenewalClause = autoRenewal ? " and auto-renewal terms above," : "";
|
|
638
641
|
if (isNYT) {
|
|
@@ -924,6 +927,22 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
924
927
|
}
|
|
925
928
|
}).catch(() => {
|
|
926
929
|
console.error("[web-apple-pay] apple-pay-modal: failed to pre-fetch reCAPTCHA tokens; CTA remains disabled");
|
|
930
|
+
if (cta) {
|
|
931
|
+
cta.removeAttribute("aria-busy");
|
|
932
|
+
}
|
|
933
|
+
const existingError = this.querySelector("#apple-pay-modal-error");
|
|
934
|
+
if (existingError) {
|
|
935
|
+
const icon = existingError.querySelector("svg");
|
|
936
|
+
existingError.textContent = MSG_RECAPTCHA_LOAD_ERROR;
|
|
937
|
+
if (icon) existingError.prepend(icon);
|
|
938
|
+
existingError.classList.add("apm__error--visible");
|
|
939
|
+
} else {
|
|
940
|
+
const errorNotice = document.createElement("div");
|
|
941
|
+
errorNotice.setAttribute("role", "alert");
|
|
942
|
+
errorNotice.className = "apm__error apm__error--visible";
|
|
943
|
+
errorNotice.textContent = MSG_RECAPTCHA_LOAD_ERROR;
|
|
944
|
+
cta?.insertAdjacentElement("beforebegin", errorNotice);
|
|
945
|
+
}
|
|
927
946
|
});
|
|
928
947
|
}
|
|
929
948
|
cta?.addEventListener("click", () => {
|
|
@@ -934,8 +953,8 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
|
|
|
934
953
|
}
|
|
935
954
|
if (this.#recaptchaTokens) {
|
|
936
955
|
this.onConfirm?.(this.#recaptchaTokens);
|
|
956
|
+
this.close();
|
|
937
957
|
}
|
|
938
|
-
this.close();
|
|
939
958
|
});
|
|
940
959
|
this.querySelector("#apple-pay-modal-close")?.addEventListener(
|
|
941
960
|
"click",
|
|
@@ -1137,11 +1156,18 @@ var apiFetch = async (path, options = {}) => {
|
|
|
1137
1156
|
});
|
|
1138
1157
|
if (!response.ok) {
|
|
1139
1158
|
const contentType = response.headers.get("content-type") || "";
|
|
1159
|
+
console.warn("[web-apple-pay] apiFetch: non-2xx response", {
|
|
1160
|
+
path,
|
|
1161
|
+
status: response.status,
|
|
1162
|
+
contentType
|
|
1163
|
+
});
|
|
1140
1164
|
if (contentType.includes("application/json")) {
|
|
1141
1165
|
const errorBody = await response.json();
|
|
1166
|
+
console.warn("[web-apple-pay] apiFetch: parsed JSON error body", { errorBody });
|
|
1142
1167
|
throw new ApiError(response.status, errorBody);
|
|
1143
1168
|
}
|
|
1144
1169
|
const errorText = await response.text();
|
|
1170
|
+
console.warn("[web-apple-pay] apiFetch: non-JSON error body", { errorText });
|
|
1145
1171
|
throw new Error(
|
|
1146
1172
|
`API request failed: ${response.status} ${response.statusText} - ${errorText}`
|
|
1147
1173
|
);
|
|
@@ -1179,6 +1205,22 @@ var getExpressCheckoutSession = () => {
|
|
|
1179
1205
|
};
|
|
1180
1206
|
|
|
1181
1207
|
// src/utils/expressCheckoutRedirect.ts
|
|
1208
|
+
var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
|
|
1209
|
+
var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
|
|
1210
|
+
var writeCheckoutRedirect = (originUrl) => {
|
|
1211
|
+
if (originUrl.startsWith("/")) {
|
|
1212
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const { hostname } = new URL(originUrl);
|
|
1217
|
+
const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
|
|
1218
|
+
if (isAllowed) {
|
|
1219
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1182
1224
|
var captureOriginUrl = () => window.location.href;
|
|
1183
1225
|
var buildQueryString = () => {
|
|
1184
1226
|
const countryCode = new URLSearchParams(window.location.search).get(
|
|
@@ -1188,11 +1230,8 @@ var buildQueryString = () => {
|
|
|
1188
1230
|
};
|
|
1189
1231
|
var decodeSku = (sku) => {
|
|
1190
1232
|
try {
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return decoded;
|
|
1194
|
-
} catch (err) {
|
|
1195
|
-
console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
|
|
1233
|
+
return atob(sku.substring(sku.indexOf("-") + 1));
|
|
1234
|
+
} catch {
|
|
1196
1235
|
return sku;
|
|
1197
1236
|
}
|
|
1198
1237
|
};
|
|
@@ -1206,24 +1245,16 @@ var redirectToCheckoutComplete = (params) => {
|
|
|
1206
1245
|
url.searchParams.set("lmo_offer", params.skuId);
|
|
1207
1246
|
url.searchParams.set("status", params.status);
|
|
1208
1247
|
url.searchParams.set("redirectToCheckout", "true");
|
|
1209
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
1210
1248
|
return url.pathname + url.search;
|
|
1211
1249
|
};
|
|
1212
1250
|
var redirectToCheckoutFallback = (params) => {
|
|
1213
|
-
const decodedOffer = decodeSku(params.skuId);
|
|
1214
|
-
console.log("[web-apple-pay] redirectToCheckoutFallback", {
|
|
1215
|
-
rawSkuId: params.skuId,
|
|
1216
|
-
decodedOffer
|
|
1217
|
-
});
|
|
1218
1251
|
const url = new URL(window.location.href);
|
|
1219
1252
|
url.pathname = "/checkout";
|
|
1220
|
-
url.searchParams.set("lmo_offer",
|
|
1253
|
+
url.searchParams.set("lmo_offer", decodeSku(params.skuId));
|
|
1221
1254
|
if (params.inlineError)
|
|
1222
1255
|
url.searchParams.set("inlineError", params.inlineError);
|
|
1223
1256
|
if (params.email) url.searchParams.set("email", params.email);
|
|
1224
1257
|
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
1225
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
1226
|
-
console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
|
|
1227
1258
|
return url.pathname + url.search;
|
|
1228
1259
|
};
|
|
1229
1260
|
|
|
@@ -1313,10 +1344,10 @@ var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
|
1313
1344
|
|
|
1314
1345
|
// src/clients/payment-checkout/apple-session.ts
|
|
1315
1346
|
function redirectToFallbackCheckout(config) {
|
|
1347
|
+
writeCheckoutRedirect(config.returnUrl);
|
|
1316
1348
|
trackFallbackCheckout(config.skuId);
|
|
1317
1349
|
window.location.href = redirectToCheckoutFallback({
|
|
1318
1350
|
skuId: config.skuId,
|
|
1319
|
-
returnUrl: config.returnUrl,
|
|
1320
1351
|
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
1321
1352
|
});
|
|
1322
1353
|
}
|
|
@@ -1506,9 +1537,15 @@ var failApplePaySession = (session, errors = []) => {
|
|
|
1506
1537
|
};
|
|
1507
1538
|
function parseWalletApiError(err) {
|
|
1508
1539
|
const body = err instanceof ApiError ? err.body : err;
|
|
1540
|
+
console.warn("[web-apple-pay] parseWalletApiError: raw error", {
|
|
1541
|
+
isApiError: err instanceof ApiError,
|
|
1542
|
+
body
|
|
1543
|
+
});
|
|
1509
1544
|
if (body && typeof body === "object" && "errorType" in body && "message" in body) {
|
|
1545
|
+
console.warn("[web-apple-pay] parseWalletApiError: structured BFF error", body);
|
|
1510
1546
|
return body;
|
|
1511
1547
|
}
|
|
1548
|
+
console.warn("[web-apple-pay] parseWalletApiError: unstructured error \u2014 no errorType found");
|
|
1512
1549
|
return null;
|
|
1513
1550
|
}
|
|
1514
1551
|
function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
@@ -1522,11 +1559,10 @@ function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
|
1522
1559
|
return void 0;
|
|
1523
1560
|
}
|
|
1524
1561
|
function redirectFallback(skuId, returnUrl, options) {
|
|
1525
|
-
|
|
1562
|
+
writeCheckoutRedirect(returnUrl);
|
|
1526
1563
|
trackFallbackCheckout(skuId);
|
|
1527
1564
|
window.location.href = redirectToCheckoutFallback({
|
|
1528
1565
|
skuId,
|
|
1529
|
-
returnUrl,
|
|
1530
1566
|
...options
|
|
1531
1567
|
});
|
|
1532
1568
|
}
|
|
@@ -1573,11 +1609,11 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
|
1573
1609
|
trackWalletPaymentSuccess(skuId, result.basketId);
|
|
1574
1610
|
session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
|
|
1575
1611
|
const userStatus = sessionData.loggedIn ? "lex" : "nex";
|
|
1612
|
+
writeCheckoutRedirect(returnUrl);
|
|
1576
1613
|
window.location.href = redirectToCheckoutComplete({
|
|
1577
1614
|
basketId: result.basketId,
|
|
1578
1615
|
status: userStatus,
|
|
1579
1616
|
skuId: decodeSku(skuId),
|
|
1580
|
-
returnUrl,
|
|
1581
1617
|
queryString
|
|
1582
1618
|
});
|
|
1583
1619
|
} catch (err) {
|
|
@@ -1587,6 +1623,10 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
|
1587
1623
|
}
|
|
1588
1624
|
console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
|
|
1589
1625
|
const walletError = parseWalletApiError(err);
|
|
1626
|
+
console.warn("[web-apple-pay] handlePaymentAuthorized: walletError result", {
|
|
1627
|
+
walletError,
|
|
1628
|
+
validatedEmail
|
|
1629
|
+
});
|
|
1590
1630
|
trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
|
|
1591
1631
|
failApplePaySession(session);
|
|
1592
1632
|
if (!walletError) {
|
|
@@ -2081,7 +2121,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2081
2121
|
if (button) {
|
|
2082
2122
|
button.classList.remove("wap-loading");
|
|
2083
2123
|
this.#updateDisabledState();
|
|
2084
|
-
this.#
|
|
2124
|
+
this.#scheduleRecaptchaTokens();
|
|
2085
2125
|
trackExpressWalletImpression(this.#offer.sku_id, {
|
|
2086
2126
|
entryPoint: this.#entryPoint,
|
|
2087
2127
|
country: this.#country
|
|
@@ -2120,10 +2160,27 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
|
|
|
2120
2160
|
const country = this.#country;
|
|
2121
2161
|
modal.onConfirm = (recaptchaTokens) => {
|
|
2122
2162
|
this.#initiateApplePay(skuId, country, recaptchaTokens);
|
|
2123
|
-
this.#
|
|
2163
|
+
this.#scheduleRecaptchaTokens();
|
|
2124
2164
|
};
|
|
2125
2165
|
modal.open(this.#recaptchaTokensPromise);
|
|
2126
2166
|
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Starts a fresh reCAPTCHA token pre-fetch and stores its promise.
|
|
2169
|
+
* If the fetch rejects, the field is cleared so the next `modal.open()` call
|
|
2170
|
+
* falls back to a new attempt via the modal's own null-fallback path — preventing
|
|
2171
|
+
* a single transient failure from permanently disabling the CTA on all retries.
|
|
2172
|
+
* The identity guard prevents a late rejection from clobbering a newer attempt.
|
|
2173
|
+
*/
|
|
2174
|
+
#scheduleRecaptchaTokens() {
|
|
2175
|
+
const attempt = getPurchaseRecaptchaTokens();
|
|
2176
|
+
attempt.catch((err) => {
|
|
2177
|
+
console.error("[web-apple-pay] reCAPTCHA token pre-fetch failed; will retry on modal open", err);
|
|
2178
|
+
if (this.#recaptchaTokensPromise === attempt) {
|
|
2179
|
+
this.#recaptchaTokensPromise = null;
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
this.#recaptchaTokensPromise = attempt;
|
|
2183
|
+
}
|
|
2127
2184
|
async #fetchSession() {
|
|
2128
2185
|
try {
|
|
2129
2186
|
return await getExpressCheckoutSession();
|
|
@@ -32,6 +32,24 @@ var createFetchMock = (sessionResponse, sessionStatus = 200) => {
|
|
|
32
32
|
headers: { "content-type": "application/json" }
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
if (requestUrl.includes("/v1/wallet/payment-options/sku/")) {
|
|
36
|
+
return new Response(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
options: {
|
|
39
|
+
apple_pay: {
|
|
40
|
+
provider: "apple-pay",
|
|
41
|
+
settings: {
|
|
42
|
+
supported_networks: ["visa", "masterCard", "amex"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
{
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: { "content-type": "application/json" }
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
35
53
|
return new Response("Not Found", {
|
|
36
54
|
status: 404,
|
|
37
55
|
statusText: "Not Found"
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized redirect URL builders for the Express Checkout payment handler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Before redirecting, the origin article URL is persisted to localStorage under
|
|
5
|
+
* the `checkout-redirect` key so that sma-checkout / the onboarding app can
|
|
6
|
+
* return the user to the article after checkout and onboarding complete.
|
|
6
7
|
*
|
|
7
8
|
* Matches sma-checkout: src/lib/navigate-to-checkout/index.tsx
|
|
8
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a URL is safe to use as a post-checkout redirect destination,
|
|
12
|
+
* then writes it to localStorage under `checkout-redirect`.
|
|
13
|
+
*
|
|
14
|
+
* Allowed values:
|
|
15
|
+
* - Relative paths (start with `/`)
|
|
16
|
+
* - Absolute URLs on `economist.com` or any subdomain of it
|
|
17
|
+
*
|
|
18
|
+
* Any other value (e.g. an external domain) is silently rejected to prevent
|
|
19
|
+
* open-redirect vulnerabilities.
|
|
20
|
+
*/
|
|
21
|
+
export declare const writeCheckoutRedirect: (originUrl: string) => void;
|
|
9
22
|
/**
|
|
10
23
|
* Captures the current page URL (the article the user is reading).
|
|
11
24
|
* Must be called before any redirect occurs.
|
|
@@ -29,26 +42,34 @@ export declare const buildQueryString: () => string;
|
|
|
29
42
|
export declare const decodeSku: (sku: string) => string;
|
|
30
43
|
/**
|
|
31
44
|
* Builds the success redirect URL.
|
|
32
|
-
* /checkout/complete?basketId={id}&lmo_offer={sku}&status={userStatus}&redirectToCheckout=true
|
|
45
|
+
* /checkout/complete?basketId={id}&lmo_offer={sku}&status={userStatus}&redirectToCheckout=true
|
|
46
|
+
*
|
|
47
|
+
* The origin article URL is written to localStorage by the caller via
|
|
48
|
+
* writeCheckoutRedirect() before this redirect fires.
|
|
33
49
|
*/
|
|
34
50
|
export declare const redirectToCheckoutComplete: (params: {
|
|
35
51
|
basketId: string;
|
|
36
52
|
status: "nex" | "lex";
|
|
37
53
|
skuId: string;
|
|
38
|
-
returnUrl: string;
|
|
39
54
|
queryString?: string;
|
|
40
55
|
}) => string;
|
|
41
56
|
/**
|
|
42
57
|
* Builds the fallback redirect URL for error scenarios.
|
|
43
|
-
* /checkout?lmo_offer={
|
|
58
|
+
* /checkout?lmo_offer={offerPath}&inlineError=...
|
|
59
|
+
*
|
|
60
|
+
* The skuId is decoded via decodeSku() before being set as lmo_offer, producing
|
|
61
|
+
* a Limio offer path (e.g. /offers2/UK-UK.DIGITAL.1YAR.X.DISC.30.NYT) as
|
|
62
|
+
* checkout expects. If decoding fails (malformed SKU), the raw skuId is used.
|
|
44
63
|
*
|
|
45
64
|
* Builds on top of the current page URL (window.location.href) so that any
|
|
46
65
|
* existing query params (UTM tags, campaign flags) are preserved — matching
|
|
47
66
|
* sma-checkout: src/lib/navigate-to-checkout/index.tsx
|
|
67
|
+
*
|
|
68
|
+
* The origin article URL is written to localStorage by the caller via
|
|
69
|
+
* writeCheckoutRedirect() before this redirect fires.
|
|
48
70
|
*/
|
|
49
71
|
export declare const redirectToCheckoutFallback: (params: {
|
|
50
72
|
skuId: string;
|
|
51
|
-
returnUrl: string;
|
|
52
73
|
inlineError?: string;
|
|
53
74
|
emailError?: string;
|
|
54
75
|
email?: string;
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
// src/utils/expressCheckoutRedirect.ts
|
|
2
|
+
var CHECKOUT_REDIRECT_KEY = "checkout-redirect";
|
|
3
|
+
var ALLOWED_REDIRECT_HOSTNAME = "economist.com";
|
|
4
|
+
var writeCheckoutRedirect = (originUrl) => {
|
|
5
|
+
if (originUrl.startsWith("/")) {
|
|
6
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const { hostname } = new URL(originUrl);
|
|
11
|
+
const isAllowed = hostname === ALLOWED_REDIRECT_HOSTNAME || hostname.endsWith(`.${ALLOWED_REDIRECT_HOSTNAME}`);
|
|
12
|
+
if (isAllowed) {
|
|
13
|
+
localStorage.setItem(CHECKOUT_REDIRECT_KEY, originUrl);
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
};
|
|
2
18
|
var captureOriginUrl = () => window.location.href;
|
|
3
19
|
var buildQueryString = () => {
|
|
4
20
|
const countryCode = new URLSearchParams(window.location.search).get(
|
|
@@ -8,11 +24,8 @@ var buildQueryString = () => {
|
|
|
8
24
|
};
|
|
9
25
|
var decodeSku = (sku) => {
|
|
10
26
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return decoded;
|
|
14
|
-
} catch (err) {
|
|
15
|
-
console.warn("[web-apple-pay] decodeSku: failed to decode, returning raw", { input: sku, err });
|
|
27
|
+
return atob(sku.substring(sku.indexOf("-") + 1));
|
|
28
|
+
} catch {
|
|
16
29
|
return sku;
|
|
17
30
|
}
|
|
18
31
|
};
|
|
@@ -26,24 +39,16 @@ var redirectToCheckoutComplete = (params) => {
|
|
|
26
39
|
url.searchParams.set("lmo_offer", params.skuId);
|
|
27
40
|
url.searchParams.set("status", params.status);
|
|
28
41
|
url.searchParams.set("redirectToCheckout", "true");
|
|
29
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
30
42
|
return url.pathname + url.search;
|
|
31
43
|
};
|
|
32
44
|
var redirectToCheckoutFallback = (params) => {
|
|
33
|
-
const decodedOffer = decodeSku(params.skuId);
|
|
34
|
-
console.log("[web-apple-pay] redirectToCheckoutFallback", {
|
|
35
|
-
rawSkuId: params.skuId,
|
|
36
|
-
decodedOffer
|
|
37
|
-
});
|
|
38
45
|
const url = new URL(window.location.href);
|
|
39
46
|
url.pathname = "/checkout";
|
|
40
|
-
url.searchParams.set("lmo_offer",
|
|
47
|
+
url.searchParams.set("lmo_offer", decodeSku(params.skuId));
|
|
41
48
|
if (params.inlineError)
|
|
42
49
|
url.searchParams.set("inlineError", params.inlineError);
|
|
43
50
|
if (params.email) url.searchParams.set("email", params.email);
|
|
44
51
|
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
45
|
-
url.searchParams.set("returnUrl", params.returnUrl);
|
|
46
|
-
console.log("[web-apple-pay] fallback redirect URL", url.pathname + url.search);
|
|
47
52
|
return url.pathname + url.search;
|
|
48
53
|
};
|
|
49
54
|
export {
|
|
@@ -51,5 +56,6 @@ export {
|
|
|
51
56
|
captureOriginUrl,
|
|
52
57
|
decodeSku,
|
|
53
58
|
redirectToCheckoutComplete,
|
|
54
|
-
redirectToCheckoutFallback
|
|
59
|
+
redirectToCheckoutFallback,
|
|
60
|
+
writeCheckoutRedirect
|
|
55
61
|
};
|
package/package.json
CHANGED