# @economist/web-apple-pay

Framework-agnostic Apple Pay UI components implemented as Web Components.

This package provides:

- `teg-apple-pay-button`
- `teg-apple-pay-modal`

It is intentionally UI-focused. Consumer applications own backend payment orchestration, merchant validation, and secret handling.

## Requirements

- Node.js `20.x` or later
- npm `10.x` or later

## Installation

```sh
npm install --save @economist/web-apple-pay
```

## Quick start

### 1) Configure runtime values

Call `configureApplePay(...)` once during app startup, before rendering Apple Pay UI:

```ts
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

```ts
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=1` query 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 `offer` and `country` before click flow is meaningful.
- Checks Apple Pay availability on connect; if unavailable, it hides.
- Skips rendering for excluded variants (`bundle`, `insider_print`). Every other `ProductVariant` (including `play`) is eligible; billing countries are restricted to the offer's price zone.
- Forwards `data-logged-in` from button to modal (`modal.dataset.loggedIn`).

#### Native Apple Pay button

**Feature flag:** it is on for `localhost`, `dev.*` and `stage.*`, and **off in production** until
`NATIVE_BUTTON_ENABLED_IN_PRODUCTION` (`src/utils/nativeButtonFlag.ts`) is flipped to `true` in a release. It fails
closed: any other host (www.economist.com, previews, IPs) keeps the custom button. A host can force it either way
with `configureApplePay({ ..., nativeButton: true | false })`. The flag sets a `data-native-button` attribute on
`teg-apple-pay-button`, and the native CSS only applies when that attribute is present.

Once loading finishes, the button uses Apple's **native CSS button** (`-webkit-appearance: -apple-pay-button`)
wherever the browser supports it (Safari / iOS). Safari draws Apple's own artwork, so it follows Apple's design
guidelines, and because it is built into WebKit there is **no SDK script and no extra request** (safe for walls and
banners). Other browsers, and the loading spinner state, use the package's custom button as the fallback.

- Apple controls colour and wording: `--apple-pay-background-color`, `--apple-pay-foreground-color` and the
  "Subscribe with" caption/logo do not apply to the native button.
- Optional overrides: `--apple-pay-button-type` (`plain`, `buy`, `subscribe`, `check-out`, ...; default `subscribe`)
  and `--apple-pay-button-style` (`black`, `white`, `white-outline`; default `black`).
- Size and radius still follow `--apple-pay-height` and `--apple-pay-border-radius`.
- jsdom cannot render this, so tests assert the template contract. Verify visually in Safari.

#### 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.

```ts
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()` _before_ `appendChild` resolves
> immediately to `undefined` — it does not wait for a future connection.
> Always call it after appending the element.

### `ApplePayModal`

- Opened by `ApplePayButton` click.
- Emits `apple-pay-confirmed` when CTA is accepted.
- `Escape` closes 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 or `null`.
    - `DUAL_OFFER_BANNER` (or omitted): always-on after offer exclusions — no homepage/Safari gate at `createIfEnabled` (device gating via `isApplePayAvailable` on mount).
    - `FREE_TRIAL_BANNER`: same always-on-after-exclusions gate as Dual; wallet impression `entry_point` is `free_trial_banner`.
    - `FREE_TRIAL_POPUP`: same always-on-after-exclusions gate. Wallet impression `entry_point` is `free_trial_popup`. On payment success for a logged-in reader, `/checkout/complete` is opened with `status=nspc`. An anonymous popup stays `nex`. Other entry points stay `lex` or `nex`. The origin URL is still written to `checkout-redirect` for sma-checkout.
    - `SUBSCRIBE`: same always-on-after-exclusions gate for `/subscribe` (sma-checkout); no Amplitude. Wallet impression `entry_point` is `subscribe`.
    - `PAYWALL`: always-on after offer exclusions + Safari/Apple placement eligibility. No Amplitude experiment / `$exposure`.
    - `REGWALL`: inert — always returns `null`.
  - **Where the user returns after purchase.** Each entry point sets a `ReturnUrlPolicy` in `ENTRY_POINT_CONFIG` (`src/apple-pay-button.ts`), which decides what is written to `localStorage['checkout-redirect']` (read by the onboarding app):
    - `CURRENT_PAGE`: the page the button is on (banners, popup, paywall, regwall), e.g. back to the article.
    - `HOST_MANAGED`: nothing is written, so the host's own value is kept (`SUBSCRIBE`: sma-checkout stores the article the user arrived from; with none stored, onboarding returns them to the home page).
    - A new entry point must declare its policy, or it will not compile.
- `ApplePayModal`: modal component class.
- `ensureApplePayLogoSymbol()`: logo symbol injection helper.
- `Offer`: offer contract type used by component props.

### Breaking changes

- `APPLE_PAY_EXPRESS_PAYWALLS_EXPERIMENT_KEY`, `APPLE_PAY_EXPRESS_VARIANTS`, and `ApplePayExperimentFlagKey` are no longer exported. Paywalls are always-on after Safari/offer checks; drop those imports.
- `APPLE_PAY_EXPRESS_EXPERIMENT_KEY` (retired banners experiment flag) remains unpublished.

## Troubleshooting

### Button does not appear

Check:

- `configureApplePay(...)` was called before rendering (with a valid `merchantId` and `walletApiBaseUrl`)
- device/browser supports Apple Pay (Safari on Apple device required — enforced by `isApplePayAvailable` on mount)
- for paywalls: Safari/Apple placement eligibility (or `?BYPASS_APPLE_PAY_CHECKS=1` for QA)
- for banners / `FREE_TRIAL_POPUP`: consumer kill switch off (no homepage/Safari gate at `createIfEnabled`)
- offer variant is not excluded (`bundle`, `insider_print`)

### 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`:

```sh
cd packages/web-apple-pay
npm run build
yalc publish --sig
```

Then in a consuming app:

```sh
yalc add @economist/web-apple-pay
```

## Storybook

Run Storybook locally:

```sh
npm run storybook --workspace @economist/web-apple-pay
```

Build static Storybook output:

```sh
npm run build-storybook --workspace @economist/web-apple-pay
```

Story coverage matrix:

- `ApplePayButton` docs page shows the stable default example.
- `ApplePayButton` behavior and state variants are available as individual canvas stories in Storybook.
- `ApplePayModal` docs page shows the stable default example.
- `ApplePayModal` behavior and state variants are available as individual canvas stories in Storybook.
