> ## Documentation Index
> Fetch the complete documentation index at: https://docs.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.
</Note>

On React Native the [hosted connect page](/docs/connections/overview) 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).
</Tip>

<AccordionGroup>
  <Accordion title="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("");
    }
    ````
  </Accordion>
</AccordionGroup>

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