@movmo/connect-button
v0.4.0
Published
React component that initiates Movmo's OAuth 2.0 PKCE flow, so partners don't hand-roll PKCE.
Maintainers
Readme
@movmo/connect-button
A branded React component that initiates Movmo's OAuth 2.0 PKCE flow against the Movmo auth UI, so partners don't hand-roll PKCE.
Zero auth-related config: every Movmo URL is derived from the single isProd flag (default false → e2e). You never set an auth URL — pass isProd when you go to production, and clientId + redirectUri are the only required props.
Why it's not a drop-in checkout
This package is distinct from @movmo/express-checkout-button.
| Package | Surface | Problem solved |
| -------------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| @movmo/express-checkout-button | Airline booking page | Embeds a Movmo checkout wallet inside the airline's booking flow |
| @movmo/connect-button | Partner's auth / account page | Initiates an OAuth 2.0 PKCE authorization so the partner's backend can act on behalf of a Movmo traveler |
Different surface, different problem — do not substitute one for the other.
What this does NOT do
- Token exchange runs on your backend. This package hands you a
codeandcodeVerifier; your server uses both alongsideclient_secretto callPOST /v1/oauth/token. Theclient_secretcredential cannot reach a browser. - See Authentication for the full server-side token exchange flow.
- This package never stores tokens, never makes API calls, and has no server-side component.
Install
npm install @movmo/connect-button
# peer deps
npm install react react-domReact 18+ is required.
Quickstart
1. Render the button on your connect page
import { ConnectMovmoButton } from '@movmo/connect-button';
export function ConnectPage() {
return (
<ConnectMovmoButton
clientId="your-client-id"
redirectUri="https://yourapp.com/oauth/callback"
// isProd — add when you go live; without it the button targets Movmo's e2e environment
/>
);
}2. Handle the callback
On your /oauth/callback page, call completeMovmoOAuth to validate the redirect and retrieve the
codeVerifier for the server-side exchange:
import { useEffect } from 'react';
import { completeMovmoOAuth } from '@movmo/connect-button';
export function CallbackPage() {
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
if (!code || !state) return;
completeMovmoOAuth(code, state)
.then(({ code, codeVerifier }) => {
// POST both to your backend — never expose client_secret in the browser
return fetch('/api/movmo/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, codeVerifier }),
});
})
.catch((err) => console.error('OAuth completion failed:', err));
}, []);
return <p>Completing authorization…</p>;
}Headless usage
For full control over the button's appearance, use the useMovmoOAuth hook directly:
import { useMovmoOAuth } from '@movmo/connect-button';
export function CustomConnectButton() {
const { startOAuth, isStarting } = useMovmoOAuth({
clientId: 'your-client-id',
redirectUri: 'https://yourapp.com/oauth/callback',
onError: (err) => console.error('OAuth error:', err),
});
return (
<button onClick={startOAuth} disabled={isStarting}>
{isStarting ? 'Redirecting…' : 'Connect your Movmo account'}
</button>
);
}MovmoRecognition
MovmoRecognition renders a fixed-position, embeddable recognition surface — an iframe
pointed at flights-ui's /recognize route — that tells the airline page whether the current
visitor is a recognized Movmo traveler, without ever exposing PII or the traveler's durable
identifier to the host page.
It carries no identity data itself:
onStatefires with'recognized' | 'anonymous' | 'none'so you know what to render, never who the traveler is.onTokenfires with a short-lived (120s), single-use, opaque token. Forward it to your own backend to redeem it — the browser can't read identity out of it.- A
RECOGNITION_CONNECTgesture from inside the iframe (e.g. "Connect your account") starts the same PKCE redirect asConnectMovmoButton, using yourclientId/redirectUri/scope.
Works standalone on any page, or alongside MovmoCheckoutButton
(@movmo/express-checkout-button) — the two are independent surfaces and don't share state.
import { MovmoRecognition } from '@movmo/connect-button';
export function AirlineLayout() {
return (
<MovmoRecognition
clientId="your-client-id"
redirectUri="https://yourapp.com/oauth/callback"
airlineName="Aero"
chipPosition="bottom-right"
onState={(state) => console.log('recognition state:', state)}
onToken={(token) => {
// Forward to your backend to redeem — never decode this in the browser.
fetch('/api/movmo/recognize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
}}
/>
);
}The wrapper starts genuinely empty (0×0, hidden) and only becomes visible once the iframe
posts its first size — there's no layout shift or placeholder chrome to account for before
that. Repositioning via chipPosition is CSS-only on the wrapper; the iframe itself is never
moved or reloaded once mounted.
MovmoRecognition props
| Prop | Type | Default | Description |
| -------------- | -------------------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| clientId | string | — | Required. OAuth client_id issued by Movmo. |
| redirectUri | string | — | Required. Redirect URI used if the traveler triggers a connect from the iframe. |
| scope | string[] | ['profile.read', 'preferences.read'] | Scopes requested if the traveler connects. See the scope note. |
| isProd | boolean | false | Selects the Movmo environment: the flights-ui origin (flights.movmo.io vs. flights.e2e.movmo.io) and the auth origin (auth.movmo.io vs. auth.e2e.movmo.io). |
| chipPosition | 'bottom-left' \| 'bottom-right' \| 'left' \| 'right' | 'bottom-left' | Corner/edge the wrapper is anchored to. CSS-only — never reloads the iframe. |
| transparency | 'full' \| 'minimal' | 'full' | Visibility tier for the recognized-state indicator: 'full' keeps a persistent collapsed chip after the first-visit banner; 'minimal' shows the first-visit banner only, then nothing. The anonymous/connect chip always renders regardless (Safari/Firefox need the in-iframe click for Storage Access). The iframe implements the behavior — this just forwards the airline's choice. |
| airlineName | string | — | Airline display name, forwarded to the iframe for personalized copy. |
| onToken | (token: string) => void | — | Called with the opaque recognition token. Forward it to your backend. |
| onState | (state: 'recognized' \| 'anonymous' \| 'none') => void | — | Called whenever the recognition state changes. Carries no identity data. |
| extraOrigins | string[] | — | Extra origins to trust in non-prod (e.g. a local flights-ui dev server). Ignored when isProd is true. |
| authorizeUrl | string | derived from isProd | Base URL for the Movmo auth UI, used only if a connect gesture fires. Defaults per isProd — the same rule as the flights-ui origin — so it never needs to be set outside local testing. Same semantics as UseMovmoOAuthOptions.authorizeUrl. |
| storage | StorageAdapter | sessionStorageAdapter | Storage adapter for the OAuth code_verifier/state, used only if a connect gesture fires. |
Prop reference
ConnectMovmoButton
All UseMovmoOAuthOptions props are also accepted (see below).
| Prop | Type | Default | Description |
| ------------- | ------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| clientId | string | — | Required. OAuth client_id issued by Movmo. |
| redirectUri | string | — | Required. Redirect URI registered for your client. |
| label | string | "Connect" | Gradient-side text. Renders to the left of the white "Movmo" badge — visually reads "<label> Movmo". |
| variant | 'primary' \| 'secondary' \| 'ghost' | 'primary' | Visual style. |
| className | string | '' | Extra CSS classes merged onto the <button> element. |
| disabled | boolean | false | Disable independently of loading state. |
| ariaLabel | string | "Connect with Movmo" | Accessible label override. |
UseMovmoOAuthOptions
| Prop | Type | Default | Description |
| --------------- | --------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| clientId | string | — | Required. |
| redirectUri | string | — | Required. |
| scope | string[] | — | See Forward-compat note. |
| isProd | boolean | false | Selects the environment defaults: auth.movmo.io (prod) vs. auth.e2e.movmo.io. Same semantics as MovmoRecognition's isProd. |
| authorizeUrl | string | derived from isProd | Base URL for the Movmo auth UI. The auth-ui parses OAuth params from the root URL — there is no /authorize sub-path in the deployed implementation. An explicit value beats the isProd default — only needed for local testing. |
| storage | StorageAdapter | sessionStorageAdapter | Where to persist code_verifier and state between redirect legs. |
| generateState | () => string | base64url of 16 random bytes | Custom state generator (useful for testing). |
| onError | (err: OAuthError) => void | — | Called with a typed error when the flow fails. The error is also re-thrown for caller try/catch. |
Storage adapter
The default sessionStorageAdapter uses window.sessionStorage. For SSR (Next.js) or
environments where sessionStorage is unavailable, pass a custom adapter:
import type { StorageAdapter } from '@movmo/connect-button';
// Example: cookie-backed adapter for SSR / cross-tab persistence
const cookieAdapter: StorageAdapter = {
getItem: (key) => {
const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
},
setItem: (key, value) => {
document.cookie = `${key}=${encodeURIComponent(value)}; path=/; SameSite=Strict`;
},
removeItem: (key) => {
document.cookie = `${key}=; path=/; max-age=0`;
},
};
// Pass it to the button or hook
<ConnectMovmoButton
clientId="your-client-id"
redirectUri="https://yourapp.com/oauth/callback"
storage={cookieAdapter}
/>The StorageAdapter interface is async-compatible — all methods may return Promise values.
Forward-compat note on scope
The scope[] prop is forwarded as a space-separated scope query param if provided, but Movmo
does not enforce per-scope authorization today. Until fine-grained scopes ship, the resulting
token carries the user's full RBAC permissions regardless of what you request.
Tracked under MOVMO-346. Accept that until
it ships, include scope in your integration now if you want forward-compat, knowing it has no
enforcement effect yet.
Versioning
0.x means the public API may shift between minor versions while we land Aviare and the next two
partner integrations. Pin to ~0.1.0 if you want patch-only updates:
"dependencies": {
"@movmo/connect-button": "~0.1.0"
}License
MIT © Movmo
