@economist/web-apple-pay 0.1.1-beta.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 +186 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1642 -0
- package/dist/src/apple-pay-button.d.ts +45 -0
- package/dist/src/apple-pay-button.js +1621 -0
- package/dist/src/apple-pay-button.stories.js +1934 -0
- package/dist/src/apple-pay-logo.d.ts +1 -0
- package/dist/src/apple-pay-logo.js +14 -0
- package/dist/src/apple-pay-modal.d.ts +12 -0
- package/dist/src/apple-pay-modal.js +442 -0
- package/dist/src/apple-pay-modal.stories.js +1778 -0
- package/dist/src/clients/payment-checkout/apple-session.d.ts +18 -0
- package/dist/src/clients/payment-checkout/apple-session.js +243 -0
- package/dist/src/clients/payment-checkout/billing-contact-details.d.ts +21 -0
- package/dist/src/clients/payment-checkout/billing-contact-details.js +109 -0
- package/dist/src/clients/payment-checkout/config.d.ts +9 -0
- package/dist/src/clients/payment-checkout/config.js +23 -0
- package/dist/src/clients/payment-checkout/contact-validation.d.ts +21 -0
- package/dist/src/clients/payment-checkout/contact-validation.js +50 -0
- package/dist/src/clients/payment-checkout/detects.d.ts +3 -0
- package/dist/src/clients/payment-checkout/detects.js +56 -0
- package/dist/src/clients/payment-checkout/index.d.ts +5 -0
- package/dist/src/clients/payment-checkout/index.js +83 -0
- package/dist/src/clients/payment-checkout/messages.d.ts +33 -0
- package/dist/src/clients/payment-checkout/messages.js +19 -0
- package/dist/src/clients/payment-checkout/payment-handler.d.ts +36 -0
- package/dist/src/clients/payment-checkout/payment-handler.js +389 -0
- package/dist/src/clients/payment-checkout/session.d.ts +2 -0
- package/dist/src/clients/payment-checkout/session.js +67 -0
- package/dist/src/clients/payment-checkout/shipping-contact-details.d.ts +10 -0
- package/dist/src/clients/payment-checkout/shipping-contact-details.js +83 -0
- package/dist/src/clients/payment-checkout/types.d.ts +118 -0
- package/dist/src/clients/payment-checkout/types.js +14 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +1642 -0
- package/dist/src/stories/fixtures/offer.d.ts +2 -0
- package/dist/src/stories/fixtures/offer.js +28 -0
- package/dist/src/stories/utils/story-env.d.ts +11 -0
- package/dist/src/stories/utils/story-env.js +148 -0
- package/dist/src/types/html-template.d.js +0 -0
- package/dist/src/types/offer.types.d.ts +73 -0
- package/dist/src/types/offer.types.js +0 -0
- package/dist/src/utils/billing-countries.d.ts +19 -0
- package/dist/src/utils/billing-countries.js +263 -0
- package/dist/src/utils/define.d.ts +1 -0
- package/dist/src/utils/define.js +9 -0
- package/dist/src/utils/expressCheckoutRedirect.d.ts +55 -0
- package/dist/src/utils/expressCheckoutRedirect.js +46 -0
- package/dist/src/utils/expressCheckoutTracking.d.ts +64 -0
- package/dist/src/utils/expressCheckoutTracking.js +84 -0
- package/dist/src/utils/pricing.d.ts +1 -0
- package/dist/src/utils/pricing.js +60 -0
- package/dist/src/utils/recaptcha.d.ts +22 -0
- package/dist/src/utils/recaptcha.js +54 -0
- package/package.json +64 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApplePaySession lifecycle manager.
|
|
3
|
+
* Creates the native Apple Pay payment sheet and wires merchant validation,
|
|
4
|
+
* payment authorization, and cancellation handlers.
|
|
5
|
+
*/
|
|
6
|
+
export type ApplePaySessionConfig = {
|
|
7
|
+
countryCode: string;
|
|
8
|
+
currencyCode: string;
|
|
9
|
+
supportedNetworks: string[];
|
|
10
|
+
totalAmount: string;
|
|
11
|
+
totalLabel: string;
|
|
12
|
+
skuId: string;
|
|
13
|
+
returnUrl: string;
|
|
14
|
+
userLoggedIn: boolean;
|
|
15
|
+
onPaymentAuthorized: (event: ApplePayJS.ApplePayPaymentAuthorizedEvent, session: ApplePaySession) => Promise<void>;
|
|
16
|
+
onCancel?: () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function startApplePaySession(config: ApplePaySessionConfig): void;
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
var validateAppleMerchant = (data) => {
|
|
58
|
+
return apiFetch("/v1/wallet/validate-merchant", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: JSON.stringify(data)
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/utils/expressCheckoutRedirect.ts
|
|
65
|
+
var redirectToCheckoutFallback = (params) => {
|
|
66
|
+
const url = new URL(window.location.href);
|
|
67
|
+
url.pathname = "/checkout";
|
|
68
|
+
url.searchParams.set("lmo_offer", params.skuId);
|
|
69
|
+
if (params.inlineError)
|
|
70
|
+
url.searchParams.set("inlineError", params.inlineError);
|
|
71
|
+
if (params.email) url.searchParams.set("email", params.email);
|
|
72
|
+
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
73
|
+
url.searchParams.set("returnUrl", params.returnUrl);
|
|
74
|
+
return url.pathname + url.search;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/utils/recaptcha.ts
|
|
78
|
+
var loadRecaptchaScript = (siteKey) => {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
if (typeof grecaptcha !== "undefined") return resolve();
|
|
81
|
+
const existingScript = document.querySelector(
|
|
82
|
+
'script[src*="recaptcha/api.js"]'
|
|
83
|
+
);
|
|
84
|
+
if (existingScript) {
|
|
85
|
+
existingScript.addEventListener("load", () => resolve());
|
|
86
|
+
existingScript.addEventListener(
|
|
87
|
+
"error",
|
|
88
|
+
() => reject(new Error("Failed to load existing reCAPTCHA script"))
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const script = document.createElement("script");
|
|
93
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
|
|
94
|
+
script.async = true;
|
|
95
|
+
script.defer = true;
|
|
96
|
+
script.onload = () => resolve();
|
|
97
|
+
script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
|
|
98
|
+
document.head.appendChild(script);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
var getRecaptchaToken = async (action) => {
|
|
102
|
+
const siteKey = getApplePayConfig()?.recaptchaSiteKey;
|
|
103
|
+
if (!siteKey) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"[web-apple-pay] Missing recaptchaSiteKey. Call configureApplePay({ recaptchaSiteKey, ... }) before initiating Apple Pay."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
await loadRecaptchaScript(siteKey);
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
grecaptcha.ready(() => {
|
|
111
|
+
grecaptcha.execute(siteKey, { action }).then(resolve).catch(reject);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// src/utils/expressCheckoutTracking.ts
|
|
117
|
+
var EXPRESS_CHECKOUT_EVENTS = {
|
|
118
|
+
/** Wallet button displayed */
|
|
119
|
+
IMPRESSION: "express_wallet_impression",
|
|
120
|
+
/** Wallet button clicked */
|
|
121
|
+
BUTTON_CLICK: "express_wallet_click",
|
|
122
|
+
/** Apple Pay sheet opened */
|
|
123
|
+
SHEET_OPEN: "wallet_payment_sheet_open",
|
|
124
|
+
/** Payment authorised by the user in the Apple Pay sheet */
|
|
125
|
+
AUTHORISED: "wallet_payment_authorised",
|
|
126
|
+
/** Purchase completed — BFF returned success */
|
|
127
|
+
SUCCESS: "wallet_payment_success",
|
|
128
|
+
/** Payment failed — BFF returned error */
|
|
129
|
+
FAILURE: "wallet_payment_failure",
|
|
130
|
+
/** Redirect to standard checkout (any fallback path) */
|
|
131
|
+
FALLBACK_CHECKOUT: "fallback_checkout"
|
|
132
|
+
};
|
|
133
|
+
var emitTedlEvent = (event) => {
|
|
134
|
+
window.tedl = window.tedl ?? {};
|
|
135
|
+
window.tedl.events = window.tedl.events ?? [];
|
|
136
|
+
window.tedl.events.push(event);
|
|
137
|
+
};
|
|
138
|
+
var track = (action, extra) => {
|
|
139
|
+
emitTedlEvent({
|
|
140
|
+
name: "user.journey",
|
|
141
|
+
type: "checkout",
|
|
142
|
+
action,
|
|
143
|
+
info: "applepay",
|
|
144
|
+
...extra
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
var trackWalletPaymentSheetOpen = (skuId) => {
|
|
148
|
+
track(EXPRESS_CHECKOUT_EVENTS.SHEET_OPEN, { sku_id: skuId });
|
|
149
|
+
};
|
|
150
|
+
var trackWalletPaymentAuthorised = (skuId) => {
|
|
151
|
+
track(EXPRESS_CHECKOUT_EVENTS.AUTHORISED, { sku_id: skuId });
|
|
152
|
+
};
|
|
153
|
+
var trackFallbackCheckout = (skuId) => {
|
|
154
|
+
track(EXPRESS_CHECKOUT_EVENTS.FALLBACK_CHECKOUT, { sku_id: skuId });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/clients/payment-checkout/messages.ts
|
|
158
|
+
var MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment and billing details and try again or contact us.";
|
|
159
|
+
|
|
160
|
+
// src/clients/payment-checkout/apple-session.ts
|
|
161
|
+
function startApplePaySession(config) {
|
|
162
|
+
console.log("[web-apple-pay] startApplePaySession", {
|
|
163
|
+
skuId: config.skuId,
|
|
164
|
+
countryCode: config.countryCode,
|
|
165
|
+
currencyCode: config.currencyCode,
|
|
166
|
+
totalAmount: config.totalAmount,
|
|
167
|
+
userLoggedIn: config.userLoggedIn
|
|
168
|
+
});
|
|
169
|
+
const paymentRequest = {
|
|
170
|
+
countryCode: config.countryCode,
|
|
171
|
+
currencyCode: config.currencyCode,
|
|
172
|
+
merchantCapabilities: ["supports3DS"],
|
|
173
|
+
supportedNetworks: config.supportedNetworks,
|
|
174
|
+
total: {
|
|
175
|
+
label: config.totalLabel,
|
|
176
|
+
amount: config.totalAmount
|
|
177
|
+
},
|
|
178
|
+
requiredBillingContactFields: ["postalAddress", "name"],
|
|
179
|
+
// Only request email from Apple Pay sheet if user is NOT logged in
|
|
180
|
+
...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
|
|
181
|
+
};
|
|
182
|
+
const session = new ApplePaySession(3, paymentRequest);
|
|
183
|
+
session.onvalidatemerchant = async (event) => {
|
|
184
|
+
console.log("[web-apple-pay] onvalidatemerchant: received", { validationURL: event.validationURL });
|
|
185
|
+
try {
|
|
186
|
+
const validationHost = new URL(event.validationURL).host;
|
|
187
|
+
if (!validationHost.endsWith("apple.com")) {
|
|
188
|
+
console.warn("[web-apple-pay] onvalidatemerchant: invalid host, aborting", { validationHost });
|
|
189
|
+
trackFallbackCheckout(config.skuId);
|
|
190
|
+
session.abort();
|
|
191
|
+
window.location.href = redirectToCheckoutFallback({
|
|
192
|
+
skuId: config.skuId,
|
|
193
|
+
returnUrl: config.returnUrl,
|
|
194
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const recaptchaToken = await getRecaptchaToken("verifyMerchant");
|
|
199
|
+
console.log("[web-apple-pay] onvalidatemerchant: calling validateAppleMerchant");
|
|
200
|
+
const response = await validateAppleMerchant({
|
|
201
|
+
validationURL: event.validationURL,
|
|
202
|
+
recaptchaToken
|
|
203
|
+
});
|
|
204
|
+
if (!response?.merchantSession) {
|
|
205
|
+
console.warn("[web-apple-pay] onvalidatemerchant: no merchantSession in response, aborting");
|
|
206
|
+
trackFallbackCheckout(config.skuId);
|
|
207
|
+
session.abort();
|
|
208
|
+
window.location.href = redirectToCheckoutFallback({
|
|
209
|
+
skuId: config.skuId,
|
|
210
|
+
returnUrl: config.returnUrl,
|
|
211
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
console.log("[web-apple-pay] onvalidatemerchant: merchant validated successfully");
|
|
216
|
+
session.completeMerchantValidation(response.merchantSession);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error("[web-apple-pay] onvalidatemerchant: error during validation", err);
|
|
219
|
+
trackFallbackCheckout(config.skuId);
|
|
220
|
+
session.abort();
|
|
221
|
+
window.location.href = redirectToCheckoutFallback({
|
|
222
|
+
skuId: config.skuId,
|
|
223
|
+
returnUrl: config.returnUrl,
|
|
224
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
session.onpaymentauthorized = (event) => {
|
|
229
|
+
console.log("[web-apple-pay] onpaymentauthorized: payment authorized by user", { skuId: config.skuId });
|
|
230
|
+
trackWalletPaymentAuthorised(config.skuId);
|
|
231
|
+
config.onPaymentAuthorized(event, session);
|
|
232
|
+
};
|
|
233
|
+
session.oncancel = () => {
|
|
234
|
+
console.log("[web-apple-pay] oncancel: payment sheet cancelled by user", { skuId: config.skuId });
|
|
235
|
+
config.onCancel?.();
|
|
236
|
+
};
|
|
237
|
+
console.log("[web-apple-pay] startApplePaySession: beginning session");
|
|
238
|
+
trackWalletPaymentSheetOpen(config.skuId);
|
|
239
|
+
session.begin();
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
startApplePaySession
|
|
243
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Pay billing contact local validator.
|
|
3
|
+
*
|
|
4
|
+
* Validates required billing fields BEFORE sending to the BFF /v1/wallet/purchase.
|
|
5
|
+
* On validation failure, throws ContactValidationError with ApplePayError[] objects
|
|
6
|
+
* that Apple uses to highlight problematic billing fields inline — keeping the sheet open.
|
|
7
|
+
*/
|
|
8
|
+
export type BillingValidationResult = {
|
|
9
|
+
firstName: string;
|
|
10
|
+
lastName: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Validates the billing contact from the Apple Pay wallet.
|
|
14
|
+
*
|
|
15
|
+
* @param contact - Billing address from the Apple Pay wallet
|
|
16
|
+
* @param supportedBillingCountries - Country codes the offer supports billing to
|
|
17
|
+
* @throws {ContactValidationError} when required fields are missing — Apple Pay
|
|
18
|
+
* sheet stays open and the first error text is shown to the user as a summary
|
|
19
|
+
* @returns firstName and lastName extracted from the contact
|
|
20
|
+
*/
|
|
21
|
+
export declare const billingContactDetails: (contact: ApplePayJS.ApplePayPaymentContact, supportedBillingCountries: string[]) => BillingValidationResult;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/utils/expressCheckoutTracking.ts
|
|
2
|
+
var emitTedlEvent = (event) => {
|
|
3
|
+
window.tedl = window.tedl ?? {};
|
|
4
|
+
window.tedl.events = window.tedl.events ?? [];
|
|
5
|
+
window.tedl.events.push(event);
|
|
6
|
+
};
|
|
7
|
+
var track = (action, extra) => {
|
|
8
|
+
emitTedlEvent({
|
|
9
|
+
name: "user.journey",
|
|
10
|
+
type: "checkout",
|
|
11
|
+
action,
|
|
12
|
+
info: "applepay",
|
|
13
|
+
...extra
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var trackValidationFieldError = (field, errorType) => {
|
|
17
|
+
track(`validation_${field}_${errorType}`, {});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/clients/payment-checkout/messages.ts
|
|
21
|
+
var MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
22
|
+
var MSG_UNSUPPORTED_BILLING_COUNTRY = "Billing country does not match offer price zone";
|
|
23
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
24
|
+
|
|
25
|
+
// src/clients/payment-checkout/contact-validation.ts
|
|
26
|
+
var pushFieldError = (errors, billingOrShippingContact = "billing", field, errorType, errorText) => {
|
|
27
|
+
const errorCode = billingOrShippingContact === "billing" ? "billingContactInvalid" : "shippingContactInvalid";
|
|
28
|
+
errors.push(new ApplePayError(errorCode, field, errorText));
|
|
29
|
+
trackValidationFieldError(field, errorType);
|
|
30
|
+
return "";
|
|
31
|
+
};
|
|
32
|
+
var pushRequiredFieldError = (errors, billingOrShippingContact = "billing", field) => {
|
|
33
|
+
return pushFieldError(
|
|
34
|
+
errors,
|
|
35
|
+
billingOrShippingContact,
|
|
36
|
+
field,
|
|
37
|
+
"required",
|
|
38
|
+
MSG_MISSING_REQUIRED_FIELD
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
var ContactValidationError = class extends Error {
|
|
42
|
+
constructor(errors) {
|
|
43
|
+
super(MSG_CONTACT_VALIDATION_INTERNAL);
|
|
44
|
+
this.errors = errors;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/clients/payment-checkout/billing-contact-details.ts
|
|
49
|
+
var countriesWithoutCityField = ["KY", "CK", "NR", "TC"];
|
|
50
|
+
var countriesRequiringAllFields = ["US", "CA", "GB"];
|
|
51
|
+
var resolveCountryConfig = (country) => {
|
|
52
|
+
const hasCityField = !countriesWithoutCityField.includes(country);
|
|
53
|
+
const requiresStateAndPostcode = countriesRequiringAllFields.includes(country);
|
|
54
|
+
return {
|
|
55
|
+
hasCityField,
|
|
56
|
+
requiresStateAndPostcode,
|
|
57
|
+
// if there is no city field, we require state and use it in place of city
|
|
58
|
+
requiresStateField: !hasCityField || requiresStateAndPostcode
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
var validateAddressFields = (errors, contact, { hasCityField, requiresStateAndPostcode, requiresStateField }) => {
|
|
62
|
+
const locality = !hasCityField ? contact.administrativeArea : contact.locality;
|
|
63
|
+
if (!contact.addressLines?.at(0)?.trim()) {
|
|
64
|
+
pushRequiredFieldError(errors, "billing", "addressLines");
|
|
65
|
+
}
|
|
66
|
+
if (!locality?.trim() && hasCityField) {
|
|
67
|
+
pushRequiredFieldError(errors, "billing", "locality");
|
|
68
|
+
}
|
|
69
|
+
if (!contact.administrativeArea?.trim() && requiresStateField) {
|
|
70
|
+
pushRequiredFieldError(errors, "billing", "administrativeArea");
|
|
71
|
+
}
|
|
72
|
+
if (!contact.postalCode?.trim() && requiresStateAndPostcode) {
|
|
73
|
+
pushRequiredFieldError(errors, "billing", "postalCode");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var validateNameFields = (errors, contact) => {
|
|
77
|
+
const firstName = contact.givenName?.trim() || "";
|
|
78
|
+
const lastName = contact.familyName?.trim() || "";
|
|
79
|
+
if (!firstName || !lastName) {
|
|
80
|
+
pushRequiredFieldError(errors, "billing", "name");
|
|
81
|
+
}
|
|
82
|
+
return { firstName, lastName };
|
|
83
|
+
};
|
|
84
|
+
var validateCountrySupport = (errors, country, supportedBillingCountries) => {
|
|
85
|
+
if (!supportedBillingCountries.includes(country)) {
|
|
86
|
+
pushFieldError(
|
|
87
|
+
errors,
|
|
88
|
+
"billing",
|
|
89
|
+
"countryCode",
|
|
90
|
+
"not-supported",
|
|
91
|
+
MSG_UNSUPPORTED_BILLING_COUNTRY
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var billingContactDetails = (contact, supportedBillingCountries) => {
|
|
96
|
+
const errors = [];
|
|
97
|
+
const country = contact.countryCode?.toUpperCase() || pushRequiredFieldError(errors, "billing", "country");
|
|
98
|
+
const config = resolveCountryConfig(country);
|
|
99
|
+
validateAddressFields(errors, contact, config);
|
|
100
|
+
const { firstName, lastName } = validateNameFields(errors, contact);
|
|
101
|
+
validateCountrySupport(errors, country, supportedBillingCountries);
|
|
102
|
+
if (errors.length > 0) {
|
|
103
|
+
throw new ContactValidationError(errors);
|
|
104
|
+
}
|
|
105
|
+
return { firstName, lastName };
|
|
106
|
+
};
|
|
107
|
+
export {
|
|
108
|
+
billingContactDetails
|
|
109
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ApplePayRuntimeConfig {
|
|
2
|
+
merchantId: string;
|
|
3
|
+
recaptchaSiteKey?: string;
|
|
4
|
+
debugBypassEnabled?: boolean;
|
|
5
|
+
walletApiBaseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const configureApplePay: (config: ApplePayRuntimeConfig) => void;
|
|
8
|
+
export declare const getApplePayConfig: () => ApplePayRuntimeConfig | null;
|
|
9
|
+
export declare const resetApplePayConfig: () => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/clients/payment-checkout/config.ts
|
|
2
|
+
var runtimeConfig = null;
|
|
3
|
+
var configureApplePay = (config) => {
|
|
4
|
+
console.log("[web-apple-pay] configureApplePay called", {
|
|
5
|
+
merchantId: config.merchantId,
|
|
6
|
+
walletApiBaseUrl: config.walletApiBaseUrl,
|
|
7
|
+
debugBypassEnabled: config.debugBypassEnabled,
|
|
8
|
+
recaptchaSiteKey: config.recaptchaSiteKey ? "***" : void 0
|
|
9
|
+
});
|
|
10
|
+
runtimeConfig = {
|
|
11
|
+
...config
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
var getApplePayConfig = () => runtimeConfig;
|
|
15
|
+
var resetApplePayConfig = () => {
|
|
16
|
+
console.log("[web-apple-pay] resetApplePayConfig called");
|
|
17
|
+
runtimeConfig = null;
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
configureApplePay,
|
|
21
|
+
getApplePayConfig,
|
|
22
|
+
resetApplePayConfig
|
|
23
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Pay contact validation helpers.
|
|
3
|
+
* Ported from sma-checkout — helpers/contact-validation.ts
|
|
4
|
+
*
|
|
5
|
+
* pushFieldError / pushRequiredFieldError also fire TEDL analytics per field.
|
|
6
|
+
*/
|
|
7
|
+
import ApplePayErrorContactField = ApplePayJS.ApplePayErrorContactField;
|
|
8
|
+
/**
|
|
9
|
+
* Appends an ApplePayError to the error accumulator and fires a TEDL analytics
|
|
10
|
+
* event for the failing field. Returns '' so it can be used inline with ||.
|
|
11
|
+
*/
|
|
12
|
+
export declare const pushFieldError: (errors: ApplePayError[], billingOrShippingContact: "billing" | "shipping" | undefined, field: ApplePayErrorContactField, errorType: "required" | "format" | "not-supported", errorText: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Convenience wrapper for required-field errors.
|
|
15
|
+
* Uses the standard 'Missing required field' message — matches sma-checkout exactly.
|
|
16
|
+
*/
|
|
17
|
+
export declare const pushRequiredFieldError: (errors: ApplePayError[], billingOrShippingContact: "billing" | "shipping" | undefined, field: ApplePayErrorContactField) => string;
|
|
18
|
+
export declare class ContactValidationError extends Error {
|
|
19
|
+
errors: ApplePayError[];
|
|
20
|
+
constructor(errors: ApplePayError[]);
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/utils/expressCheckoutTracking.ts
|
|
2
|
+
var emitTedlEvent = (event) => {
|
|
3
|
+
window.tedl = window.tedl ?? {};
|
|
4
|
+
window.tedl.events = window.tedl.events ?? [];
|
|
5
|
+
window.tedl.events.push(event);
|
|
6
|
+
};
|
|
7
|
+
var track = (action, extra) => {
|
|
8
|
+
emitTedlEvent({
|
|
9
|
+
name: "user.journey",
|
|
10
|
+
type: "checkout",
|
|
11
|
+
action,
|
|
12
|
+
info: "applepay",
|
|
13
|
+
...extra
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var trackValidationFieldError = (field, errorType) => {
|
|
17
|
+
track(`validation_${field}_${errorType}`, {});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/clients/payment-checkout/messages.ts
|
|
21
|
+
var MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
22
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
23
|
+
|
|
24
|
+
// src/clients/payment-checkout/contact-validation.ts
|
|
25
|
+
var pushFieldError = (errors, billingOrShippingContact = "billing", field, errorType, errorText) => {
|
|
26
|
+
const errorCode = billingOrShippingContact === "billing" ? "billingContactInvalid" : "shippingContactInvalid";
|
|
27
|
+
errors.push(new ApplePayError(errorCode, field, errorText));
|
|
28
|
+
trackValidationFieldError(field, errorType);
|
|
29
|
+
return "";
|
|
30
|
+
};
|
|
31
|
+
var pushRequiredFieldError = (errors, billingOrShippingContact = "billing", field) => {
|
|
32
|
+
return pushFieldError(
|
|
33
|
+
errors,
|
|
34
|
+
billingOrShippingContact,
|
|
35
|
+
field,
|
|
36
|
+
"required",
|
|
37
|
+
MSG_MISSING_REQUIRED_FIELD
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
var ContactValidationError = class extends Error {
|
|
41
|
+
constructor(errors) {
|
|
42
|
+
super(MSG_CONTACT_VALIDATION_INTERNAL);
|
|
43
|
+
this.errors = errors;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
ContactValidationError,
|
|
48
|
+
pushFieldError,
|
|
49
|
+
pushRequiredFieldError
|
|
50
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
console.log("[web-apple-pay] isApplePayAvailable: checking availability");
|
|
14
|
+
if (bypassApplePayChecks()) {
|
|
15
|
+
console.log("[web-apple-pay] isApplePayAvailable: bypass active \u2192 true");
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (!isAppleDeviceOrSafari()) {
|
|
19
|
+
console.log("[web-apple-pay] isApplePayAvailable: not Apple device or Safari \u2192 false");
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const applePaySession = window.ApplePaySession;
|
|
23
|
+
if (!applePaySession) {
|
|
24
|
+
console.log("[web-apple-pay] isApplePayAvailable: ApplePaySession not found on window \u2192 false");
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
if (typeof applePaySession.canMakePayments !== "function") {
|
|
29
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePayments is not a function \u2192 false");
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!applePaySession.canMakePayments()) {
|
|
33
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePayments() returned false \u2192 false");
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const merchantId = getApplePayConfig()?.merchantId;
|
|
37
|
+
if (!merchantId) {
|
|
38
|
+
console.warn(
|
|
39
|
+
"[web-apple-pay] Missing merchantId. Call configureApplePay({ merchantId, ... }) before rendering Apple Pay components."
|
|
40
|
+
);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
console.log("[web-apple-pay] isApplePayAvailable : calling canMakePaymentsWithActiveCard", { merchantId });
|
|
44
|
+
const result = await applePaySession.canMakePaymentsWithActiveCard(merchantId);
|
|
45
|
+
console.log("[web-apple-pay] isApplePayAvailable: canMakePaymentsWithActiveCard \u2192", result);
|
|
46
|
+
return result;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Error checking Apple Pay active card status:", error);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
bypassApplePayChecks,
|
|
54
|
+
isAppleDeviceOrSafari,
|
|
55
|
+
isApplePayAvailable
|
|
56
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PaymentOptionsRequest, PaymentOptionsResponse, PurchaseRequest, PurchaseResponse, ValidateMerchantRequest, ValidateMerchantResponse } from './types';
|
|
2
|
+
export declare const apiFetch: <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
3
|
+
export declare const getPaymentOptions: (data: PaymentOptionsRequest) => Promise<PaymentOptionsResponse>;
|
|
4
|
+
export declare const validateAppleMerchant: (data: ValidateMerchantRequest) => Promise<ValidateMerchantResponse>;
|
|
5
|
+
export declare const performPurchase: (data: PurchaseRequest) => Promise<PurchaseResponse>;
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
var getPaymentOptions = (data) => {
|
|
58
|
+
const { sku, countryCode, currencyCode, checkoutUrl } = data;
|
|
59
|
+
const params = new URLSearchParams({ checkoutUrl });
|
|
60
|
+
return apiFetch(
|
|
61
|
+
`/v1/wallet/payment-options/sku/${sku}/${countryCode}/${currencyCode}?${params}`,
|
|
62
|
+
{ method: "GET" }
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
var validateAppleMerchant = (data) => {
|
|
66
|
+
return apiFetch("/v1/wallet/validate-merchant", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: JSON.stringify(data)
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
var performPurchase = (data) => {
|
|
72
|
+
return apiFetch("/v1/wallet/purchase", {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify(data),
|
|
75
|
+
credentials: "include"
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
export {
|
|
79
|
+
apiFetch,
|
|
80
|
+
getPaymentOptions,
|
|
81
|
+
performPurchase,
|
|
82
|
+
validateAppleMerchant
|
|
83
|
+
};
|