@economist/web-apple-pay 1.0.1 → 1.2.0

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/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @economist/web-apple-pay
2
+
3
+ Framework-agnostic Apple Pay UI components implemented as Web Components.
4
+
5
+ This package provides:
6
+
7
+ - `teg-apple-pay-button`
8
+ - `teg-apple-pay-modal`
9
+
10
+ It is intentionally UI-focused. Consumer applications own backend payment orchestration, merchant validation, and secret handling.
11
+
12
+ ## Requirements
13
+
14
+ - Node.js `20.x` or later
15
+ - npm `10.x` or later
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install --save @economist/web-apple-pay
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ### 1) Configure runtime values
26
+
27
+ Call `configureApplePay(...)` once during app startup, before rendering Apple Pay UI:
28
+
29
+ ```ts
30
+ import { configureApplePay, defineApplePayElements } from '@economist/web-apple-pay';
31
+
32
+ configureApplePay({
33
+ merchantId: 'merchant.com.economist.your-app',
34
+ debugBypassEnabled: false,
35
+ // required: wallet API base URL for session and checkout calls
36
+ walletApiBaseUrl: 'https://bff-test.economist.com',
37
+ });
38
+
39
+ defineApplePayElements();
40
+ ```
41
+
42
+ ### 2) Create and mount a button
43
+
44
+ ```ts
45
+ import type { Offer, ApplePayButton } from '@economist/web-apple-pay';
46
+
47
+ const button = document.createElement('teg-apple-pay-button') as ApplePayButton;
48
+ button.offer = offer as Offer;
49
+ button.country = 'US';
50
+ button.dataset.loggedIn = 'true';
51
+
52
+ container.appendChild(button);
53
+ ```
54
+
55
+ When clicked, the button creates and opens `teg-apple-pay-modal` automatically.
56
+
57
+ ## Runtime configuration
58
+
59
+ ### `configureApplePay(config)`
60
+
61
+ Sets package-level runtime configuration used by Apple Pay availability checks.
62
+ Call this once during app startup.
63
+
64
+ `config` shape:
65
+
66
+ - `merchantId` (`string`, required): Apple Pay merchant identifier used for active-card checks.
67
+ - `debugBypassEnabled` (`boolean`, optional): allows `?BYPASS_APPLE_PAY_CHECKS=1` query param to bypass capability checks in non-production-style debugging flows.
68
+ - `walletApiBaseUrl` (`string`, required): base URL for wallet API requests.
69
+
70
+ If `merchantId` is not configured, Apple Pay availability checks return `false` and the button hides itself.
71
+
72
+ ## Component behavior
73
+
74
+ ### `ApplePayButton`
75
+
76
+ - Requires `offer` and `country` before click flow is meaningful.
77
+ - Checks Apple Pay availability on connect; if unavailable, it hides.
78
+ - Skips rendering for excluded variants (`bundle`, `insider_print`).
79
+ - Forwards `data-logged-in` from button to modal (`modal.dataset.loggedIn`).
80
+
81
+ ### `ApplePayModal`
82
+
83
+ - Opened by `ApplePayButton` click.
84
+ - Emits `apple-pay-confirmed` when CTA is accepted.
85
+ - `Escape` closes the modal and prevents propagation to other global handlers.
86
+
87
+ ## Events
88
+
89
+ ### `apple-pay-confirmed`
90
+
91
+ Dispatched from `ApplePayModal` as a `CustomEvent` with:
92
+
93
+ - `detail.skuId` (`string`)
94
+ - `detail.country` (`string`)
95
+
96
+ Consumers should listen for this event and continue payment orchestration.
97
+
98
+ ## Public API
99
+
100
+ - `defineApplePayElements()`: registers both custom elements.
101
+ - `configureApplePay(config)`: sets required runtime config (merchant ID and wallet API base URL, with optional debug bypass).
102
+ - `ApplePayButton`: button component class.
103
+ - `ApplePayModal`: modal component class.
104
+ - `ensureApplePayLogoSymbol()`: logo symbol injection helper.
105
+ - `Offer`: offer contract type used by component props.
106
+
107
+ ## Troubleshooting
108
+
109
+ ### Button does not appear
110
+
111
+ Check:
112
+
113
+ - `configureApplePay(...)` was called before rendering (with a valid `merchantId` and `walletApiBaseUrl`)
114
+ - device/browser supports Apple Pay checks (Safari/Apple device logic)
115
+ - query string/feature flag gating for your page is satisfied
116
+ - offer variant is not excluded (`bundle`, `insider_print`)
117
+
118
+ ### Logged-in checkbox copy looks wrong
119
+
120
+ Set `button.dataset.loggedIn = 'true'` before appending the button. The button forwards this flag to the modal automatically.
121
+
122
+ ## Security and responsibilities
123
+
124
+ Merchant setup must be handled by the consuming application:
125
+
126
+ - inject merchant config at runtime
127
+ - perform merchant validation on trusted backend services
128
+ - keep certificates/secrets outside this package
129
+
130
+ ## Local development
131
+
132
+ From `shared-packages`:
133
+
134
+ ```sh
135
+ cd packages/web-apple-pay
136
+ npm run build
137
+ yalc publish --sig
138
+ ```
139
+
140
+ Then in a consuming app:
141
+
142
+ ```sh
143
+ yalc add @economist/web-apple-pay
144
+ ```
package/dist/index.js CHANGED
@@ -739,8 +739,118 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
739
739
  // src/templates/apple-pay-button.template.html
740
740
  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(--wall-type-system-sans, system-ui, sans-serif);\n font-size: 17px;\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';
741
741
 
742
+ // src/clients/payment-checkout/config.ts
743
+ var runtimeConfig = null;
744
+ var configureApplePay = (config) => {
745
+ runtimeConfig = {
746
+ ...config
747
+ };
748
+ };
749
+ var getApplePayConfig = () => runtimeConfig;
750
+
751
+ // src/clients/payment-checkout/detects.ts
752
+ var isAppleDeviceOrSafari = () => {
753
+ const isIOSOrMacTouchDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
754
+ const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
755
+ return isIOSOrMacTouchDevice || isSafariBrowser;
756
+ };
757
+ var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
758
+ var isApplePayAvailable = async () => {
759
+ if (bypassApplePayChecks()) {
760
+ return true;
761
+ }
762
+ if (!isAppleDeviceOrSafari()) {
763
+ return false;
764
+ }
765
+ const applePaySession = window.ApplePaySession;
766
+ if (!applePaySession) {
767
+ return false;
768
+ }
769
+ try {
770
+ if (typeof applePaySession.canMakePayments !== "function") {
771
+ return false;
772
+ }
773
+ if (!applePaySession.canMakePayments()) {
774
+ return false;
775
+ }
776
+ const merchantId = getApplePayConfig()?.merchantId;
777
+ if (!merchantId) {
778
+ console.warn(
779
+ "[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
780
+ );
781
+ return false;
782
+ }
783
+ return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
784
+ } catch (error) {
785
+ console.error("Error checking Apple Pay active card status:", error);
786
+ return false;
787
+ }
788
+ };
789
+
790
+ // src/clients/payment-checkout/types.ts
791
+ var ApiError = class extends Error {
792
+ status;
793
+ body;
794
+ constructor(status, body) {
795
+ super(`API request failed with status ${status}`);
796
+ this.name = "ApiError";
797
+ this.status = status;
798
+ this.body = body;
799
+ }
800
+ };
801
+
802
+ // src/clients/payment-checkout/index.ts
803
+ var getWalletApiBaseUrl = () => {
804
+ const config = getApplePayConfig();
805
+ if (!config?.walletApiBaseUrl) {
806
+ throw new Error(
807
+ "[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
808
+ );
809
+ }
810
+ return config.walletApiBaseUrl;
811
+ };
812
+ var apiFetch = async (path, options = {}) => {
813
+ const baseUrl = getWalletApiBaseUrl();
814
+ const method = (options.method ?? "GET").toUpperCase();
815
+ const hasBody = options.body !== void 0 && options.body !== null;
816
+ const headers = new Headers(options.headers);
817
+ if (!headers.has("Accept")) {
818
+ headers.set("Accept", "application/json");
819
+ }
820
+ if (!headers.has("Content-Type")) {
821
+ if (hasBody || method !== "GET" && method !== "HEAD") {
822
+ headers.set("Content-Type", "application/json");
823
+ }
824
+ }
825
+ const response = await fetch(`${baseUrl}${path}`, {
826
+ ...options,
827
+ headers
828
+ });
829
+ if (!response.ok) {
830
+ const contentType = response.headers.get("content-type") || "";
831
+ if (contentType.includes("application/json")) {
832
+ const errorBody = await response.json();
833
+ throw new ApiError(response.status, errorBody);
834
+ }
835
+ const errorText = await response.text();
836
+ throw new Error(
837
+ `API request failed: ${response.status} ${response.statusText} - ${errorText}`
838
+ );
839
+ }
840
+ return response.json();
841
+ };
842
+
843
+ // src/clients/payment-checkout/session.ts
844
+ var getExpressCheckoutSession = () => {
845
+ return apiFetch("/v1/wallet/session", {
846
+ method: "GET",
847
+ credentials: "include"
848
+ });
849
+ };
850
+
742
851
  // src/apple-pay-button.ts
743
852
  var FEATURE_APPLE_PAY_EXPRESS_CHECKOUT = "FEATURE_APPLE_PAY_EXPRESS_CHECKOUT";
853
+ var APPLE_PAY_EXCLUDED_VARIANTS = ["bundle", "insider_print"];
744
854
  var ApplePayButton = class _ApplePayButton extends HTMLElement {
745
855
  static tag = "teg-apple-pay-button";
746
856
  static define() {
@@ -753,6 +863,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
753
863
  if (!offer) {
754
864
  return null;
755
865
  }
866
+ const isExcludedVariant = APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant);
867
+ if (isExcludedVariant) {
868
+ return null;
869
+ }
756
870
  if (!new URLSearchParams(window.location.search).has(
757
871
  FEATURE_APPLE_PAY_EXPRESS_CHECKOUT
758
872
  )) {
@@ -766,12 +880,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
766
880
  }
767
881
  #offer = null;
768
882
  #country = "";
883
+ #rendering = false;
769
884
  get offer() {
770
885
  return this.#offer;
771
886
  }
772
887
  set offer(value) {
773
888
  this.#offer = value;
774
- this.updateDisabledState();
889
+ this.#updateDisabledState();
775
890
  }
776
891
  get country() {
777
892
  return this.#country;
@@ -779,34 +894,52 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
779
894
  set country(value) {
780
895
  this.#country = value;
781
896
  }
782
- connectedCallback() {
783
- if (this.querySelector(".apple-pay-container")) {
897
+ async connectedCallback() {
898
+ if (this.#rendering || this.querySelector(".apple-pay-container")) {
784
899
  return;
785
900
  }
786
- ensureApplePayLogoSymbol();
787
- const template = document.createElement("template");
788
- template.innerHTML = apple_pay_button_template_default;
789
- this.appendChild(template.content.cloneNode(true));
790
- this.updateDisabledState();
791
- const button = this.querySelector(".apple-pay-btn");
792
- button.addEventListener("click", () => {
793
- if (!this.#offer) {
901
+ this.#rendering = true;
902
+ try {
903
+ const available = await isApplePayAvailable();
904
+ if (!available) {
905
+ this.style.display = "none";
794
906
  return;
795
907
  }
796
- if (document.querySelector(ApplePayModal.tag)) {
908
+ const sessionData = await this.#fetchSession();
909
+ if (sessionData?.loggedIn && (sessionData.userType === "active-subscription" || sessionData.userType === "b2b-user")) {
910
+ this.style.display = "none";
797
911
  return;
798
912
  }
799
- ApplePayModal.define();
800
- const modal = document.createElement(ApplePayModal.tag);
801
- modal.offer = this.#offer;
802
- modal.country = this.#country;
803
- if (this.dataset.loggedIn !== void 0) {
804
- modal.dataset.loggedIn = this.dataset.loggedIn;
805
- }
806
- modal.open();
807
- });
913
+ this.style.removeProperty("display");
914
+ ensureApplePayLogoSymbol();
915
+ const template = document.createElement("template");
916
+ template.innerHTML = apple_pay_button_template_default;
917
+ this.appendChild(template.content.cloneNode(true));
918
+ this.#updateDisabledState();
919
+ const button = this.querySelector(".apple-pay-btn");
920
+ button.addEventListener("click", () => {
921
+ if (!this.#offer) {
922
+ return;
923
+ }
924
+ if (document.querySelector(ApplePayModal.tag)) {
925
+ return;
926
+ }
927
+ ApplePayModal.define();
928
+ const modal = document.createElement(ApplePayModal.tag);
929
+ modal.offer = this.#offer;
930
+ modal.country = this.#country;
931
+ if (this.dataset.loggedIn !== void 0) {
932
+ modal.dataset.loggedIn = this.dataset.loggedIn;
933
+ }
934
+ modal.open();
935
+ });
936
+ } catch {
937
+ this.style.display = "none";
938
+ } finally {
939
+ this.#rendering = false;
940
+ }
808
941
  }
809
- updateDisabledState() {
942
+ #updateDisabledState() {
810
943
  const button = this.querySelector(".apple-pay-btn");
811
944
  if (!button) {
812
945
  return;
@@ -819,6 +952,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
819
952
  button.setAttribute("disabled", "");
820
953
  button.setAttribute("aria-disabled", "true");
821
954
  }
955
+ async #fetchSession() {
956
+ try {
957
+ return await getExpressCheckoutSession();
958
+ } catch {
959
+ return null;
960
+ }
961
+ }
822
962
  };
823
963
 
824
964
  // src/index.ts
@@ -829,6 +969,7 @@ function defineApplePayElements() {
829
969
  export {
830
970
  ApplePayButton,
831
971
  ApplePayModal,
972
+ configureApplePay,
832
973
  defineApplePayElements,
833
974
  ensureApplePayLogoSymbol
834
975
  };
@@ -11,6 +11,5 @@ export declare class ApplePayButton extends HTMLElement {
11
11
  set offer(value: Offer | null);
12
12
  get country(): string;
13
13
  set country(value: string);
14
- connectedCallback(): void;
15
- private updateDisabledState;
14
+ connectedCallback(): Promise<void>;
16
15
  }
@@ -739,8 +739,113 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
739
739
  // src/templates/apple-pay-button.template.html
740
740
  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(--wall-type-system-sans, system-ui, sans-serif);\n font-size: 17px;\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';
741
741
 
742
+ // src/clients/payment-checkout/config.ts
743
+ var runtimeConfig = null;
744
+ var getApplePayConfig = () => runtimeConfig;
745
+
746
+ // src/clients/payment-checkout/detects.ts
747
+ var isAppleDeviceOrSafari = () => {
748
+ const isIOSOrMacTouchDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
749
+ const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
750
+ return isIOSOrMacTouchDevice || isSafariBrowser;
751
+ };
752
+ var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
753
+ var isApplePayAvailable = async () => {
754
+ if (bypassApplePayChecks()) {
755
+ return true;
756
+ }
757
+ if (!isAppleDeviceOrSafari()) {
758
+ return false;
759
+ }
760
+ const applePaySession = window.ApplePaySession;
761
+ if (!applePaySession) {
762
+ return false;
763
+ }
764
+ try {
765
+ if (typeof applePaySession.canMakePayments !== "function") {
766
+ return false;
767
+ }
768
+ if (!applePaySession.canMakePayments()) {
769
+ return false;
770
+ }
771
+ const merchantId = getApplePayConfig()?.merchantId;
772
+ if (!merchantId) {
773
+ console.warn(
774
+ "[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
775
+ );
776
+ return false;
777
+ }
778
+ return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
779
+ } catch (error) {
780
+ console.error("Error checking Apple Pay active card status:", error);
781
+ return false;
782
+ }
783
+ };
784
+
785
+ // src/clients/payment-checkout/types.ts
786
+ var ApiError = class extends Error {
787
+ status;
788
+ body;
789
+ constructor(status, body) {
790
+ super(`API request failed with status ${status}`);
791
+ this.name = "ApiError";
792
+ this.status = status;
793
+ this.body = body;
794
+ }
795
+ };
796
+
797
+ // src/clients/payment-checkout/index.ts
798
+ var getWalletApiBaseUrl = () => {
799
+ const config = getApplePayConfig();
800
+ if (!config?.walletApiBaseUrl) {
801
+ throw new Error(
802
+ "[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
803
+ );
804
+ }
805
+ return config.walletApiBaseUrl;
806
+ };
807
+ var apiFetch = async (path, options = {}) => {
808
+ const baseUrl = getWalletApiBaseUrl();
809
+ const method = (options.method ?? "GET").toUpperCase();
810
+ const hasBody = options.body !== void 0 && options.body !== null;
811
+ const headers = new Headers(options.headers);
812
+ if (!headers.has("Accept")) {
813
+ headers.set("Accept", "application/json");
814
+ }
815
+ if (!headers.has("Content-Type")) {
816
+ if (hasBody || method !== "GET" && method !== "HEAD") {
817
+ headers.set("Content-Type", "application/json");
818
+ }
819
+ }
820
+ const response = await fetch(`${baseUrl}${path}`, {
821
+ ...options,
822
+ headers
823
+ });
824
+ if (!response.ok) {
825
+ const contentType = response.headers.get("content-type") || "";
826
+ if (contentType.includes("application/json")) {
827
+ const errorBody = await response.json();
828
+ throw new ApiError(response.status, errorBody);
829
+ }
830
+ const errorText = await response.text();
831
+ throw new Error(
832
+ `API request failed: ${response.status} ${response.statusText} - ${errorText}`
833
+ );
834
+ }
835
+ return response.json();
836
+ };
837
+
838
+ // src/clients/payment-checkout/session.ts
839
+ var getExpressCheckoutSession = () => {
840
+ return apiFetch("/v1/wallet/session", {
841
+ method: "GET",
842
+ credentials: "include"
843
+ });
844
+ };
845
+
742
846
  // src/apple-pay-button.ts
743
847
  var FEATURE_APPLE_PAY_EXPRESS_CHECKOUT = "FEATURE_APPLE_PAY_EXPRESS_CHECKOUT";
848
+ var APPLE_PAY_EXCLUDED_VARIANTS = ["bundle", "insider_print"];
744
849
  var ApplePayButton = class _ApplePayButton extends HTMLElement {
745
850
  static tag = "teg-apple-pay-button";
746
851
  static define() {
@@ -753,6 +858,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
753
858
  if (!offer) {
754
859
  return null;
755
860
  }
861
+ const isExcludedVariant = APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant);
862
+ if (isExcludedVariant) {
863
+ return null;
864
+ }
756
865
  if (!new URLSearchParams(window.location.search).has(
757
866
  FEATURE_APPLE_PAY_EXPRESS_CHECKOUT
758
867
  )) {
@@ -766,12 +875,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
766
875
  }
767
876
  #offer = null;
768
877
  #country = "";
878
+ #rendering = false;
769
879
  get offer() {
770
880
  return this.#offer;
771
881
  }
772
882
  set offer(value) {
773
883
  this.#offer = value;
774
- this.updateDisabledState();
884
+ this.#updateDisabledState();
775
885
  }
776
886
  get country() {
777
887
  return this.#country;
@@ -779,34 +889,52 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
779
889
  set country(value) {
780
890
  this.#country = value;
781
891
  }
782
- connectedCallback() {
783
- if (this.querySelector(".apple-pay-container")) {
892
+ async connectedCallback() {
893
+ if (this.#rendering || this.querySelector(".apple-pay-container")) {
784
894
  return;
785
895
  }
786
- ensureApplePayLogoSymbol();
787
- const template = document.createElement("template");
788
- template.innerHTML = apple_pay_button_template_default;
789
- this.appendChild(template.content.cloneNode(true));
790
- this.updateDisabledState();
791
- const button = this.querySelector(".apple-pay-btn");
792
- button.addEventListener("click", () => {
793
- if (!this.#offer) {
896
+ this.#rendering = true;
897
+ try {
898
+ const available = await isApplePayAvailable();
899
+ if (!available) {
900
+ this.style.display = "none";
794
901
  return;
795
902
  }
796
- if (document.querySelector(ApplePayModal.tag)) {
903
+ const sessionData = await this.#fetchSession();
904
+ if (sessionData?.loggedIn && (sessionData.userType === "active-subscription" || sessionData.userType === "b2b-user")) {
905
+ this.style.display = "none";
797
906
  return;
798
907
  }
799
- ApplePayModal.define();
800
- const modal = document.createElement(ApplePayModal.tag);
801
- modal.offer = this.#offer;
802
- modal.country = this.#country;
803
- if (this.dataset.loggedIn !== void 0) {
804
- modal.dataset.loggedIn = this.dataset.loggedIn;
805
- }
806
- modal.open();
807
- });
908
+ this.style.removeProperty("display");
909
+ ensureApplePayLogoSymbol();
910
+ const template = document.createElement("template");
911
+ template.innerHTML = apple_pay_button_template_default;
912
+ this.appendChild(template.content.cloneNode(true));
913
+ this.#updateDisabledState();
914
+ const button = this.querySelector(".apple-pay-btn");
915
+ button.addEventListener("click", () => {
916
+ if (!this.#offer) {
917
+ return;
918
+ }
919
+ if (document.querySelector(ApplePayModal.tag)) {
920
+ return;
921
+ }
922
+ ApplePayModal.define();
923
+ const modal = document.createElement(ApplePayModal.tag);
924
+ modal.offer = this.#offer;
925
+ modal.country = this.#country;
926
+ if (this.dataset.loggedIn !== void 0) {
927
+ modal.dataset.loggedIn = this.dataset.loggedIn;
928
+ }
929
+ modal.open();
930
+ });
931
+ } catch {
932
+ this.style.display = "none";
933
+ } finally {
934
+ this.#rendering = false;
935
+ }
808
936
  }
809
- updateDisabledState() {
937
+ #updateDisabledState() {
810
938
  const button = this.querySelector(".apple-pay-btn");
811
939
  if (!button) {
812
940
  return;
@@ -819,6 +947,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
819
947
  button.setAttribute("disabled", "");
820
948
  button.setAttribute("aria-disabled", "true");
821
949
  }
950
+ async #fetchSession() {
951
+ try {
952
+ return await getExpressCheckoutSession();
953
+ } catch {
954
+ return null;
955
+ }
956
+ }
822
957
  };
823
958
  export {
824
959
  ApplePayButton
@@ -0,0 +1,8 @@
1
+ export interface ApplePayRuntimeConfig {
2
+ merchantId: string;
3
+ debugBypassEnabled?: boolean;
4
+ walletApiBaseUrl: string;
5
+ }
6
+ export declare const configureApplePay: (config: ApplePayRuntimeConfig) => void;
7
+ export declare const getApplePayConfig: () => ApplePayRuntimeConfig | null;
8
+ export declare const resetApplePayConfig: () => void;
@@ -0,0 +1,16 @@
1
+ // src/clients/payment-checkout/config.ts
2
+ var runtimeConfig = null;
3
+ var configureApplePay = (config) => {
4
+ runtimeConfig = {
5
+ ...config
6
+ };
7
+ };
8
+ var getApplePayConfig = () => runtimeConfig;
9
+ var resetApplePayConfig = () => {
10
+ runtimeConfig = null;
11
+ };
12
+ export {
13
+ configureApplePay,
14
+ getApplePayConfig,
15
+ resetApplePayConfig
16
+ };
@@ -0,0 +1,3 @@
1
+ export declare const isAppleDeviceOrSafari: () => boolean;
2
+ export declare const bypassApplePayChecks: () => boolean;
3
+ export declare const isApplePayAvailable: () => Promise<boolean>;
@@ -0,0 +1,47 @@
1
+ // src/clients/payment-checkout/config.ts
2
+ var runtimeConfig = null;
3
+ var getApplePayConfig = () => runtimeConfig;
4
+
5
+ // src/clients/payment-checkout/detects.ts
6
+ var isAppleDeviceOrSafari = () => {
7
+ const isIOSOrMacTouchDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
8
+ const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
9
+ return isIOSOrMacTouchDevice || isSafariBrowser;
10
+ };
11
+ var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
12
+ var isApplePayAvailable = async () => {
13
+ if (bypassApplePayChecks()) {
14
+ return true;
15
+ }
16
+ if (!isAppleDeviceOrSafari()) {
17
+ return false;
18
+ }
19
+ const applePaySession = window.ApplePaySession;
20
+ if (!applePaySession) {
21
+ return false;
22
+ }
23
+ try {
24
+ if (typeof applePaySession.canMakePayments !== "function") {
25
+ return false;
26
+ }
27
+ if (!applePaySession.canMakePayments()) {
28
+ return false;
29
+ }
30
+ const merchantId = getApplePayConfig()?.merchantId;
31
+ if (!merchantId) {
32
+ console.warn(
33
+ "[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
34
+ );
35
+ return false;
36
+ }
37
+ return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
38
+ } catch (error) {
39
+ console.error("Error checking Apple Pay active card status:", error);
40
+ return false;
41
+ }
42
+ };
43
+ export {
44
+ bypassApplePayChecks,
45
+ isAppleDeviceOrSafari,
46
+ isApplePayAvailable
47
+ };
@@ -0,0 +1 @@
1
+ export declare const apiFetch: <T>(path: string, options?: RequestInit) => Promise<T>;
@@ -0,0 +1,59 @@
1
+ // src/clients/payment-checkout/config.ts
2
+ var runtimeConfig = null;
3
+ var getApplePayConfig = () => runtimeConfig;
4
+
5
+ // src/clients/payment-checkout/types.ts
6
+ var ApiError = class extends Error {
7
+ status;
8
+ body;
9
+ constructor(status, body) {
10
+ super(`API request failed with status ${status}`);
11
+ this.name = "ApiError";
12
+ this.status = status;
13
+ this.body = body;
14
+ }
15
+ };
16
+
17
+ // src/clients/payment-checkout/index.ts
18
+ var getWalletApiBaseUrl = () => {
19
+ const config = getApplePayConfig();
20
+ if (!config?.walletApiBaseUrl) {
21
+ throw new Error(
22
+ "[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
23
+ );
24
+ }
25
+ return config.walletApiBaseUrl;
26
+ };
27
+ var apiFetch = async (path, options = {}) => {
28
+ const baseUrl = getWalletApiBaseUrl();
29
+ const method = (options.method ?? "GET").toUpperCase();
30
+ const hasBody = options.body !== void 0 && options.body !== null;
31
+ const headers = new Headers(options.headers);
32
+ if (!headers.has("Accept")) {
33
+ headers.set("Accept", "application/json");
34
+ }
35
+ if (!headers.has("Content-Type")) {
36
+ if (hasBody || method !== "GET" && method !== "HEAD") {
37
+ headers.set("Content-Type", "application/json");
38
+ }
39
+ }
40
+ const response = await fetch(`${baseUrl}${path}`, {
41
+ ...options,
42
+ headers
43
+ });
44
+ if (!response.ok) {
45
+ const contentType = response.headers.get("content-type") || "";
46
+ if (contentType.includes("application/json")) {
47
+ const errorBody = await response.json();
48
+ throw new ApiError(response.status, errorBody);
49
+ }
50
+ const errorText = await response.text();
51
+ throw new Error(
52
+ `API request failed: ${response.status} ${response.statusText} - ${errorText}`
53
+ );
54
+ }
55
+ return response.json();
56
+ };
57
+ export {
58
+ apiFetch
59
+ };
@@ -0,0 +1,2 @@
1
+ import type { SessionResponse } from './types';
2
+ export declare const getExpressCheckoutSession: () => Promise<SessionResponse>;
@@ -0,0 +1,67 @@
1
+ // src/clients/payment-checkout/config.ts
2
+ var runtimeConfig = null;
3
+ var getApplePayConfig = () => runtimeConfig;
4
+
5
+ // src/clients/payment-checkout/types.ts
6
+ var ApiError = class extends Error {
7
+ status;
8
+ body;
9
+ constructor(status, body) {
10
+ super(`API request failed with status ${status}`);
11
+ this.name = "ApiError";
12
+ this.status = status;
13
+ this.body = body;
14
+ }
15
+ };
16
+
17
+ // src/clients/payment-checkout/index.ts
18
+ var getWalletApiBaseUrl = () => {
19
+ const config = getApplePayConfig();
20
+ if (!config?.walletApiBaseUrl) {
21
+ throw new Error(
22
+ "[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
23
+ );
24
+ }
25
+ return config.walletApiBaseUrl;
26
+ };
27
+ var apiFetch = async (path, options = {}) => {
28
+ const baseUrl = getWalletApiBaseUrl();
29
+ const method = (options.method ?? "GET").toUpperCase();
30
+ const hasBody = options.body !== void 0 && options.body !== null;
31
+ const headers = new Headers(options.headers);
32
+ if (!headers.has("Accept")) {
33
+ headers.set("Accept", "application/json");
34
+ }
35
+ if (!headers.has("Content-Type")) {
36
+ if (hasBody || method !== "GET" && method !== "HEAD") {
37
+ headers.set("Content-Type", "application/json");
38
+ }
39
+ }
40
+ const response = await fetch(`${baseUrl}${path}`, {
41
+ ...options,
42
+ headers
43
+ });
44
+ if (!response.ok) {
45
+ const contentType = response.headers.get("content-type") || "";
46
+ if (contentType.includes("application/json")) {
47
+ const errorBody = await response.json();
48
+ throw new ApiError(response.status, errorBody);
49
+ }
50
+ const errorText = await response.text();
51
+ throw new Error(
52
+ `API request failed: ${response.status} ${response.statusText} - ${errorText}`
53
+ );
54
+ }
55
+ return response.json();
56
+ };
57
+
58
+ // src/clients/payment-checkout/session.ts
59
+ var getExpressCheckoutSession = () => {
60
+ return apiFetch("/v1/wallet/session", {
61
+ method: "GET",
62
+ credentials: "include"
63
+ });
64
+ };
65
+ export {
66
+ getExpressCheckoutSession
67
+ };
@@ -0,0 +1,14 @@
1
+ export type SessionResponse = {
2
+ loggedIn: false;
3
+ userType: 'new-eligible-user';
4
+ } | {
5
+ loggedIn: true;
6
+ emailAddress: string;
7
+ contactId: string;
8
+ userType: 'active-subscription' | 'b2b-user' | 'new-eligible-user';
9
+ };
10
+ export declare class ApiError extends Error {
11
+ readonly status: number;
12
+ readonly body: unknown;
13
+ constructor(status: number, body: unknown);
14
+ }
@@ -0,0 +1,14 @@
1
+ // src/clients/payment-checkout/types.ts
2
+ var ApiError = class extends Error {
3
+ status;
4
+ body;
5
+ constructor(status, body) {
6
+ super(`API request failed with status ${status}`);
7
+ this.name = "ApiError";
8
+ this.status = status;
9
+ this.body = body;
10
+ }
11
+ };
12
+ export {
13
+ ApiError
14
+ };
@@ -1,5 +1,7 @@
1
1
  export { ApplePayButton } from './apple-pay-button';
2
2
  export { ApplePayModal } from './apple-pay-modal';
3
3
  export { ensureApplePayLogoSymbol } from './apple-pay-logo';
4
+ export { configureApplePay } from './clients/payment-checkout/config';
5
+ export type { ApplePayRuntimeConfig } from './clients/payment-checkout/config';
4
6
  export type { Offer } from './types/offer.types';
5
7
  export declare function defineApplePayElements(): void;
package/dist/src/index.js CHANGED
@@ -739,8 +739,118 @@ var ApplePayModal = class _ApplePayModal extends HTMLElement {
739
739
  // src/templates/apple-pay-button.template.html
740
740
  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(--wall-type-system-sans, system-ui, sans-serif);\n font-size: 17px;\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';
741
741
 
742
+ // src/clients/payment-checkout/config.ts
743
+ var runtimeConfig = null;
744
+ var configureApplePay = (config) => {
745
+ runtimeConfig = {
746
+ ...config
747
+ };
748
+ };
749
+ var getApplePayConfig = () => runtimeConfig;
750
+
751
+ // src/clients/payment-checkout/detects.ts
752
+ var isAppleDeviceOrSafari = () => {
753
+ const isIOSOrMacTouchDevice = /iPad|iPhone|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1 && /Macintosh/.test(navigator.userAgent);
754
+ const isSafariBrowser = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1;
755
+ return isIOSOrMacTouchDevice || isSafariBrowser;
756
+ };
757
+ var bypassApplePayChecks = () => new URLSearchParams(window.location.search).get("BYPASS_APPLE_PAY_CHECKS") !== null && getApplePayConfig()?.debugBypassEnabled === true;
758
+ var isApplePayAvailable = async () => {
759
+ if (bypassApplePayChecks()) {
760
+ return true;
761
+ }
762
+ if (!isAppleDeviceOrSafari()) {
763
+ return false;
764
+ }
765
+ const applePaySession = window.ApplePaySession;
766
+ if (!applePaySession) {
767
+ return false;
768
+ }
769
+ try {
770
+ if (typeof applePaySession.canMakePayments !== "function") {
771
+ return false;
772
+ }
773
+ if (!applePaySession.canMakePayments()) {
774
+ return false;
775
+ }
776
+ const merchantId = getApplePayConfig()?.merchantId;
777
+ if (!merchantId) {
778
+ console.warn(
779
+ "[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
780
+ );
781
+ return false;
782
+ }
783
+ return await applePaySession.canMakePaymentsWithActiveCard(merchantId);
784
+ } catch (error) {
785
+ console.error("Error checking Apple Pay active card status:", error);
786
+ return false;
787
+ }
788
+ };
789
+
790
+ // src/clients/payment-checkout/types.ts
791
+ var ApiError = class extends Error {
792
+ status;
793
+ body;
794
+ constructor(status, body) {
795
+ super(`API request failed with status ${status}`);
796
+ this.name = "ApiError";
797
+ this.status = status;
798
+ this.body = body;
799
+ }
800
+ };
801
+
802
+ // src/clients/payment-checkout/index.ts
803
+ var getWalletApiBaseUrl = () => {
804
+ const config = getApplePayConfig();
805
+ if (!config?.walletApiBaseUrl) {
806
+ throw new Error(
807
+ "[web-apple-pay] Missing walletApiBaseUrl. Call configureApplePay({ merchantId, walletApiBaseUrl, ... }) before making wallet API requests."
808
+ );
809
+ }
810
+ return config.walletApiBaseUrl;
811
+ };
812
+ var apiFetch = async (path, options = {}) => {
813
+ const baseUrl = getWalletApiBaseUrl();
814
+ const method = (options.method ?? "GET").toUpperCase();
815
+ const hasBody = options.body !== void 0 && options.body !== null;
816
+ const headers = new Headers(options.headers);
817
+ if (!headers.has("Accept")) {
818
+ headers.set("Accept", "application/json");
819
+ }
820
+ if (!headers.has("Content-Type")) {
821
+ if (hasBody || method !== "GET" && method !== "HEAD") {
822
+ headers.set("Content-Type", "application/json");
823
+ }
824
+ }
825
+ const response = await fetch(`${baseUrl}${path}`, {
826
+ ...options,
827
+ headers
828
+ });
829
+ if (!response.ok) {
830
+ const contentType = response.headers.get("content-type") || "";
831
+ if (contentType.includes("application/json")) {
832
+ const errorBody = await response.json();
833
+ throw new ApiError(response.status, errorBody);
834
+ }
835
+ const errorText = await response.text();
836
+ throw new Error(
837
+ `API request failed: ${response.status} ${response.statusText} - ${errorText}`
838
+ );
839
+ }
840
+ return response.json();
841
+ };
842
+
843
+ // src/clients/payment-checkout/session.ts
844
+ var getExpressCheckoutSession = () => {
845
+ return apiFetch("/v1/wallet/session", {
846
+ method: "GET",
847
+ credentials: "include"
848
+ });
849
+ };
850
+
742
851
  // src/apple-pay-button.ts
743
852
  var FEATURE_APPLE_PAY_EXPRESS_CHECKOUT = "FEATURE_APPLE_PAY_EXPRESS_CHECKOUT";
853
+ var APPLE_PAY_EXCLUDED_VARIANTS = ["bundle", "insider_print"];
744
854
  var ApplePayButton = class _ApplePayButton extends HTMLElement {
745
855
  static tag = "teg-apple-pay-button";
746
856
  static define() {
@@ -753,6 +863,10 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
753
863
  if (!offer) {
754
864
  return null;
755
865
  }
866
+ const isExcludedVariant = APPLE_PAY_EXCLUDED_VARIANTS.includes(offer.product?.variant);
867
+ if (isExcludedVariant) {
868
+ return null;
869
+ }
756
870
  if (!new URLSearchParams(window.location.search).has(
757
871
  FEATURE_APPLE_PAY_EXPRESS_CHECKOUT
758
872
  )) {
@@ -766,12 +880,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
766
880
  }
767
881
  #offer = null;
768
882
  #country = "";
883
+ #rendering = false;
769
884
  get offer() {
770
885
  return this.#offer;
771
886
  }
772
887
  set offer(value) {
773
888
  this.#offer = value;
774
- this.updateDisabledState();
889
+ this.#updateDisabledState();
775
890
  }
776
891
  get country() {
777
892
  return this.#country;
@@ -779,34 +894,52 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
779
894
  set country(value) {
780
895
  this.#country = value;
781
896
  }
782
- connectedCallback() {
783
- if (this.querySelector(".apple-pay-container")) {
897
+ async connectedCallback() {
898
+ if (this.#rendering || this.querySelector(".apple-pay-container")) {
784
899
  return;
785
900
  }
786
- ensureApplePayLogoSymbol();
787
- const template = document.createElement("template");
788
- template.innerHTML = apple_pay_button_template_default;
789
- this.appendChild(template.content.cloneNode(true));
790
- this.updateDisabledState();
791
- const button = this.querySelector(".apple-pay-btn");
792
- button.addEventListener("click", () => {
793
- if (!this.#offer) {
901
+ this.#rendering = true;
902
+ try {
903
+ const available = await isApplePayAvailable();
904
+ if (!available) {
905
+ this.style.display = "none";
794
906
  return;
795
907
  }
796
- if (document.querySelector(ApplePayModal.tag)) {
908
+ const sessionData = await this.#fetchSession();
909
+ if (sessionData?.loggedIn && (sessionData.userType === "active-subscription" || sessionData.userType === "b2b-user")) {
910
+ this.style.display = "none";
797
911
  return;
798
912
  }
799
- ApplePayModal.define();
800
- const modal = document.createElement(ApplePayModal.tag);
801
- modal.offer = this.#offer;
802
- modal.country = this.#country;
803
- if (this.dataset.loggedIn !== void 0) {
804
- modal.dataset.loggedIn = this.dataset.loggedIn;
805
- }
806
- modal.open();
807
- });
913
+ this.style.removeProperty("display");
914
+ ensureApplePayLogoSymbol();
915
+ const template = document.createElement("template");
916
+ template.innerHTML = apple_pay_button_template_default;
917
+ this.appendChild(template.content.cloneNode(true));
918
+ this.#updateDisabledState();
919
+ const button = this.querySelector(".apple-pay-btn");
920
+ button.addEventListener("click", () => {
921
+ if (!this.#offer) {
922
+ return;
923
+ }
924
+ if (document.querySelector(ApplePayModal.tag)) {
925
+ return;
926
+ }
927
+ ApplePayModal.define();
928
+ const modal = document.createElement(ApplePayModal.tag);
929
+ modal.offer = this.#offer;
930
+ modal.country = this.#country;
931
+ if (this.dataset.loggedIn !== void 0) {
932
+ modal.dataset.loggedIn = this.dataset.loggedIn;
933
+ }
934
+ modal.open();
935
+ });
936
+ } catch {
937
+ this.style.display = "none";
938
+ } finally {
939
+ this.#rendering = false;
940
+ }
808
941
  }
809
- updateDisabledState() {
942
+ #updateDisabledState() {
810
943
  const button = this.querySelector(".apple-pay-btn");
811
944
  if (!button) {
812
945
  return;
@@ -819,6 +952,13 @@ var ApplePayButton = class _ApplePayButton extends HTMLElement {
819
952
  button.setAttribute("disabled", "");
820
953
  button.setAttribute("aria-disabled", "true");
821
954
  }
955
+ async #fetchSession() {
956
+ try {
957
+ return await getExpressCheckoutSession();
958
+ } catch {
959
+ return null;
960
+ }
961
+ }
822
962
  };
823
963
 
824
964
  // src/index.ts
@@ -829,6 +969,7 @@ function defineApplePayElements() {
829
969
  export {
830
970
  ApplePayButton,
831
971
  ApplePayModal,
972
+ configureApplePay,
832
973
  defineApplePayElements,
833
974
  ensureApplePayLogoSymbol
834
975
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@economist/web-apple-pay",
3
3
  "description": "Web component package for Apple Pay checkout UI",
4
- "version": "1.0.1",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",