> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding React Native support

<Card title="Recommended: JavaScript SDK for React Native" icon="react" color="#4779FE">
  While this SDK is still supported, we recommend using newer [JavaScript SDK](/docs/javascript/reference/react-native-quickstart), which is optimized for React Native, but also comes with a host of other benefits.
</Card>

The way Dynamic integrates with React Native is by extending our [client](/docs/react-native/reference/client)
with the React Native Extension. This adds the `reactNative` module, which provides access to a *webview* component
that must be rendered to your app.

<Note>
  Since our client was built with a modular approach, each extension must be installed as a separate
  package, so to keep the client's package size to a minimum.
</Note>

## Installation

<Note>
  From **v4.83.0** onwards, the React Native SDK requires **Expo SDK 52 or later**. `@dynamic-labs/react-native-extension` declares `expo-modules-core: >=2.0.0` as a peer dependency, which is first satisfied by Expo SDK 52. Stay on **v4.82.x** if you need to support Expo SDK 50 or 51.
</Note>

Simply run the following in your terminal:

<Tabs>
  <Tab title="expo">
    ```bash Shell theme={"system"}
    npx expo install @dynamic-labs/react-native-extension react-native-webview expo-web-browser expo-linking expo-secure-store
    ```
  </Tab>

  <Tab title="npm">
    ```bash Shell theme={"system"}
    npm install @dynamic-labs/react-native-extension react-native-webview expo-web-browser expo-linking expo-secure-store
    ```
  </Tab>

  <Tab title="yarn">
    ```bash Shell theme={"system"}
    yarn install @dynamic-labs/react-native-extension react-native-webview expo-web-browser expo-linking expo-secure-store
    ```
  </Tab>
</Tabs>

## Prebuild (Expo) / Native linking

Since our SDK relies on native modules (e.g. `react-native-webview`, `expo-secure-store`), you need to
rebuild the native layer of your app after installing.

<Warning>
  **Expo Go is not supported.** Dynamic depends on native modules that are linked when you **prebuild** your app (for example with `npx expo prebuild` or an EAS/cloud build). Expo Go runs a generic prebuilt client and does **not** prebuild your project, so those native dependencies never make it into the binary you launch. Use a **development build**, **EAS Build**, or a **bare React Native** workflow instead.
</Warning>

<Tabs>
  <Tab title="Expo (managed)">
    If you're using Expo with a managed workflow, run:

    ```bash Shell theme={"system"}
    npx expo prebuild
    ```

    Then rebuild your app with `npx expo run:ios` or `npx expo run:android`.
  </Tab>

  <Tab title="EAS Build">
    If you're using [EAS Build](https://docs.expo.dev/build/introduction/), the native modules will be linked automatically during the cloud build. Simply run:

    ```bash Shell theme={"system"}
    eas build --profile <your-profile> --platform all
    ```

    Then install the resulting development build on your device or simulator.
  </Tab>

  <Tab title="Bare React Native">
    If you're using a bare React Native project, install the native dependencies:

    ```bash Shell theme={"system"}
    cd ios && pod install && cd ..
    ```

    Then rebuild your app using Xcode or `npx react-native run-ios` / `npx react-native run-android`.
  </Tab>
</Tabs>

## Usage with React Native

First, extend your client with our extension:

```typescript theme={"system"}
import { ReactNativeExtension } from '@dynamic-labs/react-native-extension';

export const dynamicClient = createClient({
  environmentId: 'YOUR-ENVIRONMENT-ID',
}).extend(ReactNativeExtension());
```

Next, render the webview injected into the client by the extension:

```typescript theme={"system"}
import { dynamicClient } from '<path to client file>';

export function App() {
  return (
    <>
      <dynamicClient.reactNative.WebView />

      <SafeAreaView>
        { ... }
      </SafeAreaView>
    </>
  )
}
```

You can read more about our react native package [here](/docs/react-native/reference/package-references/react-native-extension).

## Embedded WebView (native overlay)

<Note>
  Available from **v4.82.0**.
</Note>

By default, the SDK renders its webview as a regular React Native component (via
`react-native-webview`) that you mount in your app tree. If your app's navigation
aggressively unmounts and remounts screens — or you simply want the wallet session to be
fully decoupled from your view tree — you can opt in to **embedded WebView mode**.

Pass `embeddedWebView: true` to the extension:

```typescript theme={"system"}
import { createClient } from '@dynamic-labs/client'
import { ReactNativeExtension } from '@dynamic-labs/react-native-extension'

export const dynamicClient = createClient({
  environmentId: 'YOUR-ENVIRONMENT-ID',
}).extend(
  ReactNativeExtension({
    appOrigin: 'https://your-app.example',
    embeddedWebView: true,
  }),
)
```

In this mode the SDK hosts the webview-controller inside a dedicated native overlay window
(`WKWebView` on iOS, `android.webkit.WebView` on Android) owned outside the React Native
view tree. The overlay is created lazily on first use and retained for the process
lifetime.

<Warning>
  When `embeddedWebView` is `true`, **do not render** `dynamicClient.reactNative.WebView`
  in your app — it resolves to a no-op component on the embedded path.
</Warning>

```typescript theme={"system"}
import { dynamicClient } from '<path to client file>';

export function App() {
  return (
    <SafeAreaView>
      { ... }
    </SafeAreaView>
  )
}
```

<Note>
  On platforms other than iOS / Android (e.g. web), the `embeddedWebView` flag is ignored
  and the standard `react-native-webview` path is used.
</Note>

### Caveats

* **Same load engine as the standard path.** The embedded overlay runs the same
  bounded-retry load engine as the `react-native-webview` path, so a transient failure is
  retried automatically and you can observe progress and trigger a manual retry through the
  same API — see [Tuning and monitoring the WebView load](#tuning-and-monitoring-the-webview-load).
  When every attempt is exhausted, the failure surfaces as `core.initialization.error` with
  `WebViewFailedToLoadError`; treat the SDK as un-initialised in your error handler.
* **Singleton.** Re-invoking `ReactNativeExtension({ embeddedWebView: true })` (e.g. when
  you recreate the client with a different `environmentId`) re-binds the JS bridge to the
  same native overlay rather than creating a new one.
* **Debugging** still works via `webviewDebuggingEnabled: true` (Safari Web Inspector on
  iOS, `chrome://inspect` on Android).

## Tuning and monitoring the WebView load

Whichever WebView path you use, the SDK loads its webview through a bounded-retry engine:
each load attempt is given a time budget, a transient failure (network drop, wedged CDN
edge) is retried automatically after a short backoff, and the failure only becomes terminal
once every attempt is exhausted. You can tune the budgets, observe the load in real time,
and trigger a manual retry.

### Tuning the load budgets

Pass a `webviewLoad` group to the extension to tune how long each attempt may take and how
many attempts to make. Every field is optional — only tune these for pathological networks.

```typescript theme={"system"}
import { createClient } from '@dynamic-labs/client'
import { ReactNativeExtension } from '@dynamic-labs/react-native-extension'

export const dynamicClient = createClient({
  environmentId: 'YOUR-ENVIRONMENT-ID',
}).extend(
  ReactNativeExtension({
    webviewLoad: {
      maxAttempts: 3,
      pageLoadTimeoutMs: 20000,
      firstAttemptPageLoadTimeoutMs: 10000,
      sdkReadyTimeoutMs: 20000,
    },
  }),
)
```

| Field                           | Type      | Default | Description                                                                                                                                                                                                                                                                                                               |
| ------------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxAttempts`                   | `number?` | `3`     | Total load attempts per cycle (the initial attempt plus automatic retries). `1` disables automatic retries — the first failure is terminal and your app owns recovery via `client.sdk.retry()`. Values are clamped into `[1, 10]` with a console warning.                                                                 |
| `pageLoadTimeoutMs`             | `number?` | `20000` | Max time (ms) an attempt may spend loading the page (waiting for the server's response and for the page to finish).                                                                                                                                                                                                       |
| `firstAttemptPageLoadTimeoutMs` | `number?` | `10000` | Fail-fast cap (ms) on the first attempt's wait-for-response window only, so a wedged first load retries quickly instead of burning the full `pageLoadTimeoutMs`. It is a cap, never an extension — set it at or above `pageLoadTimeoutMs` to disable fail-fast. Values below 1s are clamped to 1s with a console warning. |
| `sdkReadyTimeoutMs`             | `number?` | `20000` | Max time (ms) between the page finishing its load and the Dynamic SDK inside the webview signalling it is ready.                                                                                                                                                                                                          |

<Note>
  A misconfigured value warns and is clamped into its safe range — it never throws, so a
  config typo cannot crash SDK initialisation.
</Note>

### Observing the load state

`client.sdk.loadState` is a live snapshot of the load engine. Read it through
[`useReactiveClient`](/docs/react-native/reference/hooks-events/react-hooks) to re-render on
every change:

```typescript theme={"system"}
import { useReactiveClient } from '@dynamic-labs/react-hooks'
import { Text } from 'react-native'

import { dynamicClient } from '<path to client file>'

export const LoadStatus = () => {
  const { sdk } = useReactiveClient(dynamicClient)
  const { status, attempt, maxAttempts } = sdk.loadState

  if (status === 'ready' || status === 'idle') return null

  return <Text>Connecting to Dynamic… (attempt {attempt}/{maxAttempts})</Text>
}
```

`loadState` has the following shape:

| Field         | Type                                                               | Description                                                                                                                                             |
| ------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`      | `'idle' \| 'loading' \| 'waiting_to_retry' \| 'ready' \| 'failed'` | Lifecycle status of the load. A stable contract you can branch on. `waiting_to_retry` means the attempt failed and the next is waiting out its backoff. |
| `phase`       | `string \| null`                                                   | The in-flight step of the current attempt while `loading`. Diagnostic only — do not branch on it, as the values may change between versions.            |
| `attempt`     | `number`                                                           | 1-based counter of load attempts within the current cycle. `0` until the webview load controller has been registered.                                   |
| `maxAttempts` | `number`                                                           | Total attempt budget of a cycle. `0` until the webview load controller has been registered.                                                             |
| `nextRetryAt` | `number \| null`                                                   | Wall-clock timestamp (ms) the pending retry is due at while `waiting_to_retry`, else `null`. Use it to render a countdown.                              |
| `error`       | `Error \| null`                                                    | The terminal SDK initialization error while `failed` (proxies `client.sdk.error`), else `null`.                                                         |

Outside React, subscribe to the `loadStateChanged` event instead:

```typescript theme={"system"}
const handleLoadStateChanged = (loadState) => {
  console.log('load state', loadState.status)
}

dynamicClient.sdk.on('loadStateChanged', handleLoadStateChanged)

// When you no longer need to listen
dynamicClient.sdk.off('loadStateChanged', handleLoadStateChanged)
```

### Retrying a failed load

When `status` is `failed`, call `client.sdk.retry()` to start a fresh load cycle. It
resolves with the load state of the next `ready` or `failed` transition. Calling it while
`waiting_to_retry` skips the remaining backoff and retries immediately; in any other state
it is a safe no-op.

```typescript theme={"system"}
import { useReactiveClient } from '@dynamic-labs/react-hooks'
import { Button, Text, View } from 'react-native'

import { dynamicClient } from '<path to client file>'

export const LoadRetry = () => {
  const { sdk } = useReactiveClient(dynamicClient)

  if (sdk.loadState.status !== 'failed') return null

  return (
    <View>
      <Text>Dynamic failed to load</Text>
      <Button title='Retry' onPress={() => void sdk.retry()} />
    </View>
  )
}
```

<Tip>
  Set `maxAttempts: 1` when you want your app to own the retry experience: the first failure
  becomes terminal immediately, and you drive recovery entirely through your own UI and
  `client.sdk.retry()`.
</Tip>
