@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.
Files changed (55) hide show
  1. package/README.md +186 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1642 -0
  4. package/dist/src/apple-pay-button.d.ts +45 -0
  5. package/dist/src/apple-pay-button.js +1621 -0
  6. package/dist/src/apple-pay-button.stories.js +1934 -0
  7. package/dist/src/apple-pay-logo.d.ts +1 -0
  8. package/dist/src/apple-pay-logo.js +14 -0
  9. package/dist/src/apple-pay-modal.d.ts +12 -0
  10. package/dist/src/apple-pay-modal.js +442 -0
  11. package/dist/src/apple-pay-modal.stories.js +1778 -0
  12. package/dist/src/clients/payment-checkout/apple-session.d.ts +18 -0
  13. package/dist/src/clients/payment-checkout/apple-session.js +243 -0
  14. package/dist/src/clients/payment-checkout/billing-contact-details.d.ts +21 -0
  15. package/dist/src/clients/payment-checkout/billing-contact-details.js +109 -0
  16. package/dist/src/clients/payment-checkout/config.d.ts +9 -0
  17. package/dist/src/clients/payment-checkout/config.js +23 -0
  18. package/dist/src/clients/payment-checkout/contact-validation.d.ts +21 -0
  19. package/dist/src/clients/payment-checkout/contact-validation.js +50 -0
  20. package/dist/src/clients/payment-checkout/detects.d.ts +3 -0
  21. package/dist/src/clients/payment-checkout/detects.js +56 -0
  22. package/dist/src/clients/payment-checkout/index.d.ts +5 -0
  23. package/dist/src/clients/payment-checkout/index.js +83 -0
  24. package/dist/src/clients/payment-checkout/messages.d.ts +33 -0
  25. package/dist/src/clients/payment-checkout/messages.js +19 -0
  26. package/dist/src/clients/payment-checkout/payment-handler.d.ts +36 -0
  27. package/dist/src/clients/payment-checkout/payment-handler.js +389 -0
  28. package/dist/src/clients/payment-checkout/session.d.ts +2 -0
  29. package/dist/src/clients/payment-checkout/session.js +67 -0
  30. package/dist/src/clients/payment-checkout/shipping-contact-details.d.ts +10 -0
  31. package/dist/src/clients/payment-checkout/shipping-contact-details.js +83 -0
  32. package/dist/src/clients/payment-checkout/types.d.ts +118 -0
  33. package/dist/src/clients/payment-checkout/types.js +14 -0
  34. package/dist/src/index.d.ts +7 -0
  35. package/dist/src/index.js +1642 -0
  36. package/dist/src/stories/fixtures/offer.d.ts +2 -0
  37. package/dist/src/stories/fixtures/offer.js +28 -0
  38. package/dist/src/stories/utils/story-env.d.ts +11 -0
  39. package/dist/src/stories/utils/story-env.js +148 -0
  40. package/dist/src/types/html-template.d.js +0 -0
  41. package/dist/src/types/offer.types.d.ts +73 -0
  42. package/dist/src/types/offer.types.js +0 -0
  43. package/dist/src/utils/billing-countries.d.ts +19 -0
  44. package/dist/src/utils/billing-countries.js +263 -0
  45. package/dist/src/utils/define.d.ts +1 -0
  46. package/dist/src/utils/define.js +9 -0
  47. package/dist/src/utils/expressCheckoutRedirect.d.ts +55 -0
  48. package/dist/src/utils/expressCheckoutRedirect.js +46 -0
  49. package/dist/src/utils/expressCheckoutTracking.d.ts +64 -0
  50. package/dist/src/utils/expressCheckoutTracking.js +84 -0
  51. package/dist/src/utils/pricing.d.ts +1 -0
  52. package/dist/src/utils/pricing.js +60 -0
  53. package/dist/src/utils/recaptcha.d.ts +22 -0
  54. package/dist/src/utils/recaptcha.js +54 -0
  55. package/package.json +64 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TEDL analytics events for Express Checkout.
3
+ *
4
+ * Pushes events onto window.tedl.events[] — the same mechanism as
5
+ * @economist/tedl's emitTedlEvent — without the package dependency.
6
+ * Avoids pulling a Node-targeted CJS bundle (crypto, sf-entitlement) into a
7
+ * browser-only Web Component library.
8
+ *
9
+ * Event shape matches sma-checkout's pushTedlEvent schema:
10
+ * { name: 'user.journey', type: 'checkout', action, info: 'applepay', ...extra }
11
+ *
12
+ * Event names are aligned to the PRD spec (section 10 — Tracking & Analytics).
13
+ */
14
+ declare global {
15
+ interface Window {
16
+ tedl?: {
17
+ events?: Record<string, unknown>[];
18
+ [key: string]: unknown;
19
+ };
20
+ }
21
+ }
22
+ export declare const EXPRESS_CHECKOUT_EVENTS: {
23
+ /** Wallet button displayed */
24
+ readonly IMPRESSION: "express_wallet_impression";
25
+ /** Wallet button clicked */
26
+ readonly BUTTON_CLICK: "express_wallet_click";
27
+ /** Apple Pay sheet opened */
28
+ readonly SHEET_OPEN: "wallet_payment_sheet_open";
29
+ /** Payment authorised by the user in the Apple Pay sheet */
30
+ readonly AUTHORISED: "wallet_payment_authorised";
31
+ /** Purchase completed — BFF returned success */
32
+ readonly SUCCESS: "wallet_payment_success";
33
+ /** Payment failed — BFF returned error */
34
+ readonly FAILURE: "wallet_payment_failure";
35
+ /** Redirect to standard checkout (any fallback path) */
36
+ readonly FALLBACK_CHECKOUT: "fallback_checkout";
37
+ };
38
+ /**
39
+ * Metadata required for the wallet impression event.
40
+ * Covers the 5 analytics dimensions from the PRD tracking spec:
41
+ * wallet_type, entry_point, offer_id (sku_id), country, device.
42
+ */
43
+ export type ImpressionMetadata = {
44
+ /** Entry point where the Apple Pay button appeared — e.g. 'article_paywall', 'homepage_banner'. */
45
+ entryPoint: string;
46
+ /** ISO 3166-1 alpha-2 country code of the offer (e.g. 'GB', 'US'). */
47
+ country: string;
48
+ };
49
+ /** Wallet button displayed — emits all 5 required analytics dimensions. */
50
+ export declare const trackExpressWalletImpression: (skuId: string, { entryPoint, country }: ImpressionMetadata) => void;
51
+ /** Wallet button clicked */
52
+ export declare const trackExpressWalletClick: (skuId: string) => void;
53
+ /** Apple Pay sheet opened (session.begin() called) */
54
+ export declare const trackWalletPaymentSheetOpen: (skuId: string) => void;
55
+ /** Payment authorised by the user in the Apple Pay sheet */
56
+ export declare const trackWalletPaymentAuthorised: (skuId: string) => void;
57
+ /** Purchase completed — BFF returned success */
58
+ export declare const trackWalletPaymentSuccess: (skuId: string, basketId: string) => void;
59
+ /** Payment failed — BFF returned an error */
60
+ export declare const trackWalletPaymentFailure: (skuId: string, reason: string) => void;
61
+ /** Redirect to standard checkout triggered */
62
+ export declare const trackFallbackCheckout: (skuId: string) => void;
63
+ /** Billing/shipping field failed validation (emitted per-field when Apple Pay sheet stays open) */
64
+ export declare const trackValidationFieldError: (field: string, errorType: string) => void;
@@ -0,0 +1,84 @@
1
+ // src/utils/expressCheckoutTracking.ts
2
+ var EXPRESS_CHECKOUT_EVENTS = {
3
+ /** Wallet button displayed */
4
+ IMPRESSION: "express_wallet_impression",
5
+ /** Wallet button clicked */
6
+ BUTTON_CLICK: "express_wallet_click",
7
+ /** Apple Pay sheet opened */
8
+ SHEET_OPEN: "wallet_payment_sheet_open",
9
+ /** Payment authorised by the user in the Apple Pay sheet */
10
+ AUTHORISED: "wallet_payment_authorised",
11
+ /** Purchase completed — BFF returned success */
12
+ SUCCESS: "wallet_payment_success",
13
+ /** Payment failed — BFF returned error */
14
+ FAILURE: "wallet_payment_failure",
15
+ /** Redirect to standard checkout (any fallback path) */
16
+ FALLBACK_CHECKOUT: "fallback_checkout"
17
+ };
18
+ var getDevice = () => {
19
+ const ua = navigator.userAgent;
20
+ if (/tablet|ipad/i.test(ua)) return "tablet";
21
+ if (/mobile|iphone|android/i.test(ua)) return "mobile";
22
+ return "desktop";
23
+ };
24
+ var emitTedlEvent = (event) => {
25
+ window.tedl = window.tedl ?? {};
26
+ window.tedl.events = window.tedl.events ?? [];
27
+ window.tedl.events.push(event);
28
+ };
29
+ var track = (action, extra) => {
30
+ emitTedlEvent({
31
+ name: "user.journey",
32
+ type: "checkout",
33
+ action,
34
+ info: "applepay",
35
+ ...extra
36
+ });
37
+ };
38
+ var trackExpressWalletImpression = (skuId, { entryPoint, country }) => {
39
+ track(EXPRESS_CHECKOUT_EVENTS.IMPRESSION, {
40
+ wallet_type: "apple_pay",
41
+ entry_point: entryPoint,
42
+ sku_id: skuId,
43
+ country,
44
+ device: getDevice()
45
+ });
46
+ };
47
+ var trackExpressWalletClick = (skuId) => {
48
+ track(EXPRESS_CHECKOUT_EVENTS.BUTTON_CLICK, { sku_id: skuId });
49
+ };
50
+ var trackWalletPaymentSheetOpen = (skuId) => {
51
+ track(EXPRESS_CHECKOUT_EVENTS.SHEET_OPEN, { sku_id: skuId });
52
+ };
53
+ var trackWalletPaymentAuthorised = (skuId) => {
54
+ track(EXPRESS_CHECKOUT_EVENTS.AUTHORISED, { sku_id: skuId });
55
+ };
56
+ var trackWalletPaymentSuccess = (skuId, basketId) => {
57
+ track(EXPRESS_CHECKOUT_EVENTS.SUCCESS, {
58
+ sku_id: skuId,
59
+ basket_id: basketId
60
+ });
61
+ };
62
+ var trackWalletPaymentFailure = (skuId, reason) => {
63
+ track(EXPRESS_CHECKOUT_EVENTS.FAILURE, {
64
+ sku_id: skuId,
65
+ failure_reason: reason
66
+ });
67
+ };
68
+ var trackFallbackCheckout = (skuId) => {
69
+ track(EXPRESS_CHECKOUT_EVENTS.FALLBACK_CHECKOUT, { sku_id: skuId });
70
+ };
71
+ var trackValidationFieldError = (field, errorType) => {
72
+ track(`validation_${field}_${errorType}`, {});
73
+ };
74
+ export {
75
+ EXPRESS_CHECKOUT_EVENTS,
76
+ trackExpressWalletClick,
77
+ trackExpressWalletImpression,
78
+ trackFallbackCheckout,
79
+ trackValidationFieldError,
80
+ trackWalletPaymentAuthorised,
81
+ trackWalletPaymentFailure,
82
+ trackWalletPaymentSheetOpen,
83
+ trackWalletPaymentSuccess
84
+ };
@@ -0,0 +1 @@
1
+ export declare function formatPrice(price: number | undefined, currencyCode: string, shouldFixDecimalPlaces?: boolean, roundUp?: boolean): string;
@@ -0,0 +1,60 @@
1
+ // src/utils/pricing.ts
2
+ var PRICE_CONSTANTS = {
3
+ CENTS_MULTIPLIER: 100,
4
+ DECIMAL_PLACES: 2,
5
+ NO_DECIMAL_PLACES: 0
6
+ };
7
+ function numberOfDecimalPoints(value) {
8
+ if (!value) {
9
+ return PRICE_CONSTANTS.NO_DECIMAL_PLACES;
10
+ }
11
+ return Number(value.toFixed(PRICE_CONSTANTS.DECIMAL_PLACES)) % 1 === 0 ? PRICE_CONSTANTS.NO_DECIMAL_PLACES : PRICE_CONSTANTS.DECIMAL_PLACES;
12
+ }
13
+ var priceFormatters = {
14
+ CAD: (price) => `C$${price}`,
15
+ SEK: (price) => `${price}Kr`,
16
+ AUD: (price) => `A$${price}`,
17
+ NZD: (price) => `NZ$${price}`,
18
+ HKD: (price) => `HK$${price}`,
19
+ DKK: (price) => `${price}Kr`,
20
+ NOK: (price) => `${price}Kr`,
21
+ CHF: (price) => `${price}Fr`,
22
+ SGD: (price) => `S$${price}`,
23
+ TWD: (price) => `TW$${price}`
24
+ };
25
+ function formatPrice(price, currencyCode, shouldFixDecimalPlaces = true, roundUp = false) {
26
+ if (price === void 0) {
27
+ return "";
28
+ }
29
+ const points = numberOfDecimalPoints(price);
30
+ const calculatedPrice = () => {
31
+ if (roundUp) {
32
+ return (Math.ceil(price * PRICE_CONSTANTS.CENTS_MULTIPLIER) / PRICE_CONSTANTS.CENTS_MULTIPLIER).toFixed(PRICE_CONSTANTS.DECIMAL_PLACES);
33
+ }
34
+ if (shouldFixDecimalPlaces) {
35
+ return (Math.round(price * PRICE_CONSTANTS.CENTS_MULTIPLIER) / PRICE_CONSTANTS.CENTS_MULTIPLIER).toFixed(PRICE_CONSTANTS.DECIMAL_PLACES);
36
+ }
37
+ return price;
38
+ };
39
+ const priceFixDecimal = calculatedPrice();
40
+ const priceFormatter = priceFormatters[currencyCode];
41
+ if (priceFormatter) {
42
+ const withCommas = Number(priceFixDecimal).toLocaleString("en", {
43
+ minimumFractionDigits: points
44
+ });
45
+ return priceFormatter(withCommas);
46
+ }
47
+ try {
48
+ return Number(priceFixDecimal).toLocaleString("en", {
49
+ style: "currency",
50
+ currency: currencyCode,
51
+ minimumFractionDigits: points,
52
+ currencyDisplay: "narrowSymbol"
53
+ });
54
+ } catch {
55
+ return `${currencyCode} ${priceFixDecimal}`;
56
+ }
57
+ }
58
+ export {
59
+ formatPrice
60
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * reCAPTCHA v3 token acquisition.
3
+ *
4
+ * Dynamically injects the Google reCAPTCHA script loader into the DOM
5
+ * because it is not loaded globally on article/paywall pages.
6
+ *
7
+ * The site key is provided at runtime via configureApplePay({ recaptchaSiteKey }).
8
+ */
9
+ /**
10
+ * Requests a reCAPTCHA v3 token for the given action.
11
+ *
12
+ * @param action - The reCAPTCHA action identifier (e.g. 'VerifyMerchant', 'CheckEligibility')
13
+ */
14
+ export declare const getRecaptchaToken: (action: string) => Promise<string>;
15
+ /**
16
+ * Acquires all three reCAPTCHA tokens needed by /v1/wallet/purchase in parallel.
17
+ */
18
+ export declare const getPurchaseRecaptchaTokens: () => Promise<{
19
+ checkEligibility: string;
20
+ newBasket: string;
21
+ registerUser: string;
22
+ }>;
@@ -0,0 +1,54 @@
1
+ // src/clients/payment-checkout/config.ts
2
+ var runtimeConfig = null;
3
+ var getApplePayConfig = () => runtimeConfig;
4
+
5
+ // src/utils/recaptcha.ts
6
+ var loadRecaptchaScript = (siteKey) => {
7
+ return new Promise((resolve, reject) => {
8
+ if (typeof grecaptcha !== "undefined") return resolve();
9
+ const existingScript = document.querySelector(
10
+ 'script[src*="recaptcha/api.js"]'
11
+ );
12
+ if (existingScript) {
13
+ existingScript.addEventListener("load", () => resolve());
14
+ existingScript.addEventListener(
15
+ "error",
16
+ () => reject(new Error("Failed to load existing reCAPTCHA script"))
17
+ );
18
+ return;
19
+ }
20
+ const script = document.createElement("script");
21
+ script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
22
+ script.async = true;
23
+ script.defer = true;
24
+ script.onload = () => resolve();
25
+ script.onerror = () => reject(new Error("Failed to load reCAPTCHA script"));
26
+ document.head.appendChild(script);
27
+ });
28
+ };
29
+ var getRecaptchaToken = async (action) => {
30
+ const siteKey = getApplePayConfig()?.recaptchaSiteKey;
31
+ if (!siteKey) {
32
+ throw new Error(
33
+ "[web-apple-pay] Missing recaptchaSiteKey. Call configureApplePay({ recaptchaSiteKey, ... }) before initiating Apple Pay."
34
+ );
35
+ }
36
+ await loadRecaptchaScript(siteKey);
37
+ return new Promise((resolve, reject) => {
38
+ grecaptcha.ready(() => {
39
+ grecaptcha.execute(siteKey, { action }).then(resolve).catch(reject);
40
+ });
41
+ });
42
+ };
43
+ var getPurchaseRecaptchaTokens = async () => {
44
+ const [checkEligibility, newBasket, registerUser] = await Promise.all([
45
+ getRecaptchaToken("checkEligibility"),
46
+ getRecaptchaToken("newBasket"),
47
+ getRecaptchaToken("registerUser")
48
+ ]);
49
+ return { checkEligibility, newBasket, registerUser };
50
+ };
51
+ export {
52
+ getPurchaseRecaptchaTokens,
53
+ getRecaptchaToken
54
+ };
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@economist/web-apple-pay",
3
+ "description": "Web component package for Apple Pay checkout UI",
4
+ "version": "0.1.1-beta.0",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "engines": {
19
+ "node": ">=20.x",
20
+ "npm": ">=10.x"
21
+ },
22
+ "scripts": {
23
+ "test": "jest",
24
+ "test:coverage": "npm run test -- --coverage",
25
+ "storybook": "storybook dev -p 6006",
26
+ "build-storybook": "storybook build",
27
+ "lint": "eslint .",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "tsc": "tsc --project tsconfig.json",
30
+ "build": "npm run clean && node esbuild.config.mjs && npm run tsc:emit-types",
31
+ "tsc:emit-types": "tsc --emitDeclarationOnly",
32
+ "clean": "rm -rf dist",
33
+ "semantic-release": "semantic-release"
34
+ },
35
+ "devDependencies": {
36
+ "@eslint/js": "^9.25.1",
37
+ "@storybook/addon-essentials": "^8.6.18",
38
+ "@storybook/addon-interactions": "^8.6.18",
39
+ "@storybook/test": "^8.6.18",
40
+ "@storybook/web-components-vite": "^8.6.18",
41
+ "@types/jest": "29.5.3",
42
+ "@types/applepayjs": "^14.0.9",
43
+ "esbuild": "^0.25.3",
44
+ "eslint": "^9.25.1",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "jest": "^29.7.0",
47
+ "jest-environment-jsdom": "^29.7.0",
48
+ "prettier": "3.0.1",
49
+ "storybook": "^8.6.18",
50
+ "ts-jest": "29.2.5",
51
+ "ts-node": "10.9.1",
52
+ "typescript": "^5.8.2",
53
+ "typescript-eslint": "^8.31.0"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "git+https://github.com/EconomistDigitalSolutions/shared-packages.git",
58
+ "directory": "packages/web-apple-pay"
59
+ },
60
+ "homepage": "https://github.com/EconomistDigitalSolutions/shared-packages#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/EconomistDigitalSolutions/shared-packages/issues"
63
+ }
64
+ }