Skip to Content
SDKs@synqid/react-native

@synqid/react-native

The React Native + Expo SDK. Wraps @synqid/js; adds RN-native sheets, secure storage, deep links, and a wallet-adapter bridge for Solana. Same primitives as @synqid/react, but the modal is a bottom sheet (Gorhom or your own), tokens live in the keychain, and OAuth happens through a system browser via expo-web-browser.

AI agent? Fetch /llms.txt for a one-file index of every SDK install command and copy-pasteable snippet. RN notable gotchas: deep-link redirectUri (e.g. myapp://...) + the matching scheme in app.json, both registered in the Dashboard App’s redirect URIs.

Install

pnpm add @synqid/react-native @synqid/js pnpm add expo-secure-store expo-web-browser expo-linking @gorhom/bottom-sheet

For bare React Native (non-Expo):

pnpm add @synqid/react-native @synqid/js pnpm add react-native-keychain react-native-inappbrowser-reborn @gorhom/bottom-sheet

Add the redirect scheme to your app config.

// app.json (Expo) { "expo": { "scheme": "myapp" } }
# ios/Podfile (bare RN) — add to the relevant target ios.url_types = [{ url_schemes: ['myapp'] }]
<!-- android/app/src/main/AndroidManifest.xml (bare RN) --> <activity android:launchMode="singleTask"> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="myapp" /> </intent-filter> </activity>

Provider

import { SynqProvider } from '@synqid/react-native' export default function App() { return ( <SynqProvider issuer="https://synq.id" clientId={process.env.EXPO_PUBLIC_SYNQ_CLIENT_ID!} redirectUri="myapp://oauth/callback" > <Root /> </SynqProvider> ) }

The redirectUri is your deep link. Register the matching App redirect URI in your Synq Brand’s App settings (e.g. myapp://oauth/callback).

Options

interface SynqProviderProps { issuer: string clientId: string redirectUri: string scopes?: string[] // default request scopes walletAdapter?: NativeWalletAdapter // see below uiTheme?: SynqNativeTheme // colors, spacing, fonts storage?: SecureStorage // SecureStore by default fetch?: typeof fetch // override fetch }

Hooks

import { useUser, useSynq } from '@synqid/react-native' function Welcome() { const user = useUser() const { requestSignIn, signOut } = useSynq() if (!user) { return ( <Button title="Sign in" onPress={() => requestSignIn()} /> ) } return ( <> <Text>Hi {user.firstName}</Text> <Button title="Sign out" onPress={signOut} /> </> ) }

useUser() returns the current user or null. Re-renders on sign-in/out. useSynq() returns the full client.

Sign-in sheets

Mount them once near your root:

import { SignInSheet, ConnectSheet } from '@synqid/react-native' <SynqProvider {...config}> <YourApp /> <SignInSheet /> <ConnectSheet /> </SynqProvider>

<SignInSheet> opens via requestSignIn(). <ConnectSheet> opens via requestConnect() (for linking a wallet after the user is already signed in).

Three customization tiers

Same model as @synqid/react. Pick the lightest that fits.

Tier 1 — theme defaults

Three ways to set a theme, mirroring @synqid/react. RN-native values: numbers for radii/spacing, string font weights, no CSS.

1. Pick a preset. Built-in: dark | light | glass | monochrome | brutalist.

<SynqProvider {...config} uiTheme="glass" />

2. Override tokens. Deep-merged onto the active preset.

<SynqProvider {...config} uiTheme={{ preset: 'dark', primary: '#5FD8CA', primaryForeground: '#0A0612', radius: 16, // sugar — sets all radius* scales fontFamilyDisplay: 'Inter', }} />

3. Split light + dark. With colorScheme="system" the sheet re-renders when iOS / Android system theme changes (useColorScheme()).

<SynqProvider {...config} colorScheme="system" uiTheme={{ light: { preset: 'light', primary: '#0A6E5E' }, dark: { preset: 'dark', primary: '#5FD8CA' }, }} />
Tokens catalog
GroupTokens
Colorsprimary, primaryForeground, primaryHover, surface, surfaceElevated, border, borderHover, textPrimary, textMuted, textOnPrimary, danger, success, warning, backdrop, sheetHandle
TypographyfontFamily, fontFamilyDisplay, fontFamilyMono, fontWeightHeadline, fontWeightButton
Spacing (px)spacingXs, spacingSm, spacingMd, spacingLg, spacingXl
Radii (px)radiusSm, radiusMd, radiusLg, radiusFull, radius (sugar — sets all radius*)
Bring-your-own provider icons

Pass a React component or a URI string per provider. (RN does not support inline SVG markup — use a react-native-svg component.)

<SynqProvider {...config} providerIcons={{ google: <GoogleSvg />, apple: 'https://cdn.acme.com/apple.png', }} />

Missing entries fall back to a single-letter monogram.

Slot-level style overrides
<SynqProvider {...config} signInSheetStyles={{ background: { backgroundColor: '#0F0824' }, title: { fontFamily: 'Inter-Bold', fontSize: 22 }, providerButton: { borderRadius: 28 }, }} connectSheetStyles={{ walletButton: { borderRadius: 28 }, }} />

Slots — sign-in: background, handle, container, title, providerButton, providerButtonText, walletButton, walletButtonText. Connect: background, handle, container, title, walletButton, walletButtonText.

Default markup renders a Gorhom bottom sheet with a draggable handle, provider list, optional wallet button, and swipe-to-dismiss.

Tier 2 — compound parts

import { SignIn } from '@synqid/react-native' <SignIn> <SignIn.Sheet snapPoints={['65%', '90%']}> <SignIn.Handle /> <SignIn.Header> <SignIn.Logo /> <SignIn.Title>Welcome to Acme</SignIn.Title> <SignIn.Description> Sign in or create an account. </SignIn.Description> </SignIn.Header> <SignIn.ErrorBanner /> <SignIn.ProviderList> <SignIn.Provider id="google" /> <SignIn.Provider id="apple" /> </SignIn.ProviderList> <SignIn.Divider>or</SignIn.Divider> <SignIn.WalletButton /> <SignIn.Footer> <SignIn.Terms /> </SignIn.Footer> </SignIn.Sheet> </SignIn>

Every part accepts style and asChild (for native, “asChild” means pass a single child component that receives ref + behavior props):

<SignIn.Provider id="google" asChild> <MyButton size="lg">Continue with Google</MyButton> </SignIn.Provider>

Tier 3 — render prop

<SignIn> {(ctx) => ( <MyCustomSheet visible={ctx.isOpen} onClose={ctx.close}> <FlatList data={ctx.providers} renderItem={({ item: p }) => ( <Pressable disabled={ctx.isLoading} onPress={() => ctx.signIn(p.id)} > <Image source={{ uri: p.iconUrl }} /> <Text>Continue with {p.name}</Text> </Pressable> )} /> {ctx.error && <Text style={styles.error}>{ctx.error.message}</Text>} </MyCustomSheet> )} </SignIn>

ctx shape:

interface SignInRenderContext { isOpen: boolean close: () => void providers: Array<{ id: ProviderId name: string iconUrl: string recommended: boolean }> walletAvailable: boolean isLoading: boolean loadingProvider: ProviderId | null error: SynqAuthError | null signIn: (providerId: ProviderId) => Promise<void> signInWithWallet: () => Promise<void> }

You own the markup; Synq owns the side effects.

Wallet adapter (Solana)

import { useSolanaMwaAdapter } from '@synqid/react-native' function App() { const walletAdapter = useSolanaMwaAdapter() return ( <SynqProvider {...config} walletAdapter={walletAdapter}> <Root /> </SynqProvider> ) }

useSolanaMwaAdapter selects the right adapter at runtime:

  • Android — uses Mobile Wallet Adapter (@solana-mobile/mobile- wallet-adapter-protocol). Connects to Phantom, Solflare, etc. via the OS-level wallet picker.
  • iOS — uses Phantom’s deep-link protocol (Phantom is the only iOS wallet with a stable deep-link API as of writing). Bring a different iOS adapter via walletAdapter prop if you ship on other wallets.

Custom adapter shape (e.g. you’re building a wallet of your own):

interface NativeWalletAdapter { publicKeyBase58: string | null connected: boolean connect(): Promise<void> disconnect(): Promise<void> signMessage(message: Uint8Array): Promise<Uint8Array> }

Token storage

Tokens are stored in expo-secure-store (Keychain on iOS, EncryptedSharedPreferences on Android). Each clientId gets its own storage namespace so multiple Synq apps on the same device don’t collide.

Override for bare RN with react-native-keychain:

import { SecureStorageReactNativeKeychain } from '@synqid/react-native/storage' <SynqProvider {...config} storage={new SecureStorageReactNativeKeychain({ service: 'com.acme' })} >

Custom shape:

interface SecureStorage { getItem(key: string): Promise<string | null> setItem(key: string, value: string): Promise<void> deleteItem(key: string): Promise<void> }

The OAuth redirect flow:

  1. requestSignIn() opens expo-web-browser to Synq’s /oauth2/authorize.
  2. User completes sign-in.
  3. Synq redirects to your redirectUri (myapp://oauth/callback? code=…&state=…).
  4. iOS/Android routes the URL to your app via expo-linking.
  5. @synqid/react-native intercepts the URL, dismisses the browser, exchanges the code, stores the tokens, renders signed-in state.

You don’t need to handle the deep link yourself — the SDK registers a listener at mount.

For bare React Native without Expo, the equivalent flow uses react-native-inappbrowser-reborn + Linking from React Native; the SDK’s BareRnProvider swaps the browser/linking modules out.

Animations

The default sheet uses Gorhom’s spring animations. Override:

<SignIn.Sheet snapPoints={['65%', '90%']} animationConfigs={{ damping: 18, stiffness: 140, mass: 1.2, }} >

For users with Reduce Motion enabled, animations are dampened to < 100ms.

Accessibility

Default sheets cover:

  • VoiceOver / TalkBack focus traps (the sheet content becomes the accessible root).
  • Returns focus to the opener (your sign-in button) on close.
  • Accessibility labels on every provider button.
  • Announces loading and error state via accessibilityLiveRegion.
  • Honors Reduce Motion, Larger Text, and dark-mode color schemes via the uiTheme prop.

Programmatic open

Open from anywhere with useSynq():

const { requestSignIn, requestConnect } = useSynq() await requestSignIn({ title: 'Sign in to continue', recommendedProviders: ['google'], }) // resolves with User | null const walletAddress = await requestConnect({ title: 'Connect a wallet to claim', }) // resolves with string | null

Background refresh

When the user backgrounds your app and returns, the SDK calls getSession() automatically (which refreshes the access token if near expiry). No code from you needed.

If your app has a server backend, prefer storing the refresh token on the server (via a backend session). The SDK then only holds an access token client-side, refreshed via a backend endpoint. This limits the blast radius if a device is compromised.

Errors

Same error catalog as @synqid/js. Surface via hooks:

const { signInError } = useSynq() if (signInError?.code === 'access_denied') { // user cancelled }

Mental model summary

  • <SynqProvider> once at the root. <SignInSheet> and <ConnectSheet> once near it.
  • Same three customization tiers (theme defaults, compound, render prop) — RN-flavored.
  • Tokens in the OS keychain. One namespace per clientId.
  • Wallet adapter is pluggable. MWA on Android by default; Phantom deep link on iOS by default; bring your own for other wallets.
  • Background → foreground transitions auto-refresh the session. You don’t think about it.
Last updated on