@economist/web-apple-pay
Framework-agnostic Apple Pay UI components implemented as Web Components.
This package provides:
teg-apple-pay-buttonteg-apple-pay-modal
It is intentionally UI-focused. Consumer applications own backend payment orchestration, merchant validation, and secret handling.
Requirements
- Node.js
20.xor later - npm
10.xor later
Installation
npm install --save @economist/web-apple-pay
Quick start
1) Configure runtime values
Call configureApplePay(...) once during app startup, before rendering Apple Pay UI:
import {
configureApplePay,
defineApplePayElements,
} from '@economist/web-apple-pay';
configureApplePay({
merchantId: 'merchant.com.economist.your-app',
debugBypassEnabled: false,
// required: wallet API base URL for session and checkout calls
walletApiBaseUrl: 'https://bff-test.economist.com',
});
defineApplePayElements();
2) Create and mount a button
import type { Offer, ApplePayButton } from '@economist/web-apple-pay';
const button = document.createElement('teg-apple-pay-button') as ApplePayButton;
button.offer = offer as Offer;
button.country = 'US';
button.dataset.loggedIn = 'true';
container.appendChild(button);
When clicked, the button creates and opens teg-apple-pay-modal automatically.
Runtime configuration
configureApplePay(config)
Sets package-level runtime configuration used by Apple Pay availability checks. Call this once during app startup.
config shape:
merchantId(string, required): Apple Pay merchant identifier used for active-card checks.debugBypassEnabled(boolean, optional): allows?BYPASS_APPLE_PAY_CHECKS=1query param to bypass capability checks in non-production-style debugging flows.walletApiBaseUrl(string, required): base URL for wallet API requests.
If merchantId is not configured, Apple Pay availability checks return false and the button hides itself.
Component behavior
ApplePayButton
- Requires
offerandcountrybefore click flow is meaningful. - Checks Apple Pay availability on connect; if unavailable, it hides.
- Skips rendering for excluded variants (
bundle,insider_print). Every otherProductVariant(includingplay) is eligible; billing countries are restricted to the offer's price zone. - Forwards
data-logged-infrom button to modal (modal.dataset.loggedIn).
Awaiting render completion — whenReady()
Use whenReady() to await the component's render lifecycle after appending it
to the DOM. This is the supported, stable way to know whether Apple Pay is
available without touching lifecycle internals.
container.appendChild(btn); // spec-compliant — browser fires connectedCallback naturally
await btn.whenReady(); // resolves when rendering is complete
if (btn.style.display !== 'none') {
// Apple Pay rendered successfully
} else {
btn.remove(); // unavailable or suppressed
}
Important: calling
whenReady()beforeappendChildresolves immediately toundefined— it does not wait for a future connection. Always call it after appending the element.
ApplePayModal
- Opened by
ApplePayButtonclick. - Emits
apple-pay-confirmedwhen CTA is accepted. Escapecloses the modal and prevents propagation to other global handlers.
Events
apple-pay-confirmed
Dispatched from ApplePayModal as a CustomEvent with:
detail.skuId(string)detail.country(string)
Consumers should listen for this event and continue payment orchestration.
Public API
defineApplePayElements(): registers both custom elements.configureApplePay(config): sets required runtime config (merchant ID and wallet API base URL, with optional debug bypass).ApplePayButton: button component class.whenReady(): Promise<void>— resolves when the current render lifecycle completes (available or hidden). Resolves immediately if called before the element is connected.createIfEnabled(offer, country, entryPoint?): returns a configured button ornull.DUAL_OFFER_BANNER(or omitted): always-on after offer exclusions — no homepage/Safari gate atcreateIfEnabled(device gating viaisApplePayAvailableon mount).FREE_TRIAL_BANNER: same always-on-after-exclusions gate as Dual; wallet impressionentry_pointisfree_trial_banner.FREE_TRIAL_POPUP: same always-on-after-exclusions gate. Wallet impressionentry_pointisfree_trial_popup. On payment success for a logged-in reader,/checkout/completeis opened withstatus=nspc. An anonymous popup staysnex. Other entry points staylexornex. The origin URL is still written tocheckout-redirectfor sma-checkout.SUBSCRIBE: same always-on-after-exclusions gate for/subscribe(sma-checkout); no Amplitude. Wallet impressionentry_pointissubscribe.SALES_PAGE: same always-on-after-exclusions gate for sma-checkout sales pages (Black Friday and other promotions); no Amplitude. Host-managed return URL likeSUBSCRIBE; print offers excluded. Wallet impressionentry_pointissales_page.PAYWALL: always-on after offer exclusions + Safari/Apple placement eligibility. No Amplitude experiment /$exposure.REGWALL: inert — always returnsnull.
- Analytics. The entry point is sent as
entry_pointon every TEDL funnel event:express_wallet_impression,express_wallet_click,wallet_payment_sheet_open,wallet_payment_authorised,wallet_payment_success,wallet_payment_failureandfallback_checkout. It is left out when the host does not set one. The per-fieldvalidation_*events do not carry it. - Print offers (
bundle,insider_print) — behind a flag, off by default.PRINT_OFFERS_ENABLED(compiled into the release, shipsfalse):falseexcludes print on every entry point;trueenables the feature. Even then, only entry points withallowsPrintOffers: trueinENTRY_POINT_CONFIG(src/apple-pay-button.ts) take print — currently onlySUBSCRIBE; banners, popup, paywall and regwall never do. Hosts that updatebutton.offerin place should usegetExcludedVariants(entryPoint)(it honours the flag) instead of the deprecatedAPPLE_PAY_EXCLUDED_VARIANTS. For an allowed print offer the sheet additionally collects the delivery address, name and phone and:- limits delivery to the offer's price zone minus the countries print cannot be delivered to (
src/data/no-delivery-countries.json); an offer with no deliverable country hides the button; - validates the address with the sma-checkout delivery-form rules (required fields by zone, Commerce Services length limits, US zip / Canadian postcode formats, phone required for IN and CN) and keeps the sheet open with field-level errors (
delivery-contact-details.ts, rules inutils/address-rules.ts); - sends
supportedDeliveryCountriesand the shipping contact to/v1/wallet/purchase(with the phone number as normalised by the validator, e.g.098765 43210for IN becomes+919876543210; one that cannot be normalised is left out), which re-validates and forwards the delivery address to Commerce Services. Release order: deploy the wallet-payments-api change before releasing with the flagtrue; - reports billing and delivery field errors together, so the sheet shows every bad field at once.
- Names and addresses must be Latin characters plus the approved diacritics for every purchase (SMA-3066), the same as sma-checkout; non-Latin input shows "Please use Latin characters only." in the sheet.
- limits delivery to the offer's price zone minus the countries print cannot be delivered to (
- Return after purchase. Banners, popup, paywall and regwall return the user to the page the button is on.
SUBSCRIBEandSALES_PAGEleave the return target to the host (sma-checkout keeps the article the user arrived from).
ApplePayModal: modal component class.Offer: offer contract type used by component props.
Troubleshooting
Button does not appear
Check:
configureApplePay(...)was called before rendering (with a validmerchantIdandwalletApiBaseUrl)- device/browser supports Apple Pay (Safari on Apple device required — enforced by
isApplePayAvailableon mount) - for paywalls: Safari/Apple placement eligibility (or
?BYPASS_APPLE_PAY_CHECKS=1for QA) - for banners /
FREE_TRIAL_POPUP: consumer kill switch off (no homepage/Safari gate atcreateIfEnabled) - offer variant is not excluded (
bundle,insider_print; print is excluded everywhere whilePRINT_OFFERS_ENABLEDisfalse)
Logged-in checkbox copy looks wrong
Set button.dataset.loggedIn = 'true' before appending the button. The button forwards this flag to the modal automatically.
Security and responsibilities
Merchant setup must be handled by the consuming application:
- inject merchant config at runtime
- perform merchant validation on trusted backend services
- keep certificates/secrets outside this package
Local development
From shared-packages:
cd packages/web-apple-pay
npm run build
yalc publish --sig
Then in a consuming app:
yalc add @economist/web-apple-pay
Storybook
Run Storybook locally:
npm run storybook --workspace @economist/web-apple-pay
Build static Storybook output:
npm run build-storybook --workspace @economist/web-apple-pay
Story coverage matrix:
ApplePayButtondocs page shows the stable default example.ApplePayButtonbehavior and state variants are available as individual canvas stories in Storybook.ApplePayModaldocs page shows the stable default example.ApplePayModalbehavior and state variants are available as individual canvas stories in Storybook.
Print offers ship disabled (PRINT_OFFERS_ENABLED is false); see the Public API section above.