Skip to main content
The Civic Auth Web3 API for React Native is currently in early access and subject to change.

Overview

For apps requiring embedded wallet functionality with blockchain interactions. This approach includes everything from Authentication Only plus embedded Solana wallet creation and blockchain operations. What you get:
  • Everything from Authentication Only
  • Embedded Solana wallet creation
  • Transaction signing and sending
  • Balance checking and message signing
  • Non-custodial wallet management

Prerequisites

1. Complete Installation

Before starting, complete the Installation & Setup guide.

2. Enable Web3 Wallets in Civic Dashboard

Critical Step: You must enable embedded wallets for your application in the Civic Auth dashboard:
  1. Go to Civic Auth Dashboard
  2. Select your application
  3. Navigate to Settings or Features section
  4. Enable “Embedded Wallets” or “Web3 Wallets” feature
  5. Save your changes
Without enabling wallets in the dashboard, wallet creation will fail even with correct code implementation.

3. Register Your App Domain/Scheme

Critical Step: Register your mobile app scheme with Civic Auth:
  1. In the Civic Auth dashboard, go to your app settings
  2. Find the “Allowed Origins” or “Redirect URIs” section
  3. Add your mobile app scheme (e.g., myapp:// or yourapp://)
  4. Make sure it matches exactly with your app configuration
Example Domain Registration:
  • If your app scheme is myapp://, register: myapp://
  • If your app scheme is com.yourcompany.yourapp://, register: com.yourcompany.yourapp://
Your domain registration must match exactly with the scheme configured in your app’s app.json or app.config.js file.

Getting Started

After completing the prerequisites above, you can create a Web3 wallet for authenticated users using the useWeb3Client hook. Embedded wallets are generated on behalf of users through our non-custodial wallet partner—neither Civic nor your app has access to the private keys.
Only embedded wallets are supported (no self-custodial wallet connections yet)

Installation

Install the SDK and its peer dependencies:
bash npm install @civic/react-native-auth-web3 @solana/web3.js

Native Setup

Android Configuration

Add the following to your android/app/build.gradle:
android {
    defaultConfig {
        minSdkVersion 26  // Required minimum SDK version

        // Add manifest placeholders for embedded wallet integration
        manifestPlaceholders = [
            metakeepDomain: "*.auth.metakeep.xyz",
            metakeepScheme: <YourAppSchema>
        ]
    }
}

iOS Configuration

Requirements:
  • iOS 14.0 or higher
  • Xcode 14.0 or higher
  • Swift 5.0 or higher
Step 1: Generate Native Code If you’re using Expo managed workflow, first generate the native iOS code:
npx expo prebuild
This creates the native iOS project files needed for the next steps.
Step 2: Add URL Type
  1. Navigate to the Info tab of your app target settings in Xcode
  2. In the URL Types section, click the button to add a new URL
  3. Enter the following values:
    • Identifier: metakeep
    • URL Schemes: $(PRODUCT_BUNDLE_IDENTIFIER)
Step 3: Import MetaKeep Framework Add the MetaKeep import to your ios/<YourAppName>/AppDelegate.swift file:
import MetaKeep
Step 4: Add MetaKeep SDK to iOS Dependencies Add the MetaKeep pod to your ios/Podfile:
target 'YourAppName' do
  use_expo_modules!
  
  # MetaKeep SDK
  pod 'MetaKeep', '~> 2.0.0'
  
  # ... other pods
end
Install the pod dependencies:
cd ios && pod install
If you encounter a “no such module ‘MetaKeep’” error, this step resolves it by adding the MetaKeep SDK to your iOS project dependencies.
Step 5: Handle Callback URLs Add the MetaKeep URL handling code inside the existing application(_:open:options:) method in your ios/<YourAppName>/AppDelegate.swift file:
public override func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
    
    // Add this MetaKeep handling code inside the existing method
    if url.absoluteString.lowercased().contains("metakeep") {
        MetaKeep.companion.resume(url: url.absoluteString)
        return true
    }
    
    // Keep any existing URL handling code here
    // ... existing code ...
    
    return false
}
Make sure to add the MetaKeep code inside the existing application(_:open:options:) method, not as a separate method. The file path will be ios/<YourAppName>/AppDelegate.swift after running expo prebuild.

How Wallet Creation Works

After authenticating with Civic Auth, the SDK can create a non-custodial embedded wallet for the user. The creation process requires an ID token - a JWT received from Civic Auth after successful authentication. This token proves the user’s identity and links the wallet to their Civic account. Without a valid ID token, wallet creation will fail. This ensures only authenticated users can create wallets, with each wallet uniquely tied to a specific user account.

Quick Start

import { useWeb3Client } from "@civic/react-native-auth-web3";

const web3Config = {
  solana: {
    endpoint: "https://api.devnet.solana.com", // Your RPC endpoint
  },
};

// Initialize the Web3 client with user's ID token
const web3Client = useWeb3Client(web3Config, idToken);

// Create wallets after login
if (!web3Client?.solana) {
  await web3Client?.createWallets();
}

The useWeb3Client Hook

The useWeb3Client hook returns a Web3Client object for interacting with blockchain networks. The client manages both wallet creation and transaction operations.

Web3Client Interface

interface Web3Client {
  solana: SolanaWeb3Client; // Solana wallet operations
  connected: boolean; // Connection status
  createWallets(): Promise<Wallets | null>; // Create embedded wallets
  disconnect(): Promise<void>; // Disconnect and cleanup
}

SolanaWeb3Client Methods

The solana property provides access to Solana-specific operations:
interface SolanaWeb3Client {
  readonly address: string; // Wallet public key

  // Core transaction methods
  sendTransaction(address: string, amount: number): Promise<string>;
  signTransaction(transaction: Transaction, reason: string): Promise<Buffer>;
  signMessage(message: string, reason: string): Promise<string>;

  // Utility methods
  getBalance(): Promise<number | undefined>;
  disconnect(): Promise<void>;
}

Using the Wallet

Sending Transactions

You have two options for sending transactions: Use sendTransaction for quick SOL transfers. It handles transaction creation, signing, and broadcasting:
// Send 0.5 SOL to a recipient
const txHash = await web3Client?.solana?.sendTransaction(
  "RecipientPublicKeyHere",
  0.5, // Amount in SOL
);
console.log(`Transaction: ${txHash}`);

Option 2: Custom Transactions

Use signTransaction for complex transactions with custom instructions:
import {
  Connection,
  Transaction,
  SystemProgram,
  PublicKey,
} from "@solana/web3.js";

const connection = new Connection(web3Config.solana.endpoint);

// Build custom transaction
const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(web3Client.solana.address),
    toPubkey: new PublicKey(recipientAddress),
    lamports: 0.001 * 1e9, // 0.001 SOL in lamports
  }),
);

// Sign the transaction
const signature = await web3Client?.solana?.signTransaction(
  transaction,
  "Approve transfer", // Reason shown to user
);

// Add signature and send
transaction.addSignature(new PublicKey(web3Client.solana.address), signature);
const txHash = await connection.sendRawTransaction(transaction.serialize());

Checking Balance

import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

const connection = new Connection(web3Config.solana.endpoint);
const balanceLamports = await connection.getBalance(
  new PublicKey(web3Client.solana.address),
);
const balanceSOL = balanceLamports / LAMPORTS_PER_SOL;
console.log(`Balance: ${balanceSOL} SOL`);

Signing Messages

const message = "Verify wallet ownership";
const signature = await web3Client?.solana?.signMessage(
  message,
  "Sign to verify your wallet", // Shown to user
);
console.log(`Signature: ${signature}`);

Complete Example

Here’s a complete authentication provider with Expo Auth Session and Web3 wallet creation:
import { createContext, useEffect, useMemo, useReducer } from "react";
import { AuthRequestConfig, useAuthRequest } from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";
import { civicAuthConfig } from "@/config/civicAuth";
import { useWeb3Client, type Web3Client } from "@civic/react-native-auth-web3";
import { clusterApiUrl } from "@solana/web3.js";
import { CivicWeb3ClientConfig } from "@civic/react-native-auth-web3/dist/types";

interface AuthState {
  isLoading: boolean;
  isAuthenticated: boolean;
  user?: AuthUser;
  accessToken?: string;
  refreshToken?: string;
  idToken?: string;
  expiresIn?: number;
}

interface AuthUser {
  email?: string;
  name: string;
  picture?: string;
  sub: string;
}

interface AuthAction {
  type: string;
  payload?: any;
}

const initialState: AuthState = {
  isLoading: false,
  isAuthenticated: false,
};

export type AuthContextType = {
  state: AuthState;
  signIn?: () => Promise<void>;
  signOut?: () => Promise<void>;
  web3Client?: Web3Client | null | undefined;
};

export const AuthContext = createContext<AuthContextType>({
  state: initialState,
});

// This is needed to close the webview after a complete login
WebBrowser.maybeCompleteAuthSession();

export const AuthProvider = ({
  config,
  children,
}: {
  config?: Partial<AuthRequestConfig>;
  children: React.ReactNode;
}) => {
  const finalConfig = useMemo(() => {
    return { ...civicAuthConfig, ...config };
  }, [config]);

  const [request, response, promptAsync] = useAuthRequest(
    {
      clientId: finalConfig.clientId,
      scopes: finalConfig.scopes,
      redirectUri: finalConfig.redirectUri,
      usePKCE: true,
    },
    {
      authorizationEndpoint: finalConfig.authorizationEndpoint,
      tokenEndpoint: finalConfig.tokenEndpoint,
    },
  );

  const [authState, dispatch] = useReducer(
    (previousState: AuthState, action: AuthAction): AuthState => {
      switch (action.type) {
        case "SIGN_IN":
          return {
            ...previousState,
            isAuthenticated: true,
            accessToken: action.payload.access_token,
            idToken: action.payload.id_token,
            expiresIn: action.payload.expires_in,
          };
        case "USER_INFO":
          return {
            ...previousState,
            user: action.payload,
          };
        case "SIGN_OUT":
          return initialState;
        default:
          return previousState;
      }
    },
    initialState,
  );

  const web3Config = useMemo(
    () =>
      ({
        solana: {
          endpoint: clusterApiUrl("devnet"),
        },
      }) as CivicWeb3ClientConfig,
    [],
  );

  const web3Client = useWeb3Client(web3Config, authState.idToken);

  const authContext = useMemo(
    () => ({
      state: authState,
      web3Client,
      signIn: async () => {
        promptAsync();
      },
      signOut: async () => {
        if (!authState.idToken) {
          throw new Error("No idToken found");
        }
        try {
          const endSessionUrl = new URL(finalConfig.endSessionEndpoint);
          endSessionUrl.searchParams.append("client_id", finalConfig.clientId);
          endSessionUrl.searchParams.append("id_token_hint", authState.idToken);
          endSessionUrl.searchParams.append(
            "post_logout_redirect_uri",
            finalConfig.redirectUri,
          );

          const result = await WebBrowser.openAuthSessionAsync(
            endSessionUrl.toString(),
            finalConfig.redirectUri,
          );

          // Only sign out if the session was completed successfully
          // If the user cancels (result.type === 'cancel'), we don't sign them out
          if (result.type === "success") {
            dispatch({ type: "SIGN_OUT" });
          }
        } catch (e) {
          console.warn(e);
        }
      },
    }),
    [authState, web3Client, promptAsync, finalConfig],
  );

  useEffect(() => {
    const getToken = async ({
      code,
      codeVerifier,
      redirectUri,
    }: {
      code: string;
      redirectUri: string;
      codeVerifier?: string;
    }) => {
      try {
        const response = await fetch(finalConfig.tokenEndpoint, {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/x-www-form-urlencoded",
          },
          body: new URLSearchParams({
            grant_type: "authorization_code",
            client_id: finalConfig.clientId,
            code,
            code_verifier: codeVerifier || "",
            redirect_uri: redirectUri,
          }).toString(),
        });
        if (response.ok) {
          const payload = await response.json();
          dispatch({ type: "SIGN_IN", payload });
        }
      } catch (e) {
        console.warn(e);
      }
    };
    if (response?.type === "success") {
      const { code } = response.params;
      getToken({
        code,
        codeVerifier: request?.codeVerifier,
        redirectUri: finalConfig.redirectUri || "",
      });
    } else if (response?.type === "error") {
      console.warn("Authentication error: ", response.error);
    }
  }, [dispatch, finalConfig, request?.codeVerifier, response]);

  useEffect(() => {
    const initializeUser = async () => {
      // Fetch user info
      try {
        const response = await fetch(finalConfig.userInfoEndpoint || "", {
          headers: { Authorization: `Bearer ${authState.accessToken}` },
        });
        if (response.ok) {
          const payload = await response.json();
          dispatch({ type: "USER_INFO", payload });
        }
      } catch (e) {
        console.warn("Failed to fetch user info:", e);
      }

      // Create wallets if needed
      if (!web3Client?.solana) {
        await web3Client?.createWallets();
      }
    };

    if (authState.isAuthenticated) {
      initializeUser();
    }
  }, [
    authState.isAuthenticated,
    authState.accessToken,
    finalConfig.userInfoEndpoint,
    web3Client,
  ]);

  return (
    <AuthContext.Provider value={authContext}>{children}</AuthContext.Provider>
  );
};

Troubleshooting

Common Domain Registration Issues

If wallet creation fails, check these common domain registration problems:

❌ Domain Mismatch

Problem: Your registered domain doesn’t match your app scheme exactly. Example:
  • App scheme: myapp://
  • Registered domain: myapp (missing ://)
  • Result: Wallet creation fails
Solution: Ensure exact match including ://

❌ Missing Wallet Feature

Problem: Embedded wallets not enabled in Civic dashboard. Symptoms:
  • Authentication works fine
  • createWallets() fails or returns null
  • Console errors about wallet features
Solution: Enable “Embedded Wallets” feature in dashboard settings

❌ Incorrect Redirect URI

Problem: Using hardcoded redirect URI instead of makeRedirectUri() Example:
// ❌ Wrong - hardcoded
redirectUri: "myapp://auth"

// ✅ Correct - dynamic
redirectUri: makeRedirectUri({ scheme: "myapp" })

Verifying Your Setup

To verify your domain registration is correct:
  1. Check your app.json/app.config.js scheme matches registered domain
  2. Test authentication first (should work without wallets)
  3. Only then test wallet creation
  4. Check browser developer tools for any CORS or domain errors

Example Repository

For a complete working example of Civic Auth with embedded wallets in a React Native application, check out our example repository: [https://github.com/civicteam/civic-auth-examples/tree/main/packages/mobile/react-native-expo]

Crypto Polyfill

The SDK automatically includes a crypto polyfill using expo-crypto to provide the getRandomValues function required by Solana’s PublicKey object. This polyfill ensures cryptographic operations work correctly in React Native environments where the Web Crypto API is not natively available. The polyfill is applied automatically when you import the SDK, so no additional configuration is needed.