A minimal Stripe Elements checkout that renders Apple Pay in Chrome as well as Safari, plus a diagnostics panel that reports which requirements are met.
Express Checkout Element (wallet buttons) and Payment Element (card form) share one
elements instance — the standard Elements shape. No Stripe-hosted Checkout.
npm install
cp .env.example .env # add your Stripe test keysApple Pay requires public HTTPS, so localhost will not show the button. You need
a tunnel and a registered domain:
npm start # terminal 1
ngrok http 4242 # terminal 2Then register the ngrok host as a payment method domain, either way:
npm run register-domain -- <your-host>.ngrok-free.app…or in the Dashboard, on the Payment method domains screen:
https://dashboard.stripe.com/ACCOUNT_ID/test/settings/payment_method_domains?enabled=true
Swap ACCOUNT_ID for your acct_… id; drop /test/ for live mode. Click Add a new
domain, paste the ngrok host (no https://, no trailing slash), and save.
Open the ngrok https URL. The diagnostics panel at the bottom of the page reports every requirement and what Stripe actually offered the browser.
npm run check-account # is Apple Pay enabled on the account?The ngrok hostname changes every restart, so re-run
register-domaineach session.
Four conditions must all hold. Each one fails silently — nothing thrown, nothing logged, the button simply isn't there.
Cheapest to verify, so start here — one command:
$ npm run check-account
-- Default (pmc_…) [DEFAULT] active=true
apple_pay : available=false preference=off <- not enabled
google_pay : available=false preference=off
link : available=true preference=on
card : available=true preference=on
Enable in Dashboard → Settings → Payment methods. Once on, apple_pay reads
available=true preference=on.
This is account-wide, so when it's off no browser shows the button — Safari included, even on hardware with a verified card in Wallet. That was the case while building this demo, which is why it's listed first.
From Stripe's supported-browsers table:
Apple Pay on non-Safari desktop browsers is only supported when
paymentMethods.applePayis set toalways.
Stripe's default renders Apple Pay only where it can confirm the customer has a card provisioned, which it can only do in Safari. On desktop Chrome, Edge, Firefox and Opera it renders nothing.
const expressCheckoutElement = elements.create('expressCheckout', {
paymentMethods: {
applePay: 'always', // without this, Chrome shows no Apple Pay button
},
});A/B it live. The toggle at the top of the payment card switches this flag and
reloads — the Apple Pay button appears and disappears with nothing else changing.
Also settable via ?applePay=auto|always|never. Measured on Chrome 151 / macOS:
| Mode | Card in the Mac's Wallet | Result |
|---|---|---|
'auto' (Stripe default) |
present & verified | applePay: false |
'always' |
present & verified | applePay: true |
In Chrome the wallet card is irrelevant — under auto, Stripe declines to offer
Apple Pay on non-Safari desktop regardless of what's provisioned. Per Stripe's testing
docs, 'always' also "remove[s] the requirement for an active saved card", so a
customer with an empty wallet still gets the button and completes via the iPhone
handshake.
Tradeoff: 'always' shows the button to customers with no card saved. On desktop
they complete by scanning a QR code with their iPhone (Apple opened this up in iOS 18 /
Feb 2025) — a real flow, but longer than Safari tap-to-pay, and a dead end for anyone
without an Apple device. Worth A/B testing rather than assuming it's a pure win.
Every domain displaying Apple Pay must be registered, in both test and live mode,
including subdomains — example.com, www.example.com and shop.example.com are
three separate registrations. Registering in live mode also registers it in sandboxes.
Registration also gates Link, PayPal and Amazon Pay, so when a domain is unregistered those disappear alongside Apple Pay.
See Run for both registration routes. The diagnostics panel checks the current host against the account's registered domains and names the ones it found, so a typo or a missing subdomain is obvious.
ApplePaySession is Safari's native Apple Pay API. It does not exist in Chrome —
Chrome reaches Apple Pay through the Payment Request API. This hides the button in
Chrome 100% of the time:
if (window.ApplePaySession) { showApplePayButton(); } // breaks ChromeUse the Element's availablepaymentmethodschange event instead.
Ask the Element what it decided, rather than inferring from the UI:
expressCheckoutElement.on('availablepaymentmethodschange', ({ paymentMethods }) => {
// { applePay: {available: true}, googlePay: {available: false}, … }
console.log(paymentMethods);
});If applePay.available is false, something upstream of your rendering code is unmet —
account config, domain, HTTPS, browser, or wallet state. If it's true and the button
still isn't visible, it's CSS or a conditional in your own component.
Comparing Safari against Chrome on the same page also localises it quickly: a requirement that's account-wide or domain-wide takes both browsers down, while requirements 2 and 4 are specific to non-Safari browsers.
Watch the shape. paymentMethods is a truthy object even when every wallet in it
is unavailable, so this is a bug that leaves an empty wallet row on the page:
if (paymentMethods) { showWalletRow(); } // truthy even when all are falseCount the available ones instead — see availableWallets() in
public/checkout.js.
- You can't save Stripe test cards to Apple Pay. In test mode Stripe recognises the test key and returns a successful test token, so you use a real card from your wallet and it is never charged.
- Test in both Safari and Chrome — they take different paths to Apple Pay, so one passing doesn't imply the other.
- Apple Pay and Google Pay don't share the customer's email unless you set
emailRequired: trueon the Element.
| File | Purpose |
|---|---|
public/checkout.js |
The whole integration. The Chrome fix is commented in place. |
public/index.html |
Payment page, always/auto toggle, diagnostics panel. |
server.js |
/api/create-intent and a /api/domain-status check. |
scripts/check-account.js |
npm run check-account — is Apple Pay enabled? |
scripts/register-domain.js |
npm run register-domain — registers a domain for Apple Pay. |