@servable/react-native
v1.0.0
Published
Servable client for React Native - routes, auth, user and hooks
Readme
@servable/react-native
Servable client for React Native — routes, auth, current user, and hooks. The React Native
counterpart to @servable/nextjs, with the same surface (Routes, User,
hooks, store) and the same request pipeline: device params, W3C trace propagation, retry
with backoff, access-token refresh, and step-up handling.
Install
yarn add @servable/react-native axios @react-native-async-storage/async-storageaxios, react, react-native and AsyncStorage are peer dependencies. AsyncStorage is
optional if you inject your own storage adapter (see below).
Configure
There are no NEXT_PUBLIC_* environment variables here — Metro does not substitute env vars
the way Next inlines them at build time — so everything is configured once at app start:
import Servable from '@servable/react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as Keychain from 'react-native-keychain'
Servable.configure({
serverUrl: 'https://backend.example.com', // required
version: 'v1',
timeout: 15000,
platformId: 'your-platform-id',
storage: AsyncStorage, // optional; auto-detected if installed
secureStorage: myKeychainAdapter, // strongly recommended - see Security
onAccountRedirect: () => navigation.navigate('SignIn'),
onCurrentUserChanged: (user) => userStore.set(user),
sentry: Sentry, // optional, @sentry/react-native
})onAccountRedirect and onCurrentUserChanged replace the two host-app modules the web package
imported through Next path aliases (lib/account/lib/openaccountredirect,
lib/contexts/currentUserContext). A library can't reach into its consumer, so they're
injected.
Use
const { result, error } = await Servable.Routes.Get({ path: 'account/me' })
await Servable.Routes.Post({ path: 'publication/create', params: { name: 'My pub' } })
// Uploads take React Native's file descriptor shape
await Servable.Routes.Function({
name: 'uploadAvatar',
files: [{ uri: asset.uri, fileName: 'avatar.jpg', type: 'image/jpeg' }],
})
const user = await Servable.User.currentAsync()
await Servable.User.signOut()On app start (and on resume), re-mint the access token — it's held in memory only, so it's empty after every cold start:
useEffect(() => { Servable.refreshAccessToken() }, [])Deep links and install referrers feed attribution, in place of the web's UTM query parameters:
Servable.setSourceParams({ sourceNature: 'referral', sourceId: 'abc' })How this differs from @servable/nextjs
Most of the package is a direct port. These are the places where the platform forced a real change rather than a cosmetic one.
Storage is async. getStoreValue/setStoreValue and the Routes.Get response cache all
return promises. Their web counterparts are synchronous because cookies and localStorage are;
AsyncStorage is not. Any code moved over from the web package must await them.
Refresh tokens are stored by the app. On the web the refresh token is an httpOnly cookie —
never readable by JS, resent automatically. React Native has no httpOnly cookies, so the token
is stored and sent explicitly. That makes where it's stored a security decision, which is why
secureStorage is a separate adapter: point it at Keychain / EncryptedSharedPreferences
(react-native-keychain, expo-secure-store). It falls back to ordinary storage, which is
not appropriate for production.
No SSR. The context argument (a Next { req, res } pair) is gone from every function.
No custom-domain proxy. The web package routed custom-domain traffic through a same-origin
proxy and carried a _pk_sess cookie fallback, both purely to survive browser third-party
cookie policy. Native has no origin and no cookie jar; requests always go straight to
serverUrl.
An unset serverUrl throws. The web version fell back to a relative /v1/path, which is a
valid same-origin URL in a browser. On native a relative URL is just a broken request, so this
surfaces immediately as a configuration error.
Uploads use file descriptors, not Blobs. React Native's FormData takes
{ uri, name, type } and streams from disk; appending a Blob produces a malformed part
rather than an error. Routes.Function accepts the native shape, a base64 data URI, or a
json value, and normalizes all three.
No atob/btoa/TextEncoder. None exist in Hermes or JSC, so JWT decoding and base64
encoding use Buffer when present and a pure-JS path otherwise.
Sentry is optional. @sentry/nextjs can't be installed in an RN app, and @sentry/react-native
shouldn't be forced on consumers who don't use it — pass it via configure({ sentry }) or set
a global. Without it, trace propagation still works; requests just aren't joined to a Sentry
transaction.
User.signOut() is new. On the web, signing out clears server-controlled cookies, so the
client had nothing to do. Here every credential is app-held and must be explicitly discarded.
Tests
yarn testESM-native, run under --experimental-vm-modules with testEnvironment: "node" — deliberately
not jsdom, so anything that only works because a browser global happens to exist fails loudly
instead of passing in an environment the real runtime doesn't resemble. Use
jest.unstable_mockModule with dynamic import(); plain jest.mock will not work.
