Wallets & Click to Pay
Wallets & Click to Pay
This page covers the three “express” payment surfaces MVS Pay renders inside the
embedded checkout: Apple Pay, Google Pay, and Click to Pay V2. All
three ride on the same machinery — Hyperswitch’s HyperLoader.js Web SDK,
loaded by @mvs/mvs-pay-ui, drawing the wallet/SRC tiles inside its
<PaymentElement />. MVS Pay itself ships almost no wallet-specific code: the
only first-party runtime surface is the Apple Pay domain-association route. The
rest is configuration (certs, merchant IDs, connectors) that lives in Apple’s
portal, Google’s console, Hyperswitch, and Infisical.
Native-Hyperswitch bias. Per ADR-007, wallets and Click to Pay are deliberately not re-implemented in MVS code. Apple Pay tokens are decrypted inside Hyperswitch’s key vault; Google Pay and Click to Pay tiles are rendered by HyperLoader. MVS Pay’s job is to serve one verification file and forward one env var. Everything else is upstream.
Nothing here is live yet. Per-org Apple Pay merchant certs are listed in
docs/HANDOFF.md §6d under “Live infrastructure provisioning (NONE done —
all code/manifests only)”. The code paths below exist and are tested at the
unit level, but no production org has been provisioned, and the Click to Pay
E2E (tests/e2e/click-to-pay.spec.ts) is wired but not run in CI (it
skips silently without CTP_VISA_TEST_API_KEY). Treat this page as the
contract for when provisioning begins, not a description of running prod.
How the three surfaces fit together
All three are surfaced by the same embedded widget. The merchant’s server mints
a clientSecret (Apple Pay / Google Pay) or, for the saved-method widget, an
sdkAuthorization token; the browser hands it to HyperLoader; HyperLoader draws
whichever tiles the active business profile’s connectors support.
The key consequence of this design: adding a wallet or Click to Pay almost never touches the SDK or the UI. You configure a connector and/or paste a secret, and the tile appears. The one exception is Apple Pay’s domain association, which requires MVS Pay to serve a file — covered below.
The HyperLoader bridge
@mvs/mvs-pay-ui loads Hyperswitch’s Web SDK and mounts its payment element.
The relevant files:
loadHyperLoader injects a single shared <script id="mvs-pay-hyperloader">
tag, defaulting to https://beta.hyperswitch.io/v1/HyperLoader.js
(DEFAULT_SCRIPT_SRC). It is idempotent — concurrent callers share one promise,
and a rejected load nulls the cached promise so a transient script failure does
not permanently break every widget on the page.
The default script source is the upstream beta. host. For a fully
self-hosted bundle, pass scriptSrc to <MvsPayCheckout /> (forwarded to
loadHyperLoader). This is the only place wallet rendering depends on an
Anthropic-external CDN; flagged here because it matters for CSP and for the
air-gapped story. See connect-src/script-src allowances in
packages/shared/lib/security-headers.ts (mvsPay profile).
Google Pay: the one env var
Google Pay is the simplest of the three — no certificates. The merchant ID
alone is enough. MvsPayCheckout accepts a googlePayMerchantId prop and
composes it into the HyperLoader payment element’s wallets block:
The caller reads process.env.NEXT_PUBLIC_GOOGLE_PAY_MERCHANT_ID and passes it
in. Without the merchant ID, Google Pay still renders in test mode, but
production processing requires the value (documented in the prop’s JSDoc and in
apps/mvs-pay/.env.example).
Apple Pay needs no prop on MvsPayCheckout. Apple Pay is gated entirely on
(a) the certs being uploaded to the card MCA in Hyperswitch and (b) the
domain being verified in Apple’s portal. There is no Apple Pay equivalent of
googlePayMerchantId in the widget API — the browser’s Apple Pay capability
plus the verified domain is what HyperLoader keys on.
Apple Pay — provisioning model
Apple Pay is configured per Apple Developer Merchant ID, not per Hyperswitch
merchant. One Merchant ID (merchant.com.mvscloud.<org_slug>) covers all of an
MVS org’s profiles. Two distinct certificates are involved, and they do
different jobs:
The critical security property: the Payment Processing Certificate’s CSR is
generated inside Hyperswitch’s Control Center, so the matching private key
never leaves Hyperswitch’s key vault. The operator never holds the key that
decrypts payment tokens. The full step-by-step (CSRs, openssl invocation,
Apple portal navigation, the “processed exclusively in China?” → No prompt)
lives in the Wallet Validation runbook.
Each merchant org owns its own Apple Developer Program account — MVS does not own it. Apple’s contract requires the merchant to provision the Merchant ID and Merchant Identity Certificate and share the credentials with MVS out-of-band (1Password / Infisical secure note). This is a per-org dependency that gates Apple Pay go-live and is one reason no org is provisioned yet.
Certs are uploaded to Hyperswitch against the existing card MCA (Stripe,
Cybersource, Adyen, …) — Apple Pay is not a separate connector, it rides on
the card processor’s merchant_connector_id:
The response returns a domain-association token per domain in
domain_names. That token is what MVS Pay has to serve back to Apple’s
verifier.
The Apple Pay domain-association route
This is the single piece of first-party wallet runtime in MVS Pay. Apple requires that, for every domain on which the Apple Pay button is rendered, the server respond at the canonical path:
Source: apps/mvs-pay/app/.well-known/apple-developer-merchantid-domain-association/route.ts.
It is a Node-runtime, force-dynamic route that resolves the token from an env
var and returns it as text/plain.
Token resolution
Two resolution paths:
- No
domainparam → use the environment-default key:APPLE_PAY_DOMAIN_ASSOCIATION_PRODin production (whenNODE_ENV,INFISICAL_ENV, orVERCEL_ENVindicate prod), elseAPPLE_PAY_DOMAIN_ASSOCIATION_DEV. ?domain=partner.example.com→ look up a per-domain override atAPPLE_PAY_DOMAIN_ASSOCIATION_<SLUG>. On a miss it falls through to the default so embed-host deployments keep working when a new partner is added to Hyperswitch but their override hasn’t been pasted into Infisical yet.
The slug function is exported and worth pinning down exactly:
Responses
The 404 is intentional: Apple’s verifier reads 404 as a clean “not
provisioned” signal rather than a half-broken integration, and no-store
prevents a stale negative from being cached. HEAD mirrors GET’s status and
headers with an empty body.
Because the route reads process.env directly and force-dynamic disables
Next.js caching, the token must be present in the runtime environment, not
baked at build time. MVS Pay pulls Infisical on every Vercel deploy, so
updating the token requires a redeploy (or it’s picked up on the next deploy)
— there is no app restart needed, but there is no hot-reload of secrets
either. The max-age=86400 on the 200 means Apple (and any CDN) may serve a
stale token for up to a day; purge the Vercel edge cache during initial setup
if you’re iterating.
Infisical paths
See Secrets Management and the Secrets Rotation runbook for how these flow into the deployment.
End-to-end: provisioning Apple Pay for an org
Create the Merchant ID (org admin)
In the org’s Apple Developer account, register
merchant.com.mvscloud.<org_slug> where <org_slug> is
payment_organization.org_slug.
Generate both certs
Merchant Identity Certificate via an operator-workstation CSR; Payment
Processing Certificate via a Hyperswitch-generated CSR (so its key stays
in the vault). Store the operator-held artefacts in Infisical under
/apps/mvs-pay/wallets/{org}/....
Upload certs to Hyperswitch
POST .../connectors/$MCA_ID/wallets/apple_pay/certs against the card
MCA. Capture the returned association_token per domain.
The full runbook — including the failure-mode triage (token mismatch, deploy lag, stale CDN) and the 25-month cert-rotation cadence — is the Wallet Validation runbook.
The www.mvspay.io re-verification requirement
The production checkout host is moving from pay.mvscloud.com to
www.mvspay.io (see the deployment topology and
Domain & DNS — www.mvspay.io; the cutover is
also called out in the Architecture overview
and the gap list on the Overview page).
Apple Pay domain association is bound to the exact host. A domain change is not transparent — it triggers a full re-provision on the Apple side:
Apple Pay must be re-verified for www.mvspay.io. The existing
pay.mvscloud.com verification does not carry over to a new registrable
domain. At cutover you must:
- Re-upload certs to Hyperswitch with
www.mvspay.ioadded todomain_names(this regenerates the association token for the new host). - Set the token in Infisical. With the current route logic, requests to
www.mvspay.iowith nodomainparam resolve to the defaultAPPLE_PAY_DOMAIN_ASSOCIATION_PRODkey — so simply updating_PRODto the new host’s token is the minimal change. (If you instead serve the old and new hosts in parallel during cutover, registerwww.mvspay.ioas a per-domain overrideAPPLE_PAY_DOMAIN_ASSOCIATION_WWW_MVSPAY_IOso both resolve correctly.) - Add
www.mvspay.iounder Merchant Domains in the Apple portal and click Verify again.
Until the new domain shows Verified in Apple’s portal, the Apple Pay
button will not render on www.mvspay.io.
Google Pay is also domain-scoped: the Google Pay & Wallet Console integration is
registered per domain, so www.mvspay.io needs to be added there too. The
merchant ID itself is reused (it does not change), so this is a console-side
domain addition rather than a re-issue.
Click to Pay V2 does not require domain re-verification — it is gated on the
connector being active on the profile, not on a .well-known file, so it
survives the host cutover unchanged.
Click to Pay V2
Click to Pay V2 implements the EMV Secure Remote Commerce (SRC) standard. Hyperswitch v1.123.1 ships it as two connectors:
V1 (VisaSRCI) is deprecated. Hyperswitch’s old VisaSRCI connector
predates Click to Pay V2 and is no longer maintained upstream. There are no
day-1 wizard entries for V1; operators still on it should add ctp_visa and
disable the legacy MCA (rollback flow in
Connector Configuration §9).
No new SDK component is needed
This is the central point: Click to Pay requires zero MVS code changes.
HyperLoader’s <PaymentElement /> — the same one mounted by <MvsPayCheckout />
— automatically surfaces a “Click to Pay” tile when a CTP connector is active on
the checkout’s business profile. There is no MvsPayClickToPay component, no
prop, no env var. You add the connector; the tile appears.
Provisioning via the connector wizard
Click to Pay is added through the same connector wizard as any other connector,
driven at /connectors/new. The full steps (creds pre-flight, forced
card / credit + debit payment methods, routing append, $0 auth smoke test, the
MerchantConnectorExtension sidecar row) are in the
Connector Onboarding runbook §3.
Routing interaction
Click to Pay does not bypass routing. The CTP MCA is just another
merchant_connector_id in the active routing rule:
- To pin CTP-only flows (e.g. for the E2E test), set
algorithm.type = "single"with the CTP connector. - To make CTP optional alongside a primary acquirer (the typical setup), keep the priority list with CTP as a fallback entry.
See Routing & Smart Retries for the routing model.
What’s actually tested
tests/e2e/click-to-pay.spec.ts — wired, not running in CI
The Click to Pay E2E (tests/e2e/click-to-pay.spec.ts, marked M27) drives
the connector wizard for ctp_visa, activates a single-connector routing
rule, mints a payment intent, renders <MvsPayCheckout /> via the
/embed/checkout harness, and asserts the CTP tile
(data-testid="payment-method-click_to_pay") is visible, then screenshots it.
It is wired but not executed in CI as of the M27 commit. Live execution requires all of:
- Hyperswitch sandbox up (
tests/e2e/docker-compose.hyperswitch.yml), CTP_VISA_TEST_API_KEY+CTP_VISA_TEST_PROFILE_IDin env,- mvs-pay UI + extensions running on
MVS_PAY_E2E_BASE_URL/MVS_PAY_E2E_EXTENSIONS_URL, - a Visa-issued sandbox identity for the consumer-side SRC iframe.
Without CTP_VISA_TEST_API_KEY the spec test.skips silently. So the
green CI signal does not mean Click to Pay is exercised end-to-end. There is
no equivalent E2E for Apple Pay or Google Pay (those require Safari/iOS and a
real wallet identity respectively — not in the harness). Treat all three
surfaces as validated by hand against a real low-value charge, per the
Wallet Validation runbook’s verification matrix.
Verification checklist
The full verification matrix (commands + pass criteria) is in the Wallet Validation runbook §10. The essentials:
Test wallets and CTP against a real payment intent (a ~10-cent charge),
not a $0 auth. Some PSPs gate wallet-sheet rendering on a minimum amount, so
a $0 intent can hide the very tile you’re trying to verify.
Honest gaps
- No org is provisioned. Per-org Apple Pay merchant certs are explicitly in
the “NONE done” bucket of
docs/HANDOFF.md§6d. The code is ready; the live Apple Developer accounts, certs, Google Pay integrations, and CTP credentials are not. - Wallet validation has no automated coverage. The only automated test is the Click to Pay E2E, and it skips without sandbox creds. Apple Pay / Google Pay validation is manual against a real device/browser.
- The HyperLoader CDN is external. Wallet/CTP rendering depends on
beta.hyperswitch.io/v1/HyperLoader.jsunlessscriptSrcis overridden. CSP must allow it (mvsPayprofile inpackages/shared/lib/security-headers.ts). - The www.mvspay.io cutover is pending and re-verification is manual. There is no automation that re-uploads certs / re-registers domains on the host change — it is a runbook step that must be done by hand at cutover, and Apple Pay is unavailable on the new host until it completes.
- Cert rotation is a calendar reminder. Apple Pay certs expire at 25 months; there is no automated expiry alerting wired for the per-org certs yet — the runbook prescribes a manual reminder at month 24.
Related pages
Step-by-step Apple Pay / Google Pay provisioning, cert generation, and the verification matrix.
§3 — adding ctp_visa / ctp_mastercard via the connector wizard.
<MvsPayCheckout />, the HyperLoader bindings, and the saved-cards widget.
Domain & DNS — www.mvspay.io
The host cutover that triggers Apple Pay / Google Pay re-verification.
How Click to Pay participates as just another connector in routing rules.
Where the domain-association tokens and Google Pay merchant ID live.