> ## 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.

# Hosted wallet connector on React Native

> Present the hosted connect page in a web auth session with expo-web-browser, for Expo or bare React Native.

> [!NOTE]
>
> This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.

On React Native the [hosted connect page](https://www.dynamic.xyz/docs/connections/overview.md) contract is the same as the web and iOS, run in a web auth session. Works in Expo and bare React Native with Expo modules. The whole integration is one file, `FireblocksConnect.ts`.

> [!TIP]
>
> Use `expo-web-browser`'s `openAuthSessionAsync`. It is `ASWebAuthenticationSession` on iOS and Chrome Custom Tabs on Android, the same "open web, return via a callback scheme" primitive, run ephemerally (no consent prompt).

> [accordion] View the full FireblocksConnect.ts (copy-paste ready)
>
> ````ts FireblocksConnect.ts theme={"system"}
> import * as WebBrowser from "expo-web-browser";
> import * as Linking from "expo-linking";
> import * as Crypto from "expo-crypto";
>
> // ── Public API ──────────────────────────────────────────────────────────────
>
> /** A wallet the user connected through the hosted Fireblocks flow. */
> export interface WalletConnection {
>   address: string;
>   /** "evm" | "solana" | "bitcoin" */
>   chain: string;
>   walletName: string;
>   /** Icon URL — often an SVG-sprite URL, so render it in a WebView `<img>`. */
>   walletImage: string;
> }
>
> /** Thrown when the user dismisses the sheet. */
> export class FireblocksConnectCancelled extends Error {}
> /** Thrown on a nonce mismatch or an unparseable return. */
> export class FireblocksConnectError extends Error {}
>
> /**
>  * Connect a self-custodial wallet through a hosted Fireblocks page — no SDK.
>  *
>  * ```ts
>  * const wallet = await connectWallet({
>  *   hostedPageUrl: "https://connect.example.com/",
>  *   scheme: "myapp",              // must match your app.json "scheme"
>  * });
>  * console.log(wallet.address, wallet.chain);
>  * ```
>  *
>  * Opens the page with `expo-web-browser`'s auth session
>  * (`ASWebAuthenticationSession` on iOS, Chrome Custom Tabs on Android). It
>  * appends `redirect_uri`, a random `nonce`, `embedded=1`, and an optional
>  * `environmentId`, verifies the returned nonce, and resolves with the wallet.
>  *
>  * Most wallets return inside the session; some (Phantom) finish in their own
>  * in-app browser and return via the scheme out-of-band — so we also listen on
>  * `Linking` and dismiss the session when that happens. No extra wiring needed.
>  */
> export async function connectWallet(params: {
>   hostedPageUrl: string;
>   scheme: string;
>   /**
>    * Dynamic environment ID for the hosted page to use instead of its own
>    * default, sent as `?environmentId=<uuid>`. Omit to leave the page on its
>    * default.
>    */
>   environmentId?: string;
> }): Promise<WalletConnection> {
>   const { hostedPageUrl, scheme, environmentId } = params;
>   const redirectUri = `${scheme}://wallet-callback`;
>   const nonce = randomNonce();
>   const url = withParams(hostedPageUrl, {
>     redirect_uri: redirectUri,
>     nonce,
>     embedded: "1",
>     ...(environmentId ? { environmentId } : {}),
>   });
>
>   const callbackUrl = await openAndAwaitReturn(url, redirectUri);
>   return verify(callbackUrl, nonce);
> }
>
> // ── Internals ─────────────────────────────────────────────────────────────
>
> /** Race the auth session against a deep-link listener (out-of-band returns). */
> function openAndAwaitReturn(url: string, redirectUri: string): Promise<string> {
>   return new Promise((resolve, reject) => {
>     let settled = false;
>     const finish = (fn: () => void) => {
>       if (settled) return;
>       settled = true;
>       subscription.remove();
>       fn();
>     };
>
>     const subscription = Linking.addEventListener("url", ({ url: incoming }) => {
>       if (incoming.startsWith(redirectUri)) {
>         WebBrowser.dismissAuthSession(); // close the still-open sheet
>         finish(() => resolve(incoming));
>       }
>     });
>
>     // preferEphemeralSession skips the "<app> wants to use <domain> to sign in"
>     // consent alert (no shared cookies), matching the native iOS behavior.
>     WebBrowser.openAuthSessionAsync(url, redirectUri, { preferEphemeralSession: true })
>       .then((result) => {
>         if (result.type === "success") finish(() => resolve(result.url));
>         else finish(() => reject(new FireblocksConnectCancelled("Cancelled")));
>       })
>       .catch((error) => finish(() => reject(error)));
>   });
> }
>
> function verify(callbackUrl: string, expectedNonce: string): WalletConnection {
>   const { queryParams } = Linking.parse(callbackUrl);
>   const value = (key: string): string => {
>     const v = queryParams?.[key];
>     return typeof v === "string" ? v : "";
>   };
>   if (value("nonce") !== expectedNonce) {
>     throw new FireblocksConnectError("Nonce mismatch"); // possible CSRF — reject
>   }
>   const address = value("address");
>   if (!address) throw new FireblocksConnectError("Malformed result");
>   return {
>     address,
>     chain: value("chain"),
>     walletName: value("walletName"),
>     walletImage: value("walletImage"),
>   };
> }
>
> /** Append params without relying on RN's spotty URL/URLSearchParams support. */
> function withParams(base: string, extra: Record<string, string>): string {
>   const query = Object.entries(extra)
>     .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
>     .join("&");
>   return base + (base.includes("?") ? "&" : "?") + query;
> }
>
> /** Cryptographically-random nonce (CSRF correlation). Not a secret, but must
>  *  be unguessable. */
> function randomNonce(): string {
>   const bytes = Crypto.getRandomBytes(16);
>   return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
> }
> ````

## 1. Install the modules

All three work in Expo and bare React Native.

```sh terminal theme={"system"}
npx expo install expo-web-browser expo-linking expo-crypto
```

## 2. Register your URL scheme

For Expo, set it in `app.json`. For bare React Native, add it to `Info.plist` (`CFBundleURLTypes`) and an Android intent-filter in `AndroidManifest.xml`.

```json app.json theme={"system"}
{ "expo": { "scheme": "myapp" } }
```

## 3. Connect and use the result

One call. The nonce is verified for you, and some wallets (Phantom) return out-of-band and are handled inside `connectWallet` via an `expo-linking` listener. Unlike the raw iOS integration, you do not wire `onOpenURL` yourself.

```tsx ConnectButton.tsx theme={"system"}
import { connectWallet } from "./FireblocksConnect";

try {
  const wallet = await connectWallet({
    hostedPageUrl: "https://connect.dynamicauth.com/",
    scheme: "myapp",
    environmentId: "b1e3aca9-0646-411a-b4ab-c31ce49935b3",
  });
  // wallet.address, wallet.chain,
  // wallet.walletName, wallet.walletImage
} catch (e) {
  // FireblocksConnectCancelled | FireblocksConnectError
}
```

`walletImage` is usually an SVG-sprite URL. Render it in a `react-native-webview` `<img>` rather than `<Image>`.

## Common pitfalls

* **Test on a physical device.** Wallets do not run in the Simulator.
* **Serve over HTTPS.** The flow mints WalletConnect URIs via WebCrypto.
* **Phantom** has no WalletConnect option; the hosted page routes it to its in-app browser and this module catches the return. Validate on device.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.