@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,375 @@
|
|
|
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 performPurchase = (data) => {
|
|
58
|
+
return apiFetch("/v1/wallet/purchase", {
|
|
59
|
+
method: "POST",
|
|
60
|
+
body: JSON.stringify(data),
|
|
61
|
+
credentials: "include"
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/utils/expressCheckoutTracking.ts
|
|
66
|
+
var EXPRESS_CHECKOUT_EVENTS = {
|
|
67
|
+
/** Wallet button displayed */
|
|
68
|
+
IMPRESSION: "express_wallet_impression",
|
|
69
|
+
/** Wallet button clicked */
|
|
70
|
+
BUTTON_CLICK: "express_wallet_click",
|
|
71
|
+
/** Apple Pay sheet opened */
|
|
72
|
+
SHEET_OPEN: "wallet_payment_sheet_open",
|
|
73
|
+
/** Payment authorised by the user in the Apple Pay sheet */
|
|
74
|
+
AUTHORISED: "wallet_payment_authorised",
|
|
75
|
+
/** Purchase completed — BFF returned success */
|
|
76
|
+
SUCCESS: "wallet_payment_success",
|
|
77
|
+
/** Payment failed — BFF returned error */
|
|
78
|
+
FAILURE: "wallet_payment_failure",
|
|
79
|
+
/** Redirect to standard checkout (any fallback path) */
|
|
80
|
+
FALLBACK_CHECKOUT: "fallback_checkout"
|
|
81
|
+
};
|
|
82
|
+
var emitTedlEvent = (event) => {
|
|
83
|
+
window.tedl = window.tedl ?? {};
|
|
84
|
+
window.tedl.events = window.tedl.events ?? [];
|
|
85
|
+
window.tedl.events.push(event);
|
|
86
|
+
};
|
|
87
|
+
var track = (action, extra) => {
|
|
88
|
+
emitTedlEvent({
|
|
89
|
+
name: "user.journey",
|
|
90
|
+
type: "checkout",
|
|
91
|
+
action,
|
|
92
|
+
info: "applepay",
|
|
93
|
+
...extra
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
var trackWalletPaymentSuccess = (skuId, basketId) => {
|
|
97
|
+
track(EXPRESS_CHECKOUT_EVENTS.SUCCESS, {
|
|
98
|
+
sku_id: skuId,
|
|
99
|
+
basket_id: basketId
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
var trackWalletPaymentFailure = (skuId, reason) => {
|
|
103
|
+
track(EXPRESS_CHECKOUT_EVENTS.FAILURE, {
|
|
104
|
+
sku_id: skuId,
|
|
105
|
+
failure_reason: reason
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
var trackFallbackCheckout = (skuId) => {
|
|
109
|
+
track(EXPRESS_CHECKOUT_EVENTS.FALLBACK_CHECKOUT, { sku_id: skuId });
|
|
110
|
+
};
|
|
111
|
+
var trackValidationFieldError = (field, errorType) => {
|
|
112
|
+
track(`validation_${field}_${errorType}`, {});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/clients/payment-checkout/messages.ts
|
|
116
|
+
var MSG_MISSING_REQUIRED_FIELD = "Missing required field";
|
|
117
|
+
var MSG_EMAIL_WRONG_FORMAT = "That email doesn\u2019t look right. Please enter a valid email address.";
|
|
118
|
+
var MSG_EMAIL_TOO_LONG = "This field must be 80 characters or less.";
|
|
119
|
+
var MSG_UNSUPPORTED_BILLING_COUNTRY = "Billing country does not match offer price zone";
|
|
120
|
+
var MSG_PAYMENT_GENERIC_ERROR = "Something went wrong. Please check your payment and billing details and try again or contact us.";
|
|
121
|
+
var MSG_RECAPTCHA_VALIDATION_FAILED = "Security check failed. Please try again.";
|
|
122
|
+
var MSG_EMAIL_COLLISION = "This email address is already linked to an account. Please log in, or try a different email address.";
|
|
123
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
124
|
+
|
|
125
|
+
// src/clients/payment-checkout/contact-validation.ts
|
|
126
|
+
var pushFieldError = (errors, billingOrShippingContact = "billing", field, errorType, errorText) => {
|
|
127
|
+
const errorCode = billingOrShippingContact === "billing" ? "billingContactInvalid" : "shippingContactInvalid";
|
|
128
|
+
errors.push(new ApplePayError(errorCode, field, errorText));
|
|
129
|
+
trackValidationFieldError(field, errorType);
|
|
130
|
+
return "";
|
|
131
|
+
};
|
|
132
|
+
var pushRequiredFieldError = (errors, billingOrShippingContact = "billing", field) => {
|
|
133
|
+
return pushFieldError(
|
|
134
|
+
errors,
|
|
135
|
+
billingOrShippingContact,
|
|
136
|
+
field,
|
|
137
|
+
"required",
|
|
138
|
+
MSG_MISSING_REQUIRED_FIELD
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
var ContactValidationError = class extends Error {
|
|
142
|
+
constructor(errors) {
|
|
143
|
+
super(MSG_CONTACT_VALIDATION_INTERNAL);
|
|
144
|
+
this.errors = errors;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/clients/payment-checkout/billing-contact-details.ts
|
|
149
|
+
var countriesWithoutCityField = ["KY", "CK", "NR", "TC"];
|
|
150
|
+
var countriesRequiringAllFields = ["US", "CA", "GB"];
|
|
151
|
+
var resolveCountryConfig = (country) => {
|
|
152
|
+
const hasCityField = !countriesWithoutCityField.includes(country);
|
|
153
|
+
const requiresStateAndPostcode = countriesRequiringAllFields.includes(country);
|
|
154
|
+
return {
|
|
155
|
+
hasCityField,
|
|
156
|
+
requiresStateAndPostcode,
|
|
157
|
+
// if there is no city field, we require state and use it in place of city
|
|
158
|
+
requiresStateField: !hasCityField || requiresStateAndPostcode
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
var validateAddressFields = (errors, contact, { hasCityField, requiresStateAndPostcode, requiresStateField }) => {
|
|
162
|
+
const locality = !hasCityField ? contact.administrativeArea : contact.locality;
|
|
163
|
+
if (!contact.addressLines?.at(0)?.trim()) {
|
|
164
|
+
pushRequiredFieldError(errors, "billing", "addressLines");
|
|
165
|
+
}
|
|
166
|
+
if (!locality?.trim() && hasCityField) {
|
|
167
|
+
pushRequiredFieldError(errors, "billing", "locality");
|
|
168
|
+
}
|
|
169
|
+
if (!contact.administrativeArea?.trim() && requiresStateField) {
|
|
170
|
+
pushRequiredFieldError(errors, "billing", "administrativeArea");
|
|
171
|
+
}
|
|
172
|
+
if (!contact.postalCode?.trim() && requiresStateAndPostcode) {
|
|
173
|
+
pushRequiredFieldError(errors, "billing", "postalCode");
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
var validateNameFields = (errors, contact) => {
|
|
177
|
+
const firstName = contact.givenName?.trim() || "";
|
|
178
|
+
const lastName = contact.familyName?.trim() || "";
|
|
179
|
+
if (!firstName || !lastName) {
|
|
180
|
+
pushRequiredFieldError(errors, "billing", "name");
|
|
181
|
+
}
|
|
182
|
+
return { firstName, lastName };
|
|
183
|
+
};
|
|
184
|
+
var validateCountrySupport = (errors, country, supportedBillingCountries) => {
|
|
185
|
+
if (!supportedBillingCountries.includes(country)) {
|
|
186
|
+
pushFieldError(
|
|
187
|
+
errors,
|
|
188
|
+
"billing",
|
|
189
|
+
"countryCode",
|
|
190
|
+
"not-supported",
|
|
191
|
+
MSG_UNSUPPORTED_BILLING_COUNTRY
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var billingContactDetails = (contact, supportedBillingCountries) => {
|
|
196
|
+
const errors = [];
|
|
197
|
+
const country = contact.countryCode?.toUpperCase() || pushRequiredFieldError(errors, "billing", "country");
|
|
198
|
+
const config = resolveCountryConfig(country);
|
|
199
|
+
validateAddressFields(errors, contact, config);
|
|
200
|
+
const { firstName, lastName } = validateNameFields(errors, contact);
|
|
201
|
+
validateCountrySupport(errors, country, supportedBillingCountries);
|
|
202
|
+
if (errors.length > 0) {
|
|
203
|
+
throw new ContactValidationError(errors);
|
|
204
|
+
}
|
|
205
|
+
return { firstName, lastName };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/clients/payment-checkout/shipping-contact-details.ts
|
|
209
|
+
var EMAIL_FORMAT_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
210
|
+
var shippingContactDetails = (shippingContact) => {
|
|
211
|
+
const errors = [];
|
|
212
|
+
let emailAddress = "";
|
|
213
|
+
if (shippingContact?.emailAddress === void 0 || shippingContact?.emailAddress === "") {
|
|
214
|
+
pushRequiredFieldError(errors, "shipping", "emailAddress");
|
|
215
|
+
} else {
|
|
216
|
+
emailAddress = shippingContact.emailAddress.trim();
|
|
217
|
+
if (emailAddress.length > 80) {
|
|
218
|
+
pushFieldError(
|
|
219
|
+
errors,
|
|
220
|
+
"shipping",
|
|
221
|
+
"emailAddress",
|
|
222
|
+
"format",
|
|
223
|
+
MSG_EMAIL_TOO_LONG
|
|
224
|
+
);
|
|
225
|
+
} else if (!EMAIL_FORMAT_REGEX.test(emailAddress)) {
|
|
226
|
+
pushFieldError(
|
|
227
|
+
errors,
|
|
228
|
+
"shipping",
|
|
229
|
+
"emailAddress",
|
|
230
|
+
"format",
|
|
231
|
+
MSG_EMAIL_WRONG_FORMAT
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (errors.length > 0) {
|
|
236
|
+
throw new ContactValidationError(errors);
|
|
237
|
+
}
|
|
238
|
+
return { emailAddress };
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/utils/expressCheckoutRedirect.ts
|
|
242
|
+
var decodeSku = (sku) => {
|
|
243
|
+
try {
|
|
244
|
+
return atob(sku.substring(sku.indexOf("-") + 1));
|
|
245
|
+
} catch {
|
|
246
|
+
return sku;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var redirectToCheckoutComplete = (params) => {
|
|
250
|
+
const url = new URL("/checkout/complete", window.location.origin);
|
|
251
|
+
url.searchParams.set("basketId", params.basketId);
|
|
252
|
+
if (params.queryString) {
|
|
253
|
+
const qs = params.queryString.startsWith("?") ? params.queryString.slice(1) : params.queryString;
|
|
254
|
+
new URLSearchParams(qs).forEach((v, k) => url.searchParams.set(k, v));
|
|
255
|
+
}
|
|
256
|
+
url.searchParams.set("lmo_offer", params.skuId);
|
|
257
|
+
url.searchParams.set("status", params.status);
|
|
258
|
+
url.searchParams.set("redirectToCheckout", "true");
|
|
259
|
+
url.searchParams.set("returnUrl", params.returnUrl);
|
|
260
|
+
return url.pathname + url.search;
|
|
261
|
+
};
|
|
262
|
+
var redirectToCheckoutFallback = (params) => {
|
|
263
|
+
const url = new URL(window.location.href);
|
|
264
|
+
url.pathname = "/checkout";
|
|
265
|
+
url.searchParams.set("lmo_offer", params.skuId);
|
|
266
|
+
if (params.inlineError)
|
|
267
|
+
url.searchParams.set("inlineError", params.inlineError);
|
|
268
|
+
if (params.email) url.searchParams.set("email", params.email);
|
|
269
|
+
if (params.emailError) url.searchParams.set("emailError", params.emailError);
|
|
270
|
+
url.searchParams.set("returnUrl", params.returnUrl);
|
|
271
|
+
return url.pathname + url.search;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/clients/payment-checkout/payment-handler.ts
|
|
275
|
+
var MSG_ELIGIBILITY = "Something went wrong, please try again";
|
|
276
|
+
var MSG_REGISTRATION = "We couldn't create your account. Please enter your email or try a different login method.";
|
|
277
|
+
var failApplePaySession = (session, errors = []) => {
|
|
278
|
+
session.completePayment({
|
|
279
|
+
status: ApplePaySession.STATUS_FAILURE,
|
|
280
|
+
errors
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
function parseWalletApiError(err) {
|
|
284
|
+
const body = err instanceof ApiError ? err.body : err;
|
|
285
|
+
if (body && typeof body === "object" && "errorType" in body && "message" in body) {
|
|
286
|
+
return body;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function validateContacts(event, sessionData, supportedBillingCountries) {
|
|
291
|
+
billingContactDetails(
|
|
292
|
+
event.payment.billingContact,
|
|
293
|
+
supportedBillingCountries
|
|
294
|
+
);
|
|
295
|
+
if (!sessionData.loggedIn) {
|
|
296
|
+
return shippingContactDetails(event.payment.shippingContact).emailAddress;
|
|
297
|
+
}
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
function redirectFallback(skuId, returnUrl, options) {
|
|
301
|
+
trackFallbackCheckout(skuId);
|
|
302
|
+
window.location.href = redirectToCheckoutFallback({
|
|
303
|
+
skuId,
|
|
304
|
+
returnUrl,
|
|
305
|
+
...options
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
var ERROR_HANDLERS = {
|
|
309
|
+
EMAIL_COLLISION: ({ skuId, returnUrl, validatedEmail }) => redirectFallback(skuId, returnUrl, {
|
|
310
|
+
emailError: MSG_EMAIL_COLLISION,
|
|
311
|
+
email: validatedEmail
|
|
312
|
+
}),
|
|
313
|
+
PAYMENT_DECLINED: ({ skuId, returnUrl }) => (
|
|
314
|
+
// Matches sma-checkout catch-all: always redirect with generic inlineError.
|
|
315
|
+
redirectFallback(skuId, returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR })
|
|
316
|
+
),
|
|
317
|
+
ELIGIBILITY_REJECTED: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_ELIGIBILITY }),
|
|
318
|
+
REGISTRATION_FAILED: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_REGISTRATION }),
|
|
319
|
+
VALIDATION_ERROR: ({ skuId, returnUrl }) => redirectFallback(skuId, returnUrl, { inlineError: MSG_RECAPTCHA_VALIDATION_FAILED })
|
|
320
|
+
};
|
|
321
|
+
function handleStructuredError(ctx) {
|
|
322
|
+
const handler = ERROR_HANDLERS[ctx.walletError.errorType];
|
|
323
|
+
if (handler) {
|
|
324
|
+
handler(ctx);
|
|
325
|
+
} else {
|
|
326
|
+
redirectFallback(ctx.skuId, ctx.returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async function handlePaymentAuthorized(event, session, sessionData, config) {
|
|
330
|
+
const { skuId, returnUrl, queryString } = config;
|
|
331
|
+
let validatedEmail;
|
|
332
|
+
try {
|
|
333
|
+
validatedEmail = validateContacts(
|
|
334
|
+
event,
|
|
335
|
+
sessionData,
|
|
336
|
+
config.supportedBillingCountries
|
|
337
|
+
);
|
|
338
|
+
const result = await performPurchase({
|
|
339
|
+
applePayToken: event.payment.token,
|
|
340
|
+
skuId: config.skuId,
|
|
341
|
+
billingContact: event.payment.billingContact,
|
|
342
|
+
...event.payment.shippingContact && {
|
|
343
|
+
shippingContact: event.payment.shippingContact
|
|
344
|
+
},
|
|
345
|
+
recaptchaTokens: config.recaptchaTokens
|
|
346
|
+
});
|
|
347
|
+
trackWalletPaymentSuccess(skuId, result.basketId);
|
|
348
|
+
session.completePayment({ status: ApplePaySession.STATUS_SUCCESS });
|
|
349
|
+
const userStatus = sessionData.loggedIn ? "lex" : "nex";
|
|
350
|
+
window.location.href = redirectToCheckoutComplete({
|
|
351
|
+
basketId: result.basketId,
|
|
352
|
+
status: userStatus,
|
|
353
|
+
skuId: decodeSku(skuId),
|
|
354
|
+
returnUrl,
|
|
355
|
+
queryString
|
|
356
|
+
});
|
|
357
|
+
} catch (err) {
|
|
358
|
+
if (err instanceof ContactValidationError) {
|
|
359
|
+
failApplePaySession(session, err.errors);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.error("[web-apple-pay] onpaymentauthorized error", { skuId, err });
|
|
363
|
+
const walletError = parseWalletApiError(err);
|
|
364
|
+
trackWalletPaymentFailure(skuId, walletError?.errorType ?? "UNKNOWN");
|
|
365
|
+
failApplePaySession(session);
|
|
366
|
+
if (!walletError) {
|
|
367
|
+
redirectFallback(skuId, returnUrl, { inlineError: MSG_PAYMENT_GENERIC_ERROR });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
handleStructuredError({ skuId, returnUrl, validatedEmail, walletError });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export {
|
|
374
|
+
handlePaymentAuthorized
|
|
375
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Pay shipping contact local validator.
|
|
3
|
+
* Ported from sma-checkout — handlers/create-on-payment-authorized-handler/subscribe/shipping-contact-details.ts
|
|
4
|
+
*
|
|
5
|
+
* Validates the shipping contact's email address if present.
|
|
6
|
+
*/
|
|
7
|
+
export type ShippingValidationResult = {
|
|
8
|
+
emailAddress: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const shippingContactDetails: (shippingContact: ApplePayJS.ApplePayPaymentContact | undefined) => ShippingValidationResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
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_EMAIL_WRONG_FORMAT = "That email doesn\u2019t look right. Please enter a valid email address.";
|
|
23
|
+
var MSG_EMAIL_TOO_LONG = "This field must be 80 characters or less.";
|
|
24
|
+
var MSG_CONTACT_VALIDATION_INTERNAL = "Could not validate Apple Pay contact";
|
|
25
|
+
|
|
26
|
+
// src/clients/payment-checkout/contact-validation.ts
|
|
27
|
+
var pushFieldError = (errors, billingOrShippingContact = "billing", field, errorType, errorText) => {
|
|
28
|
+
const errorCode = billingOrShippingContact === "billing" ? "billingContactInvalid" : "shippingContactInvalid";
|
|
29
|
+
errors.push(new ApplePayError(errorCode, field, errorText));
|
|
30
|
+
trackValidationFieldError(field, errorType);
|
|
31
|
+
return "";
|
|
32
|
+
};
|
|
33
|
+
var pushRequiredFieldError = (errors, billingOrShippingContact = "billing", field) => {
|
|
34
|
+
return pushFieldError(
|
|
35
|
+
errors,
|
|
36
|
+
billingOrShippingContact,
|
|
37
|
+
field,
|
|
38
|
+
"required",
|
|
39
|
+
MSG_MISSING_REQUIRED_FIELD
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
var ContactValidationError = class extends Error {
|
|
43
|
+
constructor(errors) {
|
|
44
|
+
super(MSG_CONTACT_VALIDATION_INTERNAL);
|
|
45
|
+
this.errors = errors;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/clients/payment-checkout/shipping-contact-details.ts
|
|
50
|
+
var EMAIL_FORMAT_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
51
|
+
var shippingContactDetails = (shippingContact) => {
|
|
52
|
+
const errors = [];
|
|
53
|
+
let emailAddress = "";
|
|
54
|
+
if (shippingContact?.emailAddress === void 0 || shippingContact?.emailAddress === "") {
|
|
55
|
+
pushRequiredFieldError(errors, "shipping", "emailAddress");
|
|
56
|
+
} else {
|
|
57
|
+
emailAddress = shippingContact.emailAddress.trim();
|
|
58
|
+
if (emailAddress.length > 80) {
|
|
59
|
+
pushFieldError(
|
|
60
|
+
errors,
|
|
61
|
+
"shipping",
|
|
62
|
+
"emailAddress",
|
|
63
|
+
"format",
|
|
64
|
+
MSG_EMAIL_TOO_LONG
|
|
65
|
+
);
|
|
66
|
+
} else if (!EMAIL_FORMAT_REGEX.test(emailAddress)) {
|
|
67
|
+
pushFieldError(
|
|
68
|
+
errors,
|
|
69
|
+
"shipping",
|
|
70
|
+
"emailAddress",
|
|
71
|
+
"format",
|
|
72
|
+
MSG_EMAIL_WRONG_FORMAT
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (errors.length > 0) {
|
|
77
|
+
throw new ContactValidationError(errors);
|
|
78
|
+
}
|
|
79
|
+
return { emailAddress };
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
shippingContactDetails
|
|
83
|
+
};
|
|
@@ -1,3 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types mapping to the wallet-payments-api BFF.
|
|
3
|
+
* Aligned with UI-Integration-Mocks.md.
|
|
4
|
+
*/
|
|
5
|
+
export type PaymentOptionsRequest = {
|
|
6
|
+
sku: string;
|
|
7
|
+
countryCode: string;
|
|
8
|
+
currencyCode: string;
|
|
9
|
+
/** The current page URL, sent to Commerce Services for logging purposes. */
|
|
10
|
+
checkoutUrl: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Mirrors the Commerce Services GET /ecomb2c/v1/payment-options/sku/{sku}/{countryCode}/{currencyCode}
|
|
14
|
+
* response shape — matches sma-checkout: src/lib/commerce-services/types.ts PaymentOptionsForSkuResponseBody
|
|
15
|
+
*/
|
|
16
|
+
export type PaymentOptionsResponse = {
|
|
17
|
+
options: {
|
|
18
|
+
applePay?: {
|
|
19
|
+
provider: string;
|
|
20
|
+
settings: {
|
|
21
|
+
supportedNetworks: string[];
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
payPal?: {
|
|
25
|
+
provider: string;
|
|
26
|
+
};
|
|
27
|
+
googlePay?: {
|
|
28
|
+
provider: string;
|
|
29
|
+
settings: {
|
|
30
|
+
supportedNetworks: string[];
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
creditDebit?: {
|
|
34
|
+
provider: string;
|
|
35
|
+
settings: {
|
|
36
|
+
tenantId: string;
|
|
37
|
+
id: string;
|
|
38
|
+
token: string;
|
|
39
|
+
signature: string;
|
|
40
|
+
key: string;
|
|
41
|
+
locale: string;
|
|
42
|
+
url: string;
|
|
43
|
+
paymentGateway: string;
|
|
44
|
+
height: number;
|
|
45
|
+
supportedNetworks: string[];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* GET /v1/wallet/session — uses credentials: 'include' (cookie-based).
|
|
52
|
+
*
|
|
53
|
+
* BFF always returns userType, even for anonymous users.
|
|
54
|
+
* Scenario C (active-subscription) → frontend MUST suppress Apple Pay button.
|
|
55
|
+
*/
|
|
1
56
|
export type SessionResponse = {
|
|
2
57
|
loggedIn: false;
|
|
3
58
|
userType: 'new-eligible-user';
|
|
@@ -7,6 +62,79 @@ export type SessionResponse = {
|
|
|
7
62
|
contactId: string;
|
|
8
63
|
userType: 'active-subscription' | 'b2b-user' | 'new-eligible-user';
|
|
9
64
|
};
|
|
65
|
+
export type ValidateMerchantRequest = {
|
|
66
|
+
validationURL: string;
|
|
67
|
+
recaptchaToken: string;
|
|
68
|
+
};
|
|
69
|
+
export type ValidateMerchantResponse = {
|
|
70
|
+
merchantSession: Record<string, unknown>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Minimal shape of the Apple Pay token received from `onpaymentauthorized`.
|
|
74
|
+
* Mirrors the relevant subset of `ApplePayJS.ApplePayPaymentToken` without
|
|
75
|
+
* requiring the `@types/applepayjs` package as a dependency.
|
|
76
|
+
*/
|
|
77
|
+
export type ApplePayPaymentToken = {
|
|
78
|
+
paymentData: unknown;
|
|
79
|
+
paymentMethod: {
|
|
80
|
+
displayName?: string;
|
|
81
|
+
network?: string;
|
|
82
|
+
type?: string;
|
|
83
|
+
};
|
|
84
|
+
transactionIdentifier: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Subset of ApplePayJS.ApplePayPaymentContact used in purchase and shipping.
|
|
88
|
+
*/
|
|
89
|
+
export type ApplePayPaymentContact = {
|
|
90
|
+
emailAddress?: string;
|
|
91
|
+
givenName?: string;
|
|
92
|
+
familyName?: string;
|
|
93
|
+
phoneNumber?: string;
|
|
94
|
+
addressLines?: string[];
|
|
95
|
+
locality?: string;
|
|
96
|
+
administrativeArea?: string;
|
|
97
|
+
postalCode?: string;
|
|
98
|
+
countryCode?: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* POST /v1/wallet/purchase
|
|
102
|
+
*
|
|
103
|
+
* The BFF orchestrates eligibility check, passwordless registration,
|
|
104
|
+
* basket creation, and payment finalization in a single call.
|
|
105
|
+
*/
|
|
106
|
+
export type PurchaseRequest = {
|
|
107
|
+
applePayToken: ApplePayPaymentToken;
|
|
108
|
+
/**
|
|
109
|
+
* @TODO [BFF Contract Misalignment]
|
|
110
|
+
* BFF spec requests `offerData: { ratePlanId, offerId }` but Commerce Services
|
|
111
|
+
* only provide a `skuId`. Frontend uses `skuId` directly — BFF must be aligned.
|
|
112
|
+
*/
|
|
113
|
+
skuId: string;
|
|
114
|
+
billingContact: ApplePayPaymentContact;
|
|
115
|
+
/** Required if loggedIn: false — contains emailAddress from Apple Pay sheet. */
|
|
116
|
+
shippingContact?: ApplePayPaymentContact;
|
|
117
|
+
recaptchaTokens: {
|
|
118
|
+
checkEligibility: string;
|
|
119
|
+
newBasket: string;
|
|
120
|
+
registerUser: string;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
export type PurchaseResponse = {
|
|
124
|
+
basketId: string;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Structured error body returned by all /v1/wallet/* endpoints on 4xx/5xx.
|
|
128
|
+
*/
|
|
129
|
+
export type WalletApiError = {
|
|
130
|
+
errorType: 'SERVICE_UNAVAILABLE' | 'PAYMENT_DECLINED' | 'VALIDATION_ERROR' | 'BAD_REQUEST' | 'EMAIL_COLLISION' | 'ELIGIBILITY_REJECTED' | 'REGISTRATION_FAILED';
|
|
131
|
+
message: string;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Error thrown by apiFetch for non-2xx responses.
|
|
135
|
+
* Carries the HTTP status and the parsed response body so callers can
|
|
136
|
+
* branch on structured error types (e.g. WalletApiError).
|
|
137
|
+
*/
|
|
10
138
|
export declare class ApiError extends Error {
|
|
11
139
|
readonly status: number;
|
|
12
140
|
readonly body: unknown;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ApplePayButton, APPLE_PAY_EXCLUDED_VARIANTS } from './apple-pay-button';
|
|
1
|
+
export { ApplePayButton, APPLE_PAY_EXCLUDED_VARIANTS, } from './apple-pay-button';
|
|
2
2
|
export { ApplePayModal } from './apple-pay-modal';
|
|
3
3
|
export { ensureApplePayLogoSymbol } from './apple-pay-logo';
|
|
4
4
|
export { configureApplePay } from './clients/payment-checkout/config';
|