@economist/web-apple-pay 1.5.0 → 1.6.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/dist/index.js +963 -367
- package/dist/src/apple-pay-button.d.ts +13 -1
- package/dist/src/apple-pay-button.js +963 -367
- package/dist/src/apple-pay-button.stories.js +970 -383
- package/dist/src/apple-pay-modal.js +18 -316
- package/dist/src/apple-pay-modal.stories.js +963 -367
- package/dist/src/clients/payment-checkout/apple-session.d.ts +18 -0
- package/dist/src/clients/payment-checkout/apple-session.js +227 -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 +1 -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/index.d.ts +4 -0
- package/dist/src/clients/payment-checkout/index.js +25 -1
- 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 +375 -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 +128 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +963 -367
- package/dist/src/utils/billing-countries.d.ts +19 -0
- package/dist/src/utils/billing-countries.js +263 -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/recaptcha.d.ts +22 -0
- package/dist/src/utils/recaptcha.js +54 -0
- package/package.json +2 -1
|
@@ -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,227 @@
|
|
|
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
|
+
const paymentRequest = {
|
|
163
|
+
countryCode: config.countryCode,
|
|
164
|
+
currencyCode: config.currencyCode,
|
|
165
|
+
merchantCapabilities: ["supports3DS"],
|
|
166
|
+
supportedNetworks: config.supportedNetworks,
|
|
167
|
+
total: {
|
|
168
|
+
label: config.totalLabel,
|
|
169
|
+
amount: config.totalAmount
|
|
170
|
+
},
|
|
171
|
+
requiredBillingContactFields: ["postalAddress", "name"],
|
|
172
|
+
// Only request email from Apple Pay sheet if user is NOT logged in
|
|
173
|
+
...!config.userLoggedIn && { requiredShippingContactFields: ["email"] }
|
|
174
|
+
};
|
|
175
|
+
const session = new ApplePaySession(3, paymentRequest);
|
|
176
|
+
session.onvalidatemerchant = async (event) => {
|
|
177
|
+
try {
|
|
178
|
+
const validationHost = new URL(event.validationURL).host;
|
|
179
|
+
if (!validationHost.endsWith("apple.com")) {
|
|
180
|
+
trackFallbackCheckout(config.skuId);
|
|
181
|
+
session.abort();
|
|
182
|
+
window.location.href = redirectToCheckoutFallback({
|
|
183
|
+
skuId: config.skuId,
|
|
184
|
+
returnUrl: config.returnUrl,
|
|
185
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const recaptchaToken = await getRecaptchaToken("verifyMerchant");
|
|
190
|
+
const response = await validateAppleMerchant({
|
|
191
|
+
validationURL: event.validationURL,
|
|
192
|
+
recaptchaToken
|
|
193
|
+
});
|
|
194
|
+
if (!response?.merchantSession) {
|
|
195
|
+
trackFallbackCheckout(config.skuId);
|
|
196
|
+
session.abort();
|
|
197
|
+
window.location.href = redirectToCheckoutFallback({
|
|
198
|
+
skuId: config.skuId,
|
|
199
|
+
returnUrl: config.returnUrl,
|
|
200
|
+
inlineError: MSG_PAYMENT_GENERIC_ERROR
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
session.completeMerchantValidation(response.merchantSession);
|
|
205
|
+
} catch {
|
|
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
|
+
}
|
|
214
|
+
};
|
|
215
|
+
session.onpaymentauthorized = (event) => {
|
|
216
|
+
trackWalletPaymentAuthorised(config.skuId);
|
|
217
|
+
config.onPaymentAuthorized(event, session);
|
|
218
|
+
};
|
|
219
|
+
session.oncancel = () => {
|
|
220
|
+
config.onCancel?.();
|
|
221
|
+
};
|
|
222
|
+
trackWalletPaymentSheetOpen(config.skuId);
|
|
223
|
+
session.begin();
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
startApplePaySession
|
|
227
|
+
};
|
|
@@ -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,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
|
+
};
|
|
@@ -1 +1,5 @@
|
|
|
1
|
+
import type { PaymentOptionsRequest, PaymentOptionsResponse, PurchaseRequest, PurchaseResponse, ValidateMerchantRequest, ValidateMerchantResponse } from './types';
|
|
1
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>;
|
|
@@ -54,6 +54,30 @@ var apiFetch = async (path, options = {}) => {
|
|
|
54
54
|
}
|
|
55
55
|
return response.json();
|
|
56
56
|
};
|
|
57
|
+
var getPaymentOptions = (data) => {
|
|
58
|
+
const { sku, countryCode, currencyCode, checkoutUrl } = data;
|
|
59
|
+
const params = new URLSearchParams({ checkoutUrl });
|
|
60
|
+
return apiFetch(
|
|
61
|
+
`/ecomb2c/v1/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
|
+
};
|
|
57
78
|
export {
|
|
58
|
-
apiFetch
|
|
79
|
+
apiFetch,
|
|
80
|
+
getPaymentOptions,
|
|
81
|
+
performPurchase,
|
|
82
|
+
validateAppleMerchant
|
|
59
83
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical user-facing error messages for the Apple Pay Express Checkout flow.
|
|
3
|
+
*
|
|
4
|
+
* All copy here is verified against sma-checkout (src/actions/constants/index.ts
|
|
5
|
+
* and handlers/create-on-payment-authorized-handler/) to ensure parity with the
|
|
6
|
+
* reference implementation. Update both repos if copy changes.
|
|
7
|
+
*/
|
|
8
|
+
/** Shown when a required Apple Pay contact field is absent. */
|
|
9
|
+
export declare const MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
10
|
+
/** Shown when the email address format is invalid (matches sma-checkout FormErrorMessages.EMAIL_WRONG_FORMAT). */
|
|
11
|
+
export declare const MSG_EMAIL_WRONG_FORMAT = "That email doesn\u2019t look right. Please enter a valid email address.";
|
|
12
|
+
/** Shown when the email address exceeds 80 characters (matches sma-checkout FormErrorMessages.MAX_80_CHARS). */
|
|
13
|
+
export declare const MSG_EMAIL_TOO_LONG = "This field must be 80 characters or less.";
|
|
14
|
+
/** Shown when the billing country is not in the supported list for the offer. */
|
|
15
|
+
export declare const MSG_UNSUPPORTED_BILLING_COUNTRY = "Billing country does not match offer price zone";
|
|
16
|
+
/**
|
|
17
|
+
* Generic fallback error shown to the user when merchant validation or the
|
|
18
|
+
* purchase call fails. Redirected as an inlineError on the checkout fallback page.
|
|
19
|
+
* Matches sma-checkout GENERIC_ERROR_MSG pattern.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment and billing details and try again or contact us.";
|
|
22
|
+
/**
|
|
23
|
+
* Shown when a reCAPTCHA security check fails.
|
|
24
|
+
* Matches sma-checkout reference: handlers/create-on-payment-authorized-handler/subscribe.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MSG_RECAPTCHA_VALIDATION_FAILED = "Security check failed. Please try again.";
|
|
27
|
+
/**
|
|
28
|
+
* Shown when the BFF returns an EMAIL_COLLISION error (account already exists).
|
|
29
|
+
* Matches sma-checkout L103-104.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MSG_EMAIL_COLLISION = "This email address is already linked to an account. Please log in, or try a different email address.";
|
|
32
|
+
/** Internal ContactValidationError message — not shown to the user directly. */
|
|
33
|
+
export declare const MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/clients/payment-checkout/messages.ts
|
|
2
|
+
var MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
3
|
+
var MSG_EMAIL_WRONG_FORMAT = "That email doesn\u2019t look right. Please enter a valid email address.";
|
|
4
|
+
var MSG_EMAIL_TOO_LONG = "This field must be 80 characters or less.";
|
|
5
|
+
var MSG_UNSUPPORTED_BILLING_COUNTRY = "Billing country does not match offer price zone";
|
|
6
|
+
var MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment and billing details and try again or contact us.";
|
|
7
|
+
var MSG_RECAPTCHA_VALIDATION_FAILED = "Security check failed. Please try again.";
|
|
8
|
+
var MSG_EMAIL_COLLISION = "This email address is already linked to an account. Please log in, or try a different email address.";
|
|
9
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
10
|
+
export {
|
|
11
|
+
MSG_CONTACT_VALIDATION_INTERNAL,
|
|
12
|
+
MSG_EMAIL_COLLISION,
|
|
13
|
+
MSG_EMAIL_TOO_LONG,
|
|
14
|
+
MSG_EMAIL_WRONG_FORMAT,
|
|
15
|
+
MSG_MISSING_REQUIRED_FIELD,
|
|
16
|
+
MSG_PAYMENT_GENERIC_ERROR,
|
|
17
|
+
MSG_RECAPTCHA_VALIDATION_FAILED,
|
|
18
|
+
MSG_UNSUPPORTED_BILLING_COUNTRY
|
|
19
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central onpaymentauthorized orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* The BFF /v1/wallet/purchase orchestrates ALL backend logic:
|
|
5
|
+
* - Eligibility check
|
|
6
|
+
* - Passwordless user registration (if anonymous)
|
|
7
|
+
* - Basket creation
|
|
8
|
+
* - Payment finalization
|
|
9
|
+
*
|
|
10
|
+
* This handler is responsible for:
|
|
11
|
+
* 1. Local billing/shipping contact validation (keeps Apple Pay sheet open on error)
|
|
12
|
+
* 2. Calling the single /v1/wallet/purchase endpoint
|
|
13
|
+
* 3. Handling success → redirect to /checkout/complete
|
|
14
|
+
* 4. Handling structured BFF errors → deterministic UI actions per errorType
|
|
15
|
+
*/
|
|
16
|
+
import type { SessionResponse } from './types';
|
|
17
|
+
export type PaymentHandlerConfig = {
|
|
18
|
+
skuId: string;
|
|
19
|
+
country: string;
|
|
20
|
+
returnUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional `&country=` query param captured from the page URL at the moment
|
|
23
|
+
* the payment sheet was authorised (via `buildQueryString()`).
|
|
24
|
+
* Appended verbatim to the checkout-complete and fallback redirect URLs so
|
|
25
|
+
* the country context is preserved across the redirect.
|
|
26
|
+
* e.g. `'&country=US'`, `'&country=GB%2FIE'`, or `''` when absent.
|
|
27
|
+
*/
|
|
28
|
+
queryString?: string;
|
|
29
|
+
supportedBillingCountries: string[];
|
|
30
|
+
recaptchaTokens: {
|
|
31
|
+
checkEligibility: string;
|
|
32
|
+
newBasket: string;
|
|
33
|
+
registerUser: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare function handlePaymentAuthorized(event: ApplePayJS.ApplePayPaymentAuthorizedEvent, session: ApplePaySession, sessionData: SessionResponse, config: PaymentHandlerConfig): Promise<void>;
|