@movmo_app/payments
v0.3.0
Published
Movmo Spreedly Hosted Fields shared package: PCI-safe card capture for Movmo UIs and airline partner integrations.
Downloads
543
Maintainers
Readme
@movmo_app/payments
PCI-safe card capture for Movmo UIs (accounts-ui, flights-ui) and airline partner embeds. The package is intentionally vault-neutral on its public surface — the underlying PCI vault (currently Spreedly Hosted Fields) is an implementation detail that consumers do not depend on.
What's in the box
<MovmoCardForm />— drop-in card-capture form (PCI-safe number + CVV iframes, single cardholder-name input, combinedMM / YYexpiry). The Save button is gated on Spreedly field validity; opt into auto-save via theautoSaveprop. Tokenizes the card, then calls the Movmo API to save the vaulted card to the user's profile. Renders the detected card brand inline (Visa / MC / Amex / Discover) as the user types.<PaymentMethodsManager />— full saved-cards UI: lists existing cards, lets the user set default / remove, and embeds<MovmoCardForm />for adding a new one. Renders a payment-type selector (Credit card + disabled "Coming soon" rows for PayPal / GooglePay / Klarna / ACH) by default; passpaymentTypeSelector={false}to hide it. Auto-saves the first card by default (autoSaveFirstCard={true}— matches flights-ui UX). Optional selection mode (selectedId+onSelect) for checkout flows.useMovmoCardFields()— lower-level hook for partners composing their own form layout. Exposes per-fieldvalidity+ detectedbrandso the consumer can gate its own submit button and render its own brand icon.useUserPaymentMethods(userId)— fetches the saved-cards list with{ items, status, error, refetch }. Useful standalone (e.g., showing the selected card's last4 in a checkout summary).useDeletePaymentMethod(userId)— exposes{ deletePaymentMethod, status, error }. Resolves on success, rejects on failure so callers can roll back optimistic updates.useSetDefaultPaymentMethod(userId)— exposes{ setDefault, status, error }. Same shape as delete.PaymentMethodSummary— vault-neutral type returned to consumers (no vault tokens, no customer/vault IDs leak through).
Backend contract
Consumes existing monolith-api endpoints (no backend changes required):
| Method | Path | Purpose |
| -------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| GET | /v1/payments/tokenization-session | Fetch RSA-signed Spreedly session params (environmentKey, certificateToken, nonce, timestamp, signature). |
| GET | /v1/users/:userid/payment-methods | List the user's saved cards. |
| POST | /v1/users/:userid/payment-methods/from-token | Save the vaulted card; server derives last4/brand/expMonth/expYear from the Spreedly vault, never trusting the client. |
| PUT | /v1/users/:userid/payment-methods/:methodid | Update the saved card. The manager only sends { isDefault: true } to flip the default. |
| DELETE | /v1/users/:userid/payment-methods/:methodid | Delete a saved card. |
All endpoints sit behind the standard RBAC/session-cookie auth.
Usage
Just adding a card
import { MovmoCardForm } from '@movmo_app/payments';
<MovmoCardForm
userId={user.id}
isDefault
defaultCardholderName={`${user.firstName} ${user.lastName}`}
onSuccess={(pm) => console.log('saved', pm)}
onError={(msg) => console.error(msg)}
/>;The cardholder name is a single editable field — users can save a card that isn't in their own name (spouse card, corporate card). Pass autoSave to submit automatically once the form is valid (no Save button rendered).
Full saved-cards management (account-page style)
import { PaymentMethodsManager } from '@movmo_app/payments';
<Card>
<PageHeader title="Payment methods" />
<PaymentMethodsManager
userId={user.id}
paymentTypeSelector={false}
autoSaveFirstCard={false}
onChange={(items) => refreshGlobalState(items)}
/>
</Card>;The manager renders only its list + add-card affordance — consumers own the outer chrome (page, drawer, modal, etc.). accounts-ui typically hides the payment-type selector and opts out of auto-save so the user clicks Save explicitly.
Checkout flow with payment-type selector + auto-save (flights-ui style)
import { PaymentMethodsManager } from '@movmo_app/payments';
<Drawer open={open} onClose={onClose}>
<PaymentMethodsManager
userId={user.id}
selectedId={selected?.id}
onSelect={(method) => setSelected(method)}
// paymentTypeSelector and autoSaveFirstCard default to true.
/>
</Drawer>;When onSelect is provided, each row renders a radio and the whole row is clickable to select. The default behavior matches the existing flights-ui add-payment UX: Credit card / PayPal / GooglePay / Klarna / ACH radios at the top (only Credit card is functional today; the others render as "Coming soon"); auto-save fires when the form is valid AND the user has zero saved cards.
Standalone hook usage
import { useUserPaymentMethods } from '@movmo_app/payments';
const { items, status } = useUserPaymentMethods(user.id);
const defaultCard = items.find((m) => m.isDefault);Configuration
Set the API base URL once at app boot (mirrors @movmo_app/api):
import { setPaymentsConfig } from '@movmo_app/payments';
setPaymentsConfig({ baseUrl: import.meta.env.VITE_MOVMO_API_URL });Local development
pnpm install # from monorepo root
pnpm --filter @movmo_app/payments build
pnpm --filter @movmo_app/payments test
pnpm --filter @movmo_app/payments storybook # → http://localhost:6007Storybook is the primary way to exercise the components on a laptop: it mocks both the underlying vault SDK and the monolith-api calls, so no vault credentials, no live backend, and no real card data are required. See src/MovmoCardForm/__docs__/MovmoCardForm.stories.tsx and src/PaymentMethodsManager/__docs__/PaymentMethodsManager.stories.tsx for the full state matrices.
