Checkout UI Components
@mvs/mvs-pay-ui
@mvs/mvs-pay-ui is the published React component library that wraps Hyperswitch’s
HyperLoader.js Web SDK with MVS theming. It ships the browser-side surface of MVS Pay:
embedded checkout, tokenize-only forms, hosted-link anchors, a saved-cards management widget,
and an MVS-specific instant-payout destination picker.
The package lives at packages/mvs-pay-ui/. Its public surface is re-exported from
packages/mvs-pay-ui/src/index.ts. It pairs with @mvs/mvs-pay-sdk on the
server, which mints the client_secret (and the sdk_authorization token) that each widget
needs before it can render.
This library is web-only by design. See ADR-009 · Web-only UI SDK. Hyperswitch ships native iOS, Android, React Native, and Flutter SDKs, but MVS Pay does not wrap them — that scope is deferred until a real mobile consumer (HealthOS mobile, an MVS native app, or a partner) materializes, at which point a separate plan ships the wrappers.
Package at a glance
The five exported components and their HyperLoader backing primitives:
Note that only three of the five components actually touch HyperLoader. <MvsPayCheckoutLink>
is a thin styled anchor, and <MvsPayInstantPayoutPicker> is an MVS-specific presentational
component over PayoutMethod rows.
How tokenization keeps the host page out of PCI scope
The whole point of mounting HyperLoader rather than building card fields yourself is PCI scope reduction. Raw PAN / CVV never enter the host page’s DOM, JS heap, or network requests.
Concretely:
- Server mints the secret. Your backend calls
mvsPay.payments.create(...)(orpayment-method-sessionsfor the saved-cards widget) with the secret API key. The browser only ever receives a scopedclient_secret/sdk_authorizationtoken. - The publishable key is public.
Hyper(publishableKey)takes the publishable key (NEXT_PUBLIC_HYPERSWITCH_PUBLISHABLE_KEY) — it is safe to ship to the browser and intentionally has no charge-authorizing power on its own. - Card fields render inside HyperLoader’s iframe. The host page only renders an empty
container
<div>; HyperLoader injects the sensitive fields into a cross-origin iframe it controls. Sensitive data is tokenized by Hyperswitch and never crosses back into the host page.
Because PAN/CVV stay inside HyperLoader’s iframe, an integrator that uses these components exclusively can typically self-assess against SAES-A rather than the heavier SAQ-D. This is the architectural reason MVS Pay does not ship raw card-field primitives.
The publishable key
All HyperLoader-backed components take a publishableKey: string prop. In the apps/mvs-pay
Next.js app this is sourced from:
The NEXT_PUBLIC_ prefix is what makes it available to client-side code. It is per-merchant,
so a multi-tenant host must resolve the right key for the active merchant before rendering. See
Environment Variables for the complete list.
Do not put the Hyperswitch secret (server) key behind a NEXT_PUBLIC_ prefix. Only the
publishable key is browser-safe. The secret key lives server-side in @mvs/mvs-pay-sdk and in
mvs-pay-extensions.
The HyperLoader runtime loader
All HyperLoader-backed components share a single loader, loadHyperLoader(), defined in
packages/mvs-pay-ui/src/hyper-loader.ts. It injects one <script> tag (id
mvs-pay-hyperloader) so that multiple widgets on the same page reuse the same SDK instance.
Key behaviors to know when debugging:
- Idempotent. Concurrent callers share one in-flight promise (
pending). Ifwindow.Hyperis already present, it resolves immediately. - Browser-only. Called server-side (no
window), it rejects withHyperLoader can only run in a browser environment. This is the failure you’ll see if a component is rendered during SSR without a client boundary. - Default script source:
https://beta.hyperswitch.io/v1/HyperLoader.js. Override per component via thescriptSrcprop (e.g. a self-hosted bundle). - Rejection is not cached. A transient script-load failure nulls out the cached promise so the next render retries, rather than permanently breaking every widget until a full reload.
- Late-listener guard. If the script tag already exists (HMR re-eval, StrictMode remount, a
duplicate bundled copy), the loader settles from current state instead of attaching a listener
that would never fire — it resolves if
window.Hyperis present, otherwise rejects.
Custom / self-hosted backend
Every HyperLoader-backed component accepts customBackendUrl?: string, forwarded as
Hyper(publishableKey, { customBackendUrl }). Use this when pointing the browser SDK at a
self-hosted Hyperswitch deployment rather than the default backend. See
ADR-001 · Self-host Hyperswitch.
<MvsPayCheckout>
Embedded one-time checkout. Mounts HyperLoader’s payment widget into a unique container
(derived from useId(), so multiple checkouts can coexist on one page) and cleans up on
unmount. Source: packages/mvs-pay-ui/src/components/MvsPayCheckout.tsx.
Obtain clientSecret server-side via mvsPay.payments.create(...) before rendering.
Props
Submit & wallet composition
The component does not render its own submit button — you wire one through renderSubmit,
which receives submit() and an isReady flag. submit() calls
hyper.confirmPayment({ widgets, confirmParams, redirect: "if_required" }):
- If the result has
error,onErrorfires withresult.error.message ?? "Payment failed". - Otherwise
onSuccessfires. The rawHyperConfirmResultis always returned to the caller. - Calling
submit()before the widget mounts throws"[mvs-pay-ui] checkout not ready"and firesonError.
The wallets block is composed in this order (last wins): walletReturnUrl from returnUrl,
then googlePay.merchantId from googlePayMerchantId, then any wallets on your
paymentElementOptions. So paymentElementOptions.wallets is the ultimate override. See
Wallets & Click to Pay.
Without googlePayMerchantId, Google Pay still works in test mode, but production
processing requires the merchant ID issued by the Google Pay & Wallet Console. Apple Pay
additionally needs domain association provisioning — see the
Wallet Validation runbook.
<MvsPayPaymentMethodForm>
Tokenize-only flow: save a payment method without immediately charging. Wraps HyperLoader’s
paymentMethods widget. Use it to save a card on a customer profile, capture a payout method,
or pre-authorize a card-on-file agreement. Source:
packages/mvs-pay-ui/src/components/MvsPayPaymentMethodForm.tsx.
Props
This component has no submit / confirm path and no onSuccess callback — it only mounts
the tokenization widget and exposes onReady/onError. The host is responsible for driving
the save/attach flow against the server (the widget tokenizes; persisting the result is your
backend’s job).
<MvsPaySavedCardsWidget>
Customer-facing payment-method management. Mounts HyperLoader’s paymentMethodsManagement
widget so a logged-in customer can list, select, and delete their saved methods (cards, wallets,
bank-account on file). Source: packages/mvs-pay-ui/src/components/MvsPaySavedCardsWidget.tsx.
Unlike checkout, this widget is initialised with an sdkAuthorization token (not a
per-payment clientSecret). The token is minted server-side via the extensions-service proxy
POST /v1/payment-method-sessions
(apps/mvs-pay-extensions/src/routes/payment-method-sessions.ts), which forwards to
Hyperswitch’s endpoint of the same name and scopes the session to a business profile via
x-profile-id. See Extensions Service and the
Connector Onboarding runbook (M27).
Props
This widget requires a newer HyperLoader. If hyper.paymentMethodsManagementElements is not a
function, the component throws and fires onError:
"HyperLoader does not expose paymentMethodsManagementElements; upgrade to v1.123.1+". The
per-element .on(...) event binding is likewise guarded — older HyperLoader builds without it
silently skip the onCardSelected / onCardDeleted callbacks. See the
Hyperswitch Upgrade runbook.
The event payloads from HyperLoader are loosely typed (unknown[]); the component defensively
extracts the id from any of paymentMethodId, payment_method_id, or id, or a bare string
argument.
<MvsPayCheckoutLink>
A thin styled anchor that opens a Hyperswitch hosted-checkout URL. Use it for a no-JS
share-link flow rather than embedding a widget. It does not load HyperLoader. Source:
packages/mvs-pay-ui/src/components/MvsPayCheckoutLink.tsx.
Props
Generating the hosted URL is a server concern — see Payment Links.
<MvsPayInstantPayoutPicker>
MVS-specific, not a HyperLoader widget. Renders a radiogroup of saved payout destinations
(PayoutMethod rows — bank accounts / debit cards) and emits the selected id via onSelect.
It is purely presentational: fetching the methods is the caller’s job (use
mvsPay.payoutMethods.list() or a server action that proxies it). Source:
packages/mvs-pay-ui/src/components/MvsPayInstantPayoutPicker.tsx. See
Payouts & Instant Payouts.
Props
Each MvsPayInstantPayoutPickerOption has id, optional type, brand, lastFour, name,
verified, disabled (plus arbitrary extras). The default row renders
brand/type ····lastFour, an optional name, and a Verified/Unverified badge. Rows use the ARIA
role="radio" pattern inside a role="radiogroup" (a native <input type="radio"> can’t host
the badge children).
Plaid Link integration (Phase 5 / M24, opt-in)
When the plaid prop is supplied, the “Link a bank account” / “Add another account” buttons
drive react-plaid-link’s usePlaidLink hook instead of calling onAddNewAccount:
MvsPayInstantPayoutPickerPlaidConfig:
Both fetches use credentials: "include". The button label reflects state
(“Preparing Plaid…”, “Linking…”). The link token is fetched on demand (not on mount); the
usePlaidLink hook only opens once a token loads. End-to-end provisioning is documented in the
Plaid Link runbook.
Usage example: embedded checkout
A minimal Next.js (App Router) integration. The server mints the secret; the client renders the widget.
All HyperLoader-backed components must render in a client boundary ("use client" in
Next.js). loadHyperLoader() rejects with “HyperLoader can only run in a browser environment”
if invoked during SSR.
Type exports
packages/mvs-pay-ui/src/index.ts also re-exports the HyperLoader type bindings and the loader
itself, so you can type custom escape-hatch options or call HyperLoader directly:
These are hand-written bindings over HyperLoader (packages/mvs-pay-ui/src/hyper-loader.ts),
not generated from an upstream package — most option bags carry an open [k: string]: unknown
index so you can pass through fields the bindings don’t enumerate.
Where to go next
Server-side @mvs/mvs-pay-sdk that mints the client_secret these components consume.
Hosts POST /v1/payment-method-sessions, the proxy that mints sdkAuthorization.
How a PaymentIntent flows from create → confirm → settle.
Why there are no mobile wrappers (yet).
For request/response shapes of the endpoints that back these widgets, see the API Reference tab.