@economist/web-apple-pay 1.6.2-beta.1 → 1.7.1

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
@@ -317,7 +317,6 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
317
317
  const offer = this.#offer;
318
318
  const country = this.#country;
319
319
  if (!offer) {
320
- console.error("ApplePayModal: Missing or invalid offer data");
321
320
  this.remove();
322
321
  return;
323
322
  }
@@ -439,17 +438,95 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
439
438
  };
440
439
 
441
440
  // src/templates/apple-pay-button.template.html
442
- var apple_pay_button_template_default = '<style>\n /* Use CSS variables for layout so parent walls can easily override placement and sizing */\n teg-apple-pay-button {\n display: block;\n margin: var(--apple-pay-margin, 0);\n width: 100%;\n height: 100%;\n }\n\n teg-apple-pay-button .apple-pay-container {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n height: 100%;\n }\n\n teg-apple-pay-button .apple-pay-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.375rem;\n width: 100%;\n min-width: 0;\n height: 100%;\n min-height: var(--apple-pay-height, 2.75rem);\n background-color: #000;\n color: #fff;\n border: none;\n border-radius: var(--apple-pay-border-radius, 1.375rem);\n cursor: pointer;\n font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));\n font-size: 1.0625rem;\n font-weight: 500;\n }\n\n teg-apple-pay-button .apple-pay-btn svg {\n flex-shrink: 0;\n display: block;\n }\n</style>\n\n<div class="apple-pay-container" aria-label="Express checkout with Apple Pay">\n <button\n class="apple-pay-btn"\n type="button"\n aria-label="Pay with Apple Pay"\n data-analytics="paywall:apple-pay-checkout"\n >\n <svg width="51" height="22" aria-label="Apple Pay" role="img">\n <use href="#apple-pay-logo" />\n </svg>\n </button>\n</div>\n';
441
+ var apple_pay_button_template_default = `<style>
442
+ /* Use CSS variables for layout so parent walls can easily override placement and sizing */
443
+ teg-apple-pay-button {
444
+ display: block;
445
+ margin: var(--apple-pay-margin, 0);
446
+ width: 100%;
447
+ height: 100%;
448
+ }
449
+
450
+ teg-apple-pay-button .apple-pay-container {
451
+ display: flex;
452
+ flex-direction: column;
453
+ align-items: stretch;
454
+ width: 100%;
455
+ height: 100%;
456
+ }
457
+
458
+ teg-apple-pay-button .apple-pay-btn {
459
+ display: flex;
460
+ align-items: center;
461
+ justify-content: center;
462
+ gap: 0.375rem;
463
+ width: 100%;
464
+ min-width: 0;
465
+ height: 100%;
466
+ min-height: var(--apple-pay-height, 2.75rem);
467
+ background-color: #000;
468
+ color: #fff;
469
+ border: none;
470
+ border-radius: var(--apple-pay-border-radius, 1.375rem);
471
+ cursor: pointer;
472
+ font-family: var(--mb-typeface-sans, var(--wall-type-system-sans));
473
+ font-size: 1.0625rem;
474
+ font-weight: 500;
475
+ }
476
+
477
+ teg-apple-pay-button .apple-pay-btn svg {
478
+ flex-shrink: 0;
479
+ display: block;
480
+ }
481
+
482
+ teg-apple-pay-button .apple-pay-btn.wap-loading {
483
+ position: relative;
484
+ cursor: default;
485
+ }
486
+
487
+ teg-apple-pay-button .apple-pay-btn.wap-loading svg {
488
+ visibility: hidden;
489
+ }
490
+
491
+ teg-apple-pay-button .apple-pay-btn.wap-loading::after {
492
+ content: '';
493
+ position: absolute;
494
+ left: 50%;
495
+ top: 50%;
496
+ margin-left: -0.625rem;
497
+ margin-top: -0.625rem;
498
+ width: 1.25rem;
499
+ height: 1.25rem;
500
+ border: 2px solid rgba(255, 255, 255, 0.2);
501
+ border-top-color: #fff;
502
+ border-radius: 50%;
503
+ animation: wap-button-spinner 0.8s linear infinite;
504
+ }
505
+
506
+ @keyframes wap-button-spinner {
507
+ to {
508
+ transform: rotate(360deg);
509
+ }
510
+ }
511
+ </style>
512
+
513
+ <div class="apple-pay-container" aria-label="Express checkout with Apple Pay">
514
+ <button
515
+ class="apple-pay-btn"
516
+ type="button"
517
+ aria-label="Pay with Apple Pay"
518
+ data-analytics="paywall:apple-pay-checkout"
519
+ >
520
+ <svg width="51" height="22" aria-label="Apple Pay" role="img">
521
+ <use href="#apple-pay-logo" />
522
+ </svg>
523
+ </button>
524
+ </div>
525
+ `;
443
526
 
444
527
  // src/clients/payment-checkout/config.ts
445
528
  var runtimeConfig = null;
446
529
  var configureApplePay = (config) => {
447
- console.log("[web-apple-pay] configureApplePay called", {
448
- merchantId: config.merchantId,
449
- walletApiBaseUrl: config.walletApiBaseUrl,
450
- debugBypassEnabled: config.debugBypassEnabled,
451
- recaptchaSiteKey: config.recaptchaSiteKey ? "***" : void 0
452
- });
453
530
  runtimeConfig = {
454
531
  ...config
455
532
  };
@@ -464,27 +541,21 @@ var isAppleDeviceOrSafari = () => {
464
541
  };
465
542
  var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
466
543
  var isApplePayAvailable = async () => {
467
- console.log("[web-apple-pay] isApplePayAvailable: checking availability");
468
544
  if (bypassApplePayChecks()) {
469
- console.log("[web-apple-pay] isApplePayAvailable: bypass active \u2192 true");
470
545
  return true;
471
546
  }
472
547
  if (!isAppleDeviceOrSafari()) {
473
- console.log("[web-apple-pay] isApplePayAvailable: not Apple device or Safari \u2192 false");
474
548
  return false;
475
549
  }
476
550
  const applePaySession = window.ApplePaySession;
477
551
  if (!applePaySession) {
478
- console.log("[web-apple-pay] isApplePayAvailable: ApplePaySession not found on window \u2192 false");
479
552
  return false;
480
553
  }
481
554
  try {
482
555
  if (typeof applePaySession.canMakePayments !== "function") {
483
- console.log("[web-apple-pay] isApplePayAvailable: canMakePayments is not a function \u2192 false");
484
556
  return false;
485
557
  }
486
558
  if (!applePaySession.canMakePayments()) {
487
- console.log("[web-apple-pay] isApplePayAvailable: canMakePayments() returned false \u2192 false");
488
559
  return false;
489
560
  }
490
561
  const merchantId = getApplePayConfig()?.merchantId;
@@ -494,10 +565,7 @@ var isApplePayAvailable = async () => {
494
565
  );
495
566
  return false;
496
567
  }
497
- console.log("[web-apple-pay] isApplePayAvailable : calling canMakePaymentsWithActiveCard", { merchantId });
498
- const result = await applePaySession.canMakePaymentsWithActiveCard(merchantId);
499
- console.log("[web-apple-pay] isApplePayAvailable: canMakePaymentsWithActiveCard \u2192", result);
500
- return result;
568
+ return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
501
569
  } catch (error) {
502
570
  console.error("Error checking Apple Pay active card status:", error);
503
571
  return false;
@@ -758,14 +826,39 @@ var MSG_EMAIL_COLLISION = "This email address is already linked to an account. P
758
826
  var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
759
827
 
760
828
  // src/clients/payment-checkout/apple-session.ts
761
- function startApplePaySession(config) {
762
- console.log("[web-apple-pay] startApplePaySession", {
829
+ function redirectToFallbackCheckout(config) {
830
+ trackFallbackCheckout(config.skuId);
831
+ window.location.href = redirectToCheckoutFallback({
763
832
  skuId: config.skuId,
764
- countryCode: config.countryCode,
765
- currencyCode: config.currencyCode,
766
- totalAmount: config.totalAmount,
767
- userLoggedIn: config.userLoggedIn
833
+ returnUrl: config.returnUrl,
834
+ inlineError: MSG_PAYMENT_GENERIC_ERROR
768
835
  });
836
+ }
837
+ async function handleMerchantValidation(event, session, config) {
838
+ try {
839
+ const validationHost = new URL(event.validationURL).host;
840
+ if (!validationHost.endsWith("apple.com")) {
841
+ session.abort();
842
+ redirectToFallbackCheckout(config);
843
+ return;
844
+ }
845
+ const recaptchaToken = await getRecaptchaToken("verifyMerchant");
846
+ const response = await validateAppleMerchant({
847
+ validationURL: event.validationURL,
848
+ recaptchaToken
849
+ });
850
+ if (!response?.merchantSession) {
851
+ session.abort();
852
+ redirectToFallbackCheckout(config);
853
+ return;
854
+ }
855
+ session.completeMerchantValidation(response.merchantSession);
856
+ } catch {
857
+ session.abort();
858
+ redirectToFallbackCheckout(config);
859
+ }
860
+ }
861
+ function startApplePaySession(config) {
769
862
  const paymentRequest = {
770
863
  countryCode: config.countryCode,
771
864
  currencyCode: config.currencyCode,
@@ -779,64 +872,23 @@ function startApplePaySession(config) {
779
872
  // Only request email from Apple Pay sheet if user is NOT logged in
780
873
  ...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
781
874
  };
782
- const session = new ApplePaySession(3, paymentRequest);
783
- session.onvalidatemerchant = async (event) => {
784
- console.log("[web-apple-pay] onvalidatemerchant: received", { validationURL: event.validationURL });
785
- try {
786
- const validationHost = new URL(event.validationURL).host;
787
- if (!validationHost.endsWith("apple.com")) {
788
- console.warn("[web-apple-pay] onvalidatemerchant: invalid host, aborting", { validationHost });
789
- trackFallbackCheckout(config.skuId);
790
- session.abort();
791
- window.location.href = redirectToCheckoutFallback({
792
- skuId: config.skuId,
793
- returnUrl: config.returnUrl,
794
- inlineError: MSG_PAYMENT_GENERIC_ERROR
795
- });
796
- return;
797
- }
798
- const recaptchaToken = await getRecaptchaToken("verifyMerchant");
799
- console.log("[web-apple-pay] onvalidatemerchant: calling validateAppleMerchant");
800
- const response = await validateAppleMerchant({
801
- validationURL: event.validationURL,
802
- recaptchaToken
803
- });
804
- if (!response?.merchantSession) {
805
- console.warn("[web-apple-pay] onvalidatemerchant: no merchantSession in response, aborting");
806
- trackFallbackCheckout(config.skuId);
807
- session.abort();
808
- window.location.href = redirectToCheckoutFallback({
809
- skuId: config.skuId,
810
- returnUrl: config.returnUrl,
811
- inlineError: MSG_PAYMENT_GENERIC_ERROR
812
- });
813
- return;
814
- }
815
- console.log("[web-apple-pay] onvalidatemerchant: merchant validated successfully");
816
- session.completeMerchantValidation(response.merchantSession);
817
- } catch (err) {
818
- console.error("[web-apple-pay] onvalidatemerchant: error during validation", err);
819
- trackFallbackCheckout(config.skuId);
820
- session.abort();
821
- window.location.href = redirectToCheckoutFallback({
822
- skuId: config.skuId,
823
- returnUrl: config.returnUrl,
824
- inlineError: MSG_PAYMENT_GENERIC_ERROR
825
- });
826
- }
827
- };
828
- session.onpaymentauthorized = (event) => {
829
- console.log("[web-apple-pay] onpaymentauthorized: payment authorized by user", { skuId: config.skuId });
830
- trackWalletPaymentAuthorised(config.skuId);
831
- config.onPaymentAuthorized(event, session);
832
- };
833
- session.oncancel = () => {
834
- console.log("[web-apple-pay] oncancel: payment sheet cancelled by user", { skuId: config.skuId });
835
- config.onCancel?.();
836
- };
837
- console.log("[web-apple-pay] startApplePaySession: beginning session");
838
- trackWalletPaymentSheetOpen(config.skuId);
839
- session.begin();
875
+ try {
876
+ const session = new ApplePaySession(3, paymentRequest);
877
+ session.onvalidatemerchant = async (event) => {
878
+ await handleMerchantValidation(event, session, config);
879
+ };
880
+ session.onpaymentauthorized = (event) => {
881
+ trackWalletPaymentAuthorised(config.skuId);
882
+ config.onPaymentAuthorized(event, session);
883
+ };
884
+ session.oncancel = () => {
885
+ config.onCancel?.();
886
+ };
887
+ trackWalletPaymentSheetOpen(config.skuId);
888
+ session.begin();
889
+ } catch {
890
+ redirectToFallbackCheckout(config);
891
+ }
840
892
  }
841
893
 
842
894
  // src/clients/payment-checkout/contact-validation.ts
@@ -1013,16 +1065,12 @@ function handleStructuredError(ctx) {
1013
1065
  async function handlePaymentAuthorized(event, session, sessionData, config) {
1014
1066
  const { skuId, returnUrl, queryString } = config;
1015
1067
  let validatedEmail;
1016
- console.log("[web-apple-pay] handlePaymentAuthorized: starting", { skuId, userLoggedIn: sessionData.loggedIn });
1017
1068
  try {
1018
- console.log("[web-apple-pay] handlePaymentAuthorized: step 1 \u2014 validating contacts");
1019
1069
  validatedEmail = validateContacts(
1020
1070
  event,
1021
1071
  sessionData,
1022
1072
  config.supportedBillingCountries
1023
1073
  );
1024
- console.log("[web-apple-pay] handlePaymentAuthorized: contacts valid", { validatedEmail });
1025
- console.log("[web-apple-pay] handlePaymentAuthorized: step 2 \u2014 calling performPurchase", { skuId });
1026
1074
  const result = await performPurchase({
1027
1075
  applePayToken: event.payment.token,
1028
1076
  skuId: config.skuId,
@@ -1032,8 +1080,6 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1032
1080
  },
1033
1081
  recaptchaTokens: config.recaptchaTokens
1034
1082
  });
1035
- console.log("[web-apple-pay] handlePaymentAuthorized: performPurchase succeeded", { basketId: result.basketId });
1036
- console.log("[web-apple-pay] handlePaymentAuthorized: step 3 \u2014 completing payment and redirecting");
1037
1083
  trackWalletPaymentSuccess(skuId, result.basketId);
1038
1084
  session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
1039
1085
  const userStatus = sessionData.loggedIn ? "lex" : "nex";
@@ -1046,25 +1092,17 @@ async function handlePaymentAuthorized(event, session, sessionData, config) {
1046
1092
  });
1047
1093
  } catch (err) {
1048
1094
  if (err instanceof ContactValidationError) {
1049
- console.warn("[web-apple-pay] handlePaymentAuthorized: contact validation failed", err.errors);
1050
1095
  failApplePaySession(session, err.errors);
1051
1096
  return;
1052
1097
  }
1053
1098
  console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
1054
1099
  const walletError = parseWalletApiError(err);
1055
- console.error("[web-apple-pay] handlePaymentAuthorized: BFF/network error", {
1056
- skuId,
1057
- errorType: walletError?.errorType ?? "UNKNOWN",
1058
- message: walletError?.message
1059
- });
1060
1100
  trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
1061
1101
  failApplePaySession(session);
1062
1102
  if (!walletError) {
1063
- console.warn("[web-apple-pay] handlePaymentAuthorized: unstructured error, using generic fallback");
1064
1103
  redirectFallback(skuId, returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR });
1065
1104
  return;
1066
1105
  }
1067
- console.log("[web-apple-pay] handlePaymentAuthorized: dispatching structured error handler", { errorType: walletError.errorType });
1068
1106
  handleStructuredError({ skuId, returnUrl, validatedEmail, walletError });
1069
1107
  }
1070
1108
  }
@@ -1351,11 +1389,15 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1351
1389
  * Creates and configures an ApplePayButton if the feature flag is present.
1352
1390
  */
1353
1391
  static createIfEnabled(offer, country, entryPoint) {
1354
- if (!offer) return null;
1355
- if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant))
1392
+ if (!offer) {
1356
1393
  return null;
1357
- if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT))
1394
+ }
1395
+ if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant)) {
1358
1396
  return null;
1397
+ }
1398
+ if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT)) {
1399
+ return null;
1400
+ }
1359
1401
  _ApplePayButton.define();
1360
1402
  const button = document.createElement(_ApplePayButton.tag);
1361
1403
  button.offer = offer;
@@ -1382,6 +1424,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1382
1424
  #returnUrl = "";
1383
1425
  #supportedBillingCountries = [];
1384
1426
  #entryPoint = "";
1427
+ #recaptchaTokens = null;
1385
1428
  // ─── Public API ─────────────────────────────────────────────────────────────
1386
1429
  get offer() {
1387
1430
  return this.#offer;
@@ -1452,14 +1495,16 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1452
1495
  async #run() {
1453
1496
  try {
1454
1497
  if (await this.#checkAvailability()) return;
1455
- if (await this.#checkSession()) return;
1456
1498
  if (this.#checkOffer()) return;
1499
+ this.#renderTemplate();
1500
+ if (await this.#checkSession()) return;
1457
1501
  this.#captureReturnUrl();
1458
1502
  this.#computeBillingCountries();
1459
1503
  if (await this.#fetchAndCacheOptions()) return;
1460
- this.#renderTemplate();
1504
+ this.#finishLoading();
1461
1505
  } catch {
1462
1506
  this.style.display = "none";
1507
+ this.querySelector(SEL_CONTAINER)?.remove();
1463
1508
  } finally {
1464
1509
  this.#rendering = false;
1465
1510
  }
@@ -1481,6 +1526,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1481
1526
  this.#sessionData = await this.#fetchSession();
1482
1527
  if (this.#sessionData?.loggedIn && (this.#sessionData.userType === "active-subscription" || this.#sessionData.userType === "b2b-user")) {
1483
1528
  this.style.display = "none";
1529
+ this.querySelector(SEL_CONTAINER)?.remove();
1484
1530
  return true;
1485
1531
  }
1486
1532
  return false;
@@ -1514,8 +1560,9 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1514
1560
  */
1515
1561
  async #fetchAndCacheOptions() {
1516
1562
  this.#paymentOptions = await this.#fetchPaymentOptions();
1517
- if (!this.#paymentOptions?.apple_pay) {
1563
+ if (!this.#paymentOptions?.options?.apple_pay) {
1518
1564
  this.style.display = "none";
1565
+ this.querySelector(SEL_CONTAINER)?.remove();
1519
1566
  return true;
1520
1567
  }
1521
1568
  return false;
@@ -1527,19 +1574,30 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1527
1574
  const template = document.createElement("template");
1528
1575
  template.innerHTML = apple_pay_button_template_default;
1529
1576
  this.appendChild(template.content.cloneNode(true));
1530
- this.#updateDisabledState();
1531
- trackExpressWalletImpression(this.#offer.sku_id, {
1532
- entryPoint: this.#entryPoint,
1533
- country: this.#country
1534
- });
1535
1577
  const button = this.querySelector(SEL_BUTTON);
1578
+ button.classList.add("wap-loading");
1579
+ button.setAttribute("disabled", "");
1580
+ button.setAttribute("aria-disabled", "true");
1581
+ button.setAttribute("aria-busy", "true");
1536
1582
  button.addEventListener("click", () => {
1583
+ if (button.classList.contains("wap-loading")) return;
1537
1584
  this.#openModal();
1538
1585
  });
1539
- this.addEventListener("apple-pay-confirmed", (event) => {
1586
+ document.addEventListener("apple-pay-confirmed", (event) => {
1540
1587
  const { skuId, country } = event.detail;
1541
1588
  this.#initiateApplePay(skuId, country);
1542
- });
1589
+ }, { once: true });
1590
+ }
1591
+ #finishLoading() {
1592
+ const button = this.querySelector(SEL_BUTTON);
1593
+ if (button) {
1594
+ button.classList.remove("wap-loading");
1595
+ this.#updateDisabledState();
1596
+ trackExpressWalletImpression(this.#offer.sku_id, {
1597
+ entryPoint: this.#entryPoint,
1598
+ country: this.#country
1599
+ });
1600
+ }
1543
1601
  }
1544
1602
  // ─── Private helpers ────────────────────────────────────────────────────────
1545
1603
  #updateDisabledState() {
@@ -1548,14 +1606,24 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1548
1606
  if (this.#offer) {
1549
1607
  button.removeAttribute("disabled");
1550
1608
  button.removeAttribute("aria-disabled");
1609
+ button.removeAttribute("aria-busy");
1551
1610
  return;
1552
1611
  }
1553
1612
  button.setAttribute("disabled", "");
1554
1613
  button.setAttribute("aria-disabled", "true");
1555
1614
  }
1556
1615
  #openModal() {
1557
- if (!this.#offer || document.querySelector(ApplePayModal.tag)) return;
1616
+ if (!this.#offer) {
1617
+ return;
1618
+ }
1619
+ if (document.querySelector(ApplePayModal.tag)) {
1620
+ return;
1621
+ }
1558
1622
  trackExpressWalletClick(this.#offer.sku_id);
1623
+ getPurchaseRecaptchaTokens().then((tokens) => {
1624
+ this.#recaptchaTokens = tokens;
1625
+ }).catch(() => {
1626
+ });
1559
1627
  ApplePayModal.define();
1560
1628
  const modal = document.createElement(ApplePayModal.tag);
1561
1629
  modal.offer = this.#offer;
@@ -1590,7 +1658,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1590
1658
  return {
1591
1659
  countryCode: country,
1592
1660
  currencyCode: offer.price?.currency_code ?? DEFAULT_CURRENCY,
1593
- supportedNetworks: paymentOptions.apple_pay.settings.supported_networks,
1661
+ supportedNetworks: paymentOptions.options.apple_pay.settings.supported_networks,
1594
1662
  totalAmount,
1595
1663
  totalLabel: TOTAL_LABEL,
1596
1664
  skuId,
@@ -1610,7 +1678,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1610
1678
  try {
1611
1679
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
1612
1680
  const paymentOptions = this.#paymentOptions;
1613
- const recaptchaTokens = await getPurchaseRecaptchaTokens();
1681
+ const recaptchaTokens = this.#recaptchaTokens ?? await getPurchaseRecaptchaTokens();
1614
1682
  startApplePaySession(
1615
1683
  this.#buildSessionParams(
1616
1684
  skuId,