@economist/web-apple-pay 0.1.3-beta.5 → 0.1.3-beta.7

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,7 +438,91 @@ 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;
@@ -459,20 +542,25 @@ var isAppleDeviceOrSafari = () => {
459
542
  var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
460
543
  var isApplePayAvailable = async () => {
461
544
  if (bypassApplePayChecks()) {
545
+ console.debug("[web-apple-pay] isApplePayAvailable: bypass enabled, returning true");
462
546
  return true;
463
547
  }
464
548
  if (!isAppleDeviceOrSafari()) {
549
+ console.debug("[web-apple-pay] isApplePayAvailable: not an Apple device or Safari", { userAgent: navigator.userAgent });
465
550
  return false;
466
551
  }
467
552
  const applePaySession = window.ApplePaySession;
468
553
  if (!applePaySession) {
554
+ console.debug("[web-apple-pay] isApplePayAvailable: ApplePaySession not present on window");
469
555
  return false;
470
556
  }
471
557
  try {
472
558
  if (typeof applePaySession.canMakePayments !== "function") {
559
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePayments is not a function");
473
560
  return false;
474
561
  }
475
562
  if (!applePaySession.canMakePayments()) {
563
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePayments() returned false");
476
564
  return false;
477
565
  }
478
566
  const merchantId = getApplePayConfig()?.merchantId;
@@ -482,7 +570,9 @@ var isApplePayAvailable = async () => {
482
570
  );
483
571
  return false;
484
572
  }
485
- return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
573
+ const hasActiveCard = await applePaySession.canMakePaymentsWithActiveCard(merchantId);
574
+ console.debug("[web-apple-pay] isApplePayAvailable: canMakePaymentsWithActiveCard =", hasActiveCard, { merchantId });
575
+ return hasActiveCard;
486
576
  } catch (error) {
487
577
  console.error("Error checking Apple Pay active card status:", error);
488
578
  return false;
@@ -744,7 +834,52 @@ var MSG_EMAIL_COLLISION = "This email address is already linked to an account. P
744
834
  var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
745
835
 
746
836
  // src/clients/payment-checkout/apple-session.ts
837
+ function redirectToFallbackCheckout(config) {
838
+ trackFallbackCheckout(config.skuId);
839
+ window.location.href = redirectToCheckoutFallback({
840
+ skuId: config.skuId,
841
+ returnUrl: config.returnUrl,
842
+ inlineError: MSG_PAYMENT_GENERIC_ERROR
843
+ });
844
+ }
845
+ async function handleMerchantValidation(event, session, config) {
846
+ console.debug("[web-apple-pay] onvalidatemerchant: validating", { validationURL: event.validationURL });
847
+ try {
848
+ const validationHost = new URL(event.validationURL).host;
849
+ if (!validationHost.endsWith("apple.com")) {
850
+ console.debug("[web-apple-pay] onvalidatemerchant: invalid host, aborting", { validationHost });
851
+ session.abort();
852
+ redirectToFallbackCheckout(config);
853
+ return;
854
+ }
855
+ const recaptchaToken = await getRecaptchaToken("verifyMerchant");
856
+ const response = await validateAppleMerchant({
857
+ validationURL: event.validationURL,
858
+ recaptchaToken
859
+ });
860
+ if (!response?.merchantSession) {
861
+ console.debug("[web-apple-pay] onvalidatemerchant: no merchantSession in response, aborting");
862
+ session.abort();
863
+ redirectToFallbackCheckout(config);
864
+ return;
865
+ }
866
+ console.debug("[web-apple-pay] onvalidatemerchant: merchant validated successfully");
867
+ session.completeMerchantValidation(response.merchantSession);
868
+ } catch (err) {
869
+ console.error("[web-apple-pay] onvalidatemerchant: error during validation", err);
870
+ session.abort();
871
+ redirectToFallbackCheckout(config);
872
+ }
873
+ }
747
874
  function startApplePaySession(config) {
875
+ console.debug("[web-apple-pay] startApplePaySession: starting session", {
876
+ skuId: config.skuId,
877
+ countryCode: config.countryCode,
878
+ currencyCode: config.currencyCode,
879
+ supportedNetworks: config.supportedNetworks,
880
+ totalAmount: config.totalAmount,
881
+ userLoggedIn: config.userLoggedIn
882
+ });
748
883
  const paymentRequest = {
749
884
  countryCode: config.countryCode,
750
885
  currencyCode: config.currencyCode,
@@ -758,55 +893,27 @@ function startApplePaySession(config) {
758
893
  // Only request email from Apple Pay sheet if user is NOT logged in
759
894
  ...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
760
895
  };
761
- const session = new ApplePaySession(3, paymentRequest);
762
- session.onvalidatemerchant = async (event) => {
763
- try {
764
- const validationHost = new URL(event.validationURL).host;
765
- if (!validationHost.endsWith("apple.com")) {
766
- trackFallbackCheckout(config.skuId);
767
- session.abort();
768
- window.location.href = redirectToCheckoutFallback({
769
- skuId: config.skuId,
770
- returnUrl: config.returnUrl,
771
- inlineError: MSG_PAYMENT_GENERIC_ERROR
772
- });
773
- return;
774
- }
775
- const recaptchaToken = await getRecaptchaToken("verifyMerchant");
776
- const response = await validateAppleMerchant({
777
- validationURL: event.validationURL,
778
- recaptchaToken
779
- });
780
- if (!response?.merchantSession) {
781
- trackFallbackCheckout(config.skuId);
782
- session.abort();
783
- window.location.href = redirectToCheckoutFallback({
784
- skuId: config.skuId,
785
- returnUrl: config.returnUrl,
786
- inlineError: MSG_PAYMENT_GENERIC_ERROR
787
- });
788
- return;
789
- }
790
- session.completeMerchantValidation(response.merchantSession);
791
- } catch {
792
- trackFallbackCheckout(config.skuId);
793
- session.abort();
794
- window.location.href = redirectToCheckoutFallback({
795
- skuId: config.skuId,
796
- returnUrl: config.returnUrl,
797
- inlineError: MSG_PAYMENT_GENERIC_ERROR
798
- });
799
- }
800
- };
801
- session.onpaymentauthorized = (event) => {
802
- trackWalletPaymentAuthorised(config.skuId);
803
- config.onPaymentAuthorized(event, session);
804
- };
805
- session.oncancel = () => {
806
- config.onCancel?.();
807
- };
808
- trackWalletPaymentSheetOpen(config.skuId);
809
- session.begin();
896
+ try {
897
+ const session = new ApplePaySession(3, paymentRequest);
898
+ session.onvalidatemerchant = async (event) => {
899
+ await handleMerchantValidation(event, session, config);
900
+ };
901
+ session.onpaymentauthorized = (event) => {
902
+ console.debug("[web-apple-pay] onpaymentauthorized: payment authorised");
903
+ trackWalletPaymentAuthorised(config.skuId);
904
+ config.onPaymentAuthorized(event, session);
905
+ };
906
+ session.oncancel = () => {
907
+ console.debug("[web-apple-pay] oncancel: user cancelled Apple Pay sheet");
908
+ config.onCancel?.();
909
+ };
910
+ console.debug("[web-apple-pay] startApplePaySession: session.begin()");
911
+ trackWalletPaymentSheetOpen(config.skuId);
912
+ session.begin();
913
+ } catch (err) {
914
+ console.error("[web-apple-pay] startApplePaySession: failed to create session", err);
915
+ redirectToFallbackCheckout(config);
916
+ }
810
917
  }
811
918
 
812
919
  // src/clients/payment-checkout/contact-validation.ts
@@ -1309,11 +1416,15 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1309
1416
  * Creates and configures an ApplePayButton if the feature flag is present.
1310
1417
  */
1311
1418
  static createIfEnabled(offer, country, entryPoint) {
1312
- if (!offer) return null;
1313
- if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant))
1419
+ if (!offer) {
1314
1420
  return null;
1315
- if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT))
1421
+ }
1422
+ if (APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant)) {
1423
+ return null;
1424
+ }
1425
+ if (!new URLSearchParams(window.location.search).has(FEATURE_APPLE_PAY_EXPRESS_CHECKOUT)) {
1316
1426
  return null;
1427
+ }
1317
1428
  _ApplePayButton.define();
1318
1429
  const button = document.createElement(_ApplePayButton.tag);
1319
1430
  button.offer = offer;
@@ -1340,6 +1451,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1340
1451
  #returnUrl = "";
1341
1452
  #supportedBillingCountries = [];
1342
1453
  #entryPoint = "";
1454
+ #recaptchaTokens = null;
1343
1455
  // ─── Public API ─────────────────────────────────────────────────────────────
1344
1456
  get offer() {
1345
1457
  return this.#offer;
@@ -1408,16 +1520,20 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1408
1520
  }
1409
1521
  // ─── Render pipeline ────────────────────────────────────────────────────────
1410
1522
  async #run() {
1523
+ console.debug("[web-apple-pay] #run: start");
1411
1524
  try {
1412
1525
  if (await this.#checkAvailability()) return;
1413
- if (await this.#checkSession()) return;
1414
1526
  if (this.#checkOffer()) return;
1527
+ this.#renderTemplate();
1528
+ if (await this.#checkSession()) return;
1415
1529
  this.#captureReturnUrl();
1416
1530
  this.#computeBillingCountries();
1417
1531
  if (await this.#fetchAndCacheOptions()) return;
1418
- this.#renderTemplate();
1419
- } catch {
1532
+ this.#finishLoading();
1533
+ } catch (err) {
1534
+ console.error("[web-apple-pay] #run: unexpected error, hiding button", err);
1420
1535
  this.style.display = "none";
1536
+ this.querySelector(SEL_CONTAINER)?.remove();
1421
1537
  } finally {
1422
1538
  this.#rendering = false;
1423
1539
  }
@@ -1426,9 +1542,11 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1426
1542
  async #checkAvailability() {
1427
1543
  const available = await isApplePayAvailable();
1428
1544
  if (!available) {
1545
+ console.debug("[web-apple-pay] #checkAvailability: not available on this device/browser");
1429
1546
  this.style.display = "none";
1430
1547
  return true;
1431
1548
  }
1549
+ console.debug("[web-apple-pay] #checkAvailability: ok");
1432
1550
  return false;
1433
1551
  }
1434
1552
  /**
@@ -1437,18 +1555,24 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1437
1555
  */
1438
1556
  async #checkSession() {
1439
1557
  this.#sessionData = await this.#fetchSession();
1558
+ console.debug("[web-apple-pay] #checkSession: session data", { loggedIn: this.#sessionData?.loggedIn, userType: this.#sessionData?.userType });
1440
1559
  if (this.#sessionData?.loggedIn && (this.#sessionData.userType === "active-subscription" || this.#sessionData.userType === "b2b-user")) {
1560
+ console.debug("[web-apple-pay] #checkSession: ineligible user, hiding button", { userType: this.#sessionData.userType });
1441
1561
  this.style.display = "none";
1562
+ this.querySelector(SEL_CONTAINER)?.remove();
1442
1563
  return true;
1443
1564
  }
1565
+ console.debug("[web-apple-pay] #checkSession: ok");
1444
1566
  return false;
1445
1567
  }
1446
1568
  /** Returns `true` (hide) when no offer has been set before mount. */
1447
1569
  #checkOffer() {
1448
1570
  if (!this.#offer) {
1571
+ console.debug("[web-apple-pay] #checkOffer: no offer set, hiding button");
1449
1572
  this.style.display = "none";
1450
1573
  return true;
1451
1574
  }
1575
+ console.debug("[web-apple-pay] #checkOffer: ok", { skuId: this.#offer.sku_id });
1452
1576
  return false;
1453
1577
  }
1454
1578
  /** Captures the current page URL once at mount for reuse across the session lifetime. */
@@ -1472,32 +1596,49 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1472
1596
  */
1473
1597
  async #fetchAndCacheOptions() {
1474
1598
  this.#paymentOptions = await this.#fetchPaymentOptions();
1475
- if (!this.#paymentOptions?.apple_pay) {
1599
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: payment options", this.#paymentOptions);
1600
+ if (!this.#paymentOptions?.options?.apple_pay) {
1601
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: apple_pay absent in options, hiding button");
1476
1602
  this.style.display = "none";
1603
+ this.querySelector(SEL_CONTAINER)?.remove();
1477
1604
  return true;
1478
1605
  }
1606
+ console.debug("[web-apple-pay] #fetchAndCacheOptions: ok", this.#paymentOptions.options.apple_pay);
1479
1607
  return false;
1480
1608
  }
1481
1609
  /** Injects the button template and wires click and confirm event listeners. */
1482
1610
  #renderTemplate() {
1611
+ console.debug("[web-apple-pay] #renderTemplate: injecting button template");
1483
1612
  this.style.removeProperty("display");
1484
1613
  ensureApplePayLogoSymbol();
1485
1614
  const template = document.createElement("template");
1486
1615
  template.innerHTML = apple_pay_button_template_default;
1487
1616
  this.appendChild(template.content.cloneNode(true));
1488
- this.#updateDisabledState();
1489
- trackExpressWalletImpression(this.#offer.sku_id, {
1490
- entryPoint: this.#entryPoint,
1491
- country: this.#country
1492
- });
1493
1617
  const button = this.querySelector(SEL_BUTTON);
1618
+ button.classList.add("wap-loading");
1619
+ button.setAttribute("disabled", "");
1620
+ button.setAttribute("aria-disabled", "true");
1621
+ button.setAttribute("aria-busy", "true");
1494
1622
  button.addEventListener("click", () => {
1623
+ if (button.classList.contains("wap-loading")) return;
1495
1624
  this.#openModal();
1496
1625
  });
1497
- this.addEventListener("apple-pay-confirmed", (event) => {
1626
+ document.addEventListener("apple-pay-confirmed", (event) => {
1498
1627
  const { skuId, country } = event.detail;
1499
1628
  this.#initiateApplePay(skuId, country);
1500
- });
1629
+ }, { once: true });
1630
+ }
1631
+ #finishLoading() {
1632
+ console.debug("[web-apple-pay] #finishLoading: all checks passed, enabling button");
1633
+ const button = this.querySelector(SEL_BUTTON);
1634
+ if (button) {
1635
+ button.classList.remove("wap-loading");
1636
+ this.#updateDisabledState();
1637
+ trackExpressWalletImpression(this.#offer.sku_id, {
1638
+ entryPoint: this.#entryPoint,
1639
+ country: this.#country
1640
+ });
1641
+ }
1501
1642
  }
1502
1643
  // ─── Private helpers ────────────────────────────────────────────────────────
1503
1644
  #updateDisabledState() {
@@ -1506,14 +1647,24 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1506
1647
  if (this.#offer) {
1507
1648
  button.removeAttribute("disabled");
1508
1649
  button.removeAttribute("aria-disabled");
1650
+ button.removeAttribute("aria-busy");
1509
1651
  return;
1510
1652
  }
1511
1653
  button.setAttribute("disabled", "");
1512
1654
  button.setAttribute("aria-disabled", "true");
1513
1655
  }
1514
1656
  #openModal() {
1515
- if (!this.#offer || document.querySelector(ApplePayModal.tag)) return;
1657
+ if (!this.#offer) {
1658
+ return;
1659
+ }
1660
+ if (document.querySelector(ApplePayModal.tag)) {
1661
+ return;
1662
+ }
1516
1663
  trackExpressWalletClick(this.#offer.sku_id);
1664
+ getPurchaseRecaptchaTokens().then((tokens) => {
1665
+ this.#recaptchaTokens = tokens;
1666
+ }).catch(() => {
1667
+ });
1517
1668
  ApplePayModal.define();
1518
1669
  const modal = document.createElement(ApplePayModal.tag);
1519
1670
  modal.offer = this.#offer;
@@ -1548,7 +1699,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1548
1699
  return {
1549
1700
  countryCode: country,
1550
1701
  currencyCode: offer.price?.currency_code ?? DEFAULT_CURRENCY,
1551
- supportedNetworks: paymentOptions.apple_pay.settings.supported_networks,
1702
+ supportedNetworks: paymentOptions.options.apple_pay.settings.supported_networks,
1552
1703
  totalAmount,
1553
1704
  totalLabel: TOTAL_LABEL,
1554
1705
  skuId,
@@ -1569,7 +1720,7 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
1569
1720
  try {
1570
1721
  const sessionData = this.#sessionData ?? _ApplePayButton.#ANONYMOUS_SESSION;
1571
1722
  const paymentOptions = this.#paymentOptions;
1572
- const recaptchaTokens = await getPurchaseRecaptchaTokens();
1723
+ const recaptchaTokens = this.#recaptchaTokens ?? await getPurchaseRecaptchaTokens();
1573
1724
  startApplePaySession(
1574
1725
  this.#buildSessionParams(
1575
1726
  skuId,