# React Native / Expo support - `@bounded-sh/client`

The SDK ships a dedicated React Native entry (`react-native` condition ->
`dist/index.native.js`) and a platform-abstraction layer so the same
`@bounded-sh/client` package runs on web and React Native.

## 1. One-time setup before `init()`

React Native has no `localStorage`, no `document`, and no reliable browser
base64 helpers, so configure the SDK platform once at app startup:

> **Security: store login tokens ENCRYPTED, never in plaintext.** The SDK persists
> whatever adapter you give it, as-is - it adds no encryption of its own. A plain
> `createMMKV()` (or `AsyncStorage`) leaves the user's session tokens readable to
> anyone with device/backup/filesystem access. Back the store with the OS secure
> vault: an MMKV `encryptionKey` kept in the Keychain/Keystore (shown below), or a
> Keychain-backed adapter directly.

```ts
import "react-native-get-random-values";
import "react-native-url-polyfill/auto";
import { setPlatform, ReactNativeSessionManager } from "@bounded-sh/client";
import { createMMKV } from "react-native-mmkv";
import * as Keychain from "react-native-keychain";
import { decode as atob, encode as btoa } from "base-64";

// Load (or create) a random MMKV encryption key and keep it in the OS secure vault
// (Keychain on iOS / Keystore-backed on Android) - NEVER hardcode a key in source.
async function loadEncryptionKey(): Promise<string> {
  const service = "sh.bounded.session";
  const existing = await Keychain.getGenericPassword({ service });
  if (existing) return existing.password;
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes); // from react-native-get-random-values, imported above
  const key = btoa(String.fromCharCode(...bytes));
  await Keychain.setGenericPassword("bounded", key, { service });
  return key;
}

// Call this once at startup BEFORE using the SDK (e.g. gate your app on it).
export async function configureBoundedStorage() {
  const store = createMMKV({ id: "sh.bounded.session", encryptionKey: await loadEncryptionKey() });
  const storage = {
    getItem: (key: string) => store.getString(key) ?? null,
    setItem: (key: string, value: string) => store.set(key, value),
    removeItem: (key: string) => store.remove(key),
  };
  setPlatform({ storage, sessionStorage: storage, atob, btoa, hasDOM: false });
  ReactNativeSessionManager.configure({ storage, atob });
}
```

`setPlatform` fills general browser API gaps. `ReactNativeSessionManager.configure`
selects the RN token store used by login, restore, logout, and authenticated
requests.

## 2. Hosted email / social login

Human login runs through the hosted Bounded issuer. Retired app-origin OTP
helpers (`sendEmailOtp`, `verifyEmailOtp`, `sendTextOtp`, `verifyTextOtp`) are
not exported by the package.

Install the Expo optional peers if your RN app uses hosted login:

```sh
npx expo install expo-web-browser expo-crypto
```

Use an https universal link as your redirect URI and register that origin in the
Bounded app's `allowedOrigins`:

```ts
import { init, loginWithRedirect, getCurrentUser } from "@bounded-sh/client";

await init({ appId: "YOUR_APP_ID" });

const user = await loginWithRedirect({
  redirectUri: "https://yourapp.com/auth/callback",
  methods: ["email"],
});

const restored = await getCurrentUser();
```

On native, `loginWithRedirect()` opens the system/in-app browser and resolves
with the user when the callback returns to the app. `completeLoginFromRedirect()`
is for web callback pages.

## 2b. Solana Mobile wallet (Seeker / Saga)

`@solana-mobile/wallet-standard-mobile` is an optional peer, the same as the Expo
peers above, so it is not installed for you:

```sh
npm install @solana-mobile/wallet-standard-mobile
```

Leaving it out keeps React Native and the metro toolchain out of web-only
installs of this package, which is why it is not a hard dependency. It is loaded
lazily, on the one path that registers the phone's wallet, so an app that never
offers a mobile wallet never pays for it.

Without it that registration throws `WalletConfigError` naming the package;
every other wallet, and the rest of the wallet lane, is unaffected.

## 3. Guest login is browser-only in the current release

Do not call `signInAnonymously()` from React Native. The current guest identity
uses a non-extractable WebCrypto Ed25519 key stored in IndexedDB; it does not use
the React Native session-storage adapter above. Native runtimes therefore fail
closed before making an authentication request.

Use hosted email/social login from section 2, or the explicit Privy Expo bridge
below. Do not add an IndexedDB polyfill to persist a guest signing key: that does
not provide the platform-backed, non-exportable key contract a native guest flow
needs. Native anonymous auth will be documented here only after the SDK ships a
Keychain/Keystore-backed signing implementation and migration/loss semantics.

## 4. Privy on React Native (`privy-expo`)

`@privy-io/expo` is hook-based, so the host app renders `<PrivyProvider>` and
bridges the hooks into a `PrivyExpoProvider` instance.

```bash
npm i @privy-io/expo @privy-io/expo-native-extensions
```

```tsx
import { PrivyProvider, usePrivy, useEmbeddedSolanaWallet, useIdentityToken } from "@privy-io/expo";
import { PrivyExpoProvider, init, loginWithPrivy } from "@bounded-sh/client";

const provider = new PrivyExpoProvider(PRIVY_APP_ID, SOLANA_RPC_URL);

function PrivyBridge() {
  const privy = usePrivy();
  const wallet = useEmbeddedSolanaWallet();
  const { getIdentityToken } = useIdentityToken();

  useEffect(() => {
    provider.setPrivyMethods({
      isReady: privy.isReady,
      isAuthenticated: !!privy.user,
      user: privy.user,
      login: privy.login,
      logout: privy.logout,
      getAccessToken: privy.getAccessToken,
      getIdentityToken,
      getWalletProvider: async () => {
        const solanaWallet = wallet.wallets?.[0];
        if (!solanaWallet) return null;
        const solanaProvider = await solanaWallet.getProvider();
        return {
          address: solanaWallet.address,
          signMessage: solanaProvider.signMessage,
          signTransaction: solanaProvider.signTransaction,
          signAndSendTransaction: solanaProvider.signAndSendTransaction,
        };
      },
    });
  }, [privy.isReady, privy.user, wallet.wallets]);

  return null;
}

await init({
  appId,
  authMethod: "privy-expo",
  privyExpoProvider: provider,
  rpcUrl: SOLANA_RPC_URL,
});

await loginWithPrivy();
```

## 5. Metro notes

The default client entry no longer imports Phantom, web Privy, mobile wallet
adapter, or `react-dom` provider implementations. Apps that only use hosted login
or guest login should not need Metro stubs for those optional peers.

For Solana signing paths, keep `react-native-get-random-values` at app entry and
make sure `buffer` is polyfilled if your bundler setup does not provide it.
