Skip to content

feat(react-native): add the headless React Native binding - #151

Merged
Bccorb merged 3 commits into
mainfrom
feat/react-native-binding
Sep 13, 2026
Merged

feat(react-native): add the headless React Native binding#151
Bccorb merged 3 commits into
mainfrom
feat/react-native-binding

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #150 (which is stacked on #149). The second binding over @seamless-auth/client, and the package the RoxTarget pilot app and the Expo template will install. Part of fells-code/seamless-templates#40; the prebuilt native views (#126) are deliberately out of scope for the first version.

@seamless-auth/react-native 0.1.0:

  • AuthProvider over the shared session store, always on bearer transport. Props: apiHost, basePath (default /auth), magicLinkRedirectUri, ports, fetch. No browser defaults: a native app supplies its ports, and the provider keys the session on their identities.
  • useAuth, useAuthClient (the session's own client, which carries a sign-in from /login through its OTP or passkey step), useLoginMethods, usePasskeySupport (reads the passkey port).
  • Ports, each taking its native module as a parameter so an app installs only what it uses and Metro never resolves a module it did not:
    • createSecureStoreTokenStorage(SecureStore, { key?, storeOptions? }): the pair as one keystore entry; never throws (a locked or unparseable keystore reads as signed out).
    • createNativePasskeyPort(Passkeys): react-native-passkeys; a null result and the platform's cancel/duplicate codes map onto NotAllowedError / InvalidStateError as PasskeyCeremonyError, other failures keep their name.
    • createWebBrowserOAuthRedirect(WebBrowser, options?): openAuthSessionAsync, resolving { type: 'callback', code, state } or cancelled; parseOAuthCallbackUrl exported for deep-link handling.
    • describeDevice(Platform, name?) for passkey metadata.
  • Re-exports the client surface an app reaches for, so an app installs one package.
  • README covers wiring, the emulator host, the associated-domain prerequisite for passkeys, and the magic link poll.

Bundle externals are react, react/jsx-runtime, @seamless-auth/client.

Test plan

  • npm run lint, npm run typecheck, npm run build, npx prettier --check ., npm run check-npm-build
  • npm test: 406 passing across three projects, coverage 91.12 / 82.50 / 90.88 / 91.36
  • New: react-native/tests/ports.test.ts (secure store round-trip, key and options passthrough, unparseable and throwing keystores, native passkey ceremonies and failure mapping, callback URL parsing for https and custom schemes, auth session outcomes, device description) and react-native/tests/provider.test.tsx (session restored from the keystore over bearer transport with the right headers and no credentials, signed-out start without a refresh call, custom mount path, useAuth outside the provider)
  • npx changeset status: client minor, react minor, react-native minor

Not verified here

  • Against a real device or simulator. That happens in the RoxTarget pilot app, which is the next PR series and will feed fixes back here.

@Bccorb

Bccorb commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 94833ac: the transport now flattens a Headers instance (or an entries array) before merging request headers, and a caller's header replaces a default of the same name case-insensitively. Found wiring the RoxTarget app: its apiFetch builds headers with new Headers(), and spreading that on React Native produced a nested map object that Expo's native fetch refused, so every authorizedFetch failed with a network error.

@Bccorb

Bccorb commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Also pushed faba6fa: authorizedFetch now sends a URL under the adapter's mount as an adapter call (transport header, route identity and effect). RoxTarget's /auth/sessions list goes through the same apiFetch as its /api routes, and without this it was answered in cookie mode.

@Bccorb
Bccorb force-pushed the feat/react-native-binding branch from faba6fa to 99f6d4b Compare September 13, 2026 14:17
Base automatically changed from feat/client-ports to main September 13, 2026 15:42
An error occurred while trying to automatically change base from feat/client-ports to main September 13, 2026 15:42
An AuthProvider over the shared session store, always on bearer transport;
useAuth, useAuthClient, useLoginMethods and usePasskeySupport; and the native
ports: createSecureStoreTokenStorage over expo-secure-store,
createNativePasskeyPort over react-native-passkeys (mapping native failures
onto the DOMException names the client's error readers understand),
createWebBrowserOAuthRedirect over an expo-web-browser auth session resolving
the callback's code and state, and describeDevice for passkey metadata. Each
port takes its native module as a parameter, so an app installs only what it
uses and Metro never resolves a module it did not.

No screens: the app brings its own, the way a web app that skips AuthRoutes
does. The flows, session state and token custody come from
@seamless-auth/client.
Spreading a Headers instance copies its internals rather than its entries.
On React Native the polyfill keeps a map field, and the nested object made
Expo's native fetch refuse every authorizedFetch call whose caller built its
headers with new Headers(). The caller's headers are now read as entries,
whatever shape they came in, and replace a default of the same name
case-insensitively instead of travelling beside it.
…apter calls

An application reaching the adapter's own routes through authorizedFetch
(its session list, a passthrough it adds) got no transport header, so in
bearer transport the adapter answered in cookie mode and refused the call.
A URL under the mount now takes the same road as the client's own calls:
the header, the route's identity, and its effect on the held tokens.
@Bccorb
Bccorb force-pushed the feat/react-native-binding branch from 99f6d4b to 4800994 Compare September 13, 2026 15:43
@Bccorb
Bccorb merged commit 280ceee into main Sep 13, 2026
3 checks passed
@Bccorb
Bccorb deleted the feat/react-native-binding branch September 13, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant