diff --git a/docs.json b/docs.json
index ea91f3b7..8aff10fa 100644
--- a/docs.json
+++ b/docs.json
@@ -36,189 +36,181 @@
}
},
"navigation": {
- "tabs": [{
- "tab": "Auth",
- "groups": [
- {
- "group": "Overview",
- "pages": [
- "index",
- "overview/pricing",
- "overview/bring-your-app-to-production",
- {
- "group": "Login Options",
- "icon": "arrow-right-to-bracket",
- "pages": [
- "overview/login-options",
- "overview/passkeys"
- ]
- },
- "overview/faqs",
- "overview/authentication-flows",
- "overview/changelog"
- ]
- },
- {
- "group": "AI Prompts",
- "icon": "robot",
- "pages": [
- "ai-prompts/overview",
- "ai-prompts/nextjs",
- "ai-prompts/react",
+ "tabs": [
+ {
+ "tab": "Auth",
+ "groups": [
+ {
+ "group": "Overview",
+ "pages": [
+ "index",
+ "overview/pricing",
+ "overview/bring-your-app-to-production",
+ {
+ "group": "Login Options",
+ "icon": "arrow-right-to-bracket",
+ "pages": ["overview/login-options", "overview/passkeys"]
+ },
+ "overview/faqs",
+ "overview/authentication-flows",
+ "overview/changelog"
+ ]
+ },
+ {
+ "group": "AI Prompts",
+ "icon": "robot",
+ "pages": [
+ "ai-prompts/overview",
+ "ai-prompts/nextjs",
+ "ai-prompts/react",
+ {
+ "group": "Python",
+ "icon": "python",
+ "pages": [
+ "ai-prompts/python/fastapi",
+ "ai-prompts/python/flask",
+ "ai-prompts/python/django"
+ ]
+ },
+ {
+ "group": "Web3",
+ "icon": "wallet",
+ "pages": ["ai-prompts/web3/solana", "ai-prompts/web3/ethereum"]
+ },
+ {
+ "group": "No-code Platforms",
+ "icon": "wand-magic-sparkles",
+ "pages": [
+ "ai-prompts/no-code/lovable",
+ "ai-prompts/no-code/bolt",
+ "ai-prompts/no-code/v0",
+ "ai-prompts/no-code/replit"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Integration",
+ "pages": [
+ "integration/react",
+ "integration/nextjs",
+ "integration/vanillajs",
+ {
+ "group": "Node.JS",
+ "icon": "node-js",
+ "pages": [
+ "integration/nodejs",
+ "integration/nodejs/express",
+ "integration/nodejs/hono",
+ "integration/nodejs/fastify"
+ ]
+ },
+ {
+ "group": "Python",
+ "icon": "python",
+ "pages": [
+ "integration/python",
+ "integration/python/fastapi",
+ "integration/python/flask",
+ "integration/python/django"
+ ]
+ },
+ {
+ "group": "Mobile",
+ "icon": "mobile-screen-button",
+ "pages": ["integration/mobile/react-native"]
+ },
+ "integration/other",
+ "integration/error-codes"
+ ]
+ },
+ {
+ "group": "Web3",
+ "pages": [
+ "web3/embedded-wallets",
+ "web3/ethereum-evm",
+ "web3/solana",
+ {
+ "group": "Mobile",
+ "icon": "mobile",
+ "pages": ["web3/mobile/solana"]
+ }
+ ]
+ },
+ {
+ "group": "Libraries & Tools",
+ "icon": "code",
+ "pages": ["libraries/auth-verify"]
+ },
+ {
+ "group": "Guides",
+ "pages": ["guides/add-auth-to-mcp"]
+ }
+ ],
+ "global": {
+ "anchors": [
{
- "group": "Python",
- "icon": "python",
- "pages": [
- "ai-prompts/python/fastapi",
- "ai-prompts/python/flask",
- "ai-prompts/python/django"
- ]
+ "anchor": "About us",
+ "href": "https://www.civic.com/about",
+ "icon": "circle-info"
},
{
- "group": "Web3",
- "icon": "wallet",
- "pages": [
- "ai-prompts/web3/solana",
- "ai-prompts/web3/ethereum"
- ]
+ "anchor": "Blog",
+ "href": "https://www.civic.com/blog",
+ "icon": "newspaper"
},
{
- "group": "No-code Platforms",
- "icon": "wand-magic-sparkles",
- "pages": [
- "ai-prompts/no-code/lovable",
- "ai-prompts/no-code/bolt",
- "ai-prompts/no-code/v0",
- "ai-prompts/no-code/replit"
- ]
+ "anchor": "Join Our Community",
+ "href": "https://join.slack.com/t/civic-developers/shared_invite/zt-37tv9fyo7-aDT43mUjOFQwdQFmfZLTRw",
+ "icon": "slack"
}
]
- },
- {
- "group": "Integration",
- "pages": [
- "integration/react",
- "integration/nextjs",
- "integration/vanillajs",
- {
- "group": "Node.JS",
- "icon": "node-js",
- "pages": [
- "integration/nodejs",
- "integration/nodejs/express",
- "integration/nodejs/hono",
- "integration/nodejs/fastify"
- ]
- },
- {
- "group": "Python",
- "icon": "python",
- "pages": [
- "integration/python",
- "integration/python/fastapi",
- "integration/python/flask",
- "integration/python/django"
- ]
- },
- {
- "group": "Mobile",
- "icon": "mobile-screen-button",
- "pages": [
- "integration/mobile/react-native"
- ]
- },
- "integration/other",
- "integration/error-codes"
- ]
- },
- {
- "group": "Web3",
- "pages": [
- "web3/embedded-wallets",
- "web3/ethereum-evm",
- "web3/solana"
- ]
- },
- {
- "group": "Libraries & Tools",
- "icon": "code",
- "pages": [
- "libraries/auth-verify"
- ]
- },
- {
- "group": "Guides",
- "pages": [
- "guides/add-auth-to-mcp"
- ]
}
- ],
- "global": {
- "anchors": [
+ },
+ {
+ "tab": "Labs",
+ "groups": [
+ {
+ "group": " ",
+ "pages": ["labs", "labs/flask-status", "labs/feedback"]
+ },
{
- "anchor": "About us",
- "href": "https://www.civic.com/about",
- "icon": "circle-info"
+ "group": "Flasks",
+ "pages": [
+ "labs/projects/mcp-hub",
+ "labs/projects/x402-mcp",
+ "labs/projects/guardrail-proxy",
+ "labs/projects/bodyguard",
+ "labs/projects/passthrough-proxy",
+ "labs/projects/civic-knowledge"
+ ]
},
{
- "anchor": "Blog",
- "href": "https://www.civic.com/blog",
- "icon": "newspaper"
+ "group": "Concepts",
+ "pages": [
+ "labs/concepts/mcp",
+ "labs/concepts/guardrails",
+ "labs/concepts/prompt-injection",
+ "labs/concepts/auth-strategies",
+ "labs/concepts/hooks",
+ "labs/concepts/rag"
+ ]
},
{
- "anchor": "Join Our Community",
- "href": "https://join.slack.com/t/civic-developers/shared_invite/zt-37tv9fyo7-aDT43mUjOFQwdQFmfZLTRw",
- "icon": "slack"
+ "group": "🔓Integration",
+ "pages": [
+ "labs/private/getting-started",
+ "labs/private/mcp-hub",
+ "labs/private/x402-mcp",
+ "labs/private/guardrail-proxy",
+ "labs/private/bodyguard",
+ "labs/private/passthrough-proxy",
+ "labs/private/civic-knowledge"
+ ]
}
]
}
- },{
- "tab": "Labs",
- "groups": [
- {
- "group": " ",
- "pages": [
- "labs",
- "labs/flask-status",
- "labs/feedback"
- ]
- },
- {
- "group": "Flasks",
- "pages": [
- "labs/projects/mcp-hub",
- "labs/projects/x402-mcp",
- "labs/projects/guardrail-proxy",
- "labs/projects/bodyguard",
- "labs/projects/passthrough-proxy",
- "labs/projects/civic-knowledge"
- ]
- },
- {
- "group": "Concepts",
- "pages": [
- "labs/concepts/mcp",
- "labs/concepts/guardrails",
- "labs/concepts/prompt-injection",
- "labs/concepts/auth-strategies",
- "labs/concepts/hooks",
- "labs/concepts/rag"
- ]
- },
- {
- "group": "🔓Integration",
- "pages": [
- "labs/private/getting-started",
- "labs/private/mcp-hub",
- "labs/private/x402-mcp",
- "labs/private/guardrail-proxy",
- "labs/private/bodyguard",
- "labs/private/passthrough-proxy",
- "labs/private/civic-knowledge"
- ]
- }
- ]
- }]
+ ]
},
"logo": {
"light": "/logo/light.png",
diff --git a/web3/mobile/solana.mdx b/web3/mobile/solana.mdx
new file mode 100644
index 00000000..74d3c972
--- /dev/null
+++ b/web3/mobile/solana.mdx
@@ -0,0 +1,477 @@
+---
+title: "Mobile Solana"
+icon: "code"
+public: true
+---
+
+
+ The Civic Auth Solana API for React Native is currently in early access and
+ subject to change.
+
+
+## Getting Started
+
+After authenticating a user with Civic Auth, you can create a Web3 wallet for them 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 ```
+
+
+ ```bash yarn add @civic/react-native-auth-web3 @solana/web3.js ```
+
+
+ ```bash pnpm add @civic/react-native-auth-web3 @solana/web3.js ```
+
+
+
+### Native Setup
+
+#### Android Configuration
+
+Add the following to your `android/app/build.gradle`:
+
+```gradle
+android {
+ defaultConfig {
+ minSdkVersion 26 // Required minimum SDK version
+
+ // Add manifest placeholders for embedded wallet integration
+ manifestPlaceholders = [
+ metakeepDomain: "*.auth.metakeep.xyz",
+ metakeepScheme:
+ ]
+ }
+}
+```
+
+#### iOS Configuration
+
+**Requirements:**
+
+- iOS 14.0 or higher
+- Xcode 14.0 or higher
+- Swift 5.0 or higher
+
+**Step 1: 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 2: Handle Callback URLs**
+
+Add the following code to your `ios//AppDelegate.swift`:
+
+```swift
+public override func application(
+ _ app: UIApplication,
+ open url: URL,
+ options: [UIApplication.OpenURLOptionsKey: Any] = [:]
+) -> Bool {
+
+ ...
+
+ if url.absoluteString.lowercased().contains("metakeep") {
+ MetaKeep.companion.resume(url: url.absoluteString)
+ return true
+ }
+
+ ...
+
+}
+```
+
+### 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
+
+```tsx
+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
+
+```typescript
+interface Web3Client {
+ solana: SolanaWeb3Client; // Solana wallet operations
+ connected: boolean; // Connection status
+ createWallets(): Promise; // Create embedded wallets
+ disconnect(): Promise; // Disconnect and cleanup
+}
+```
+
+### SolanaWeb3Client Methods
+
+The `solana` property provides access to Solana-specific operations:
+
+```typescript
+interface SolanaWeb3Client {
+ readonly address: string; // Wallet public key
+
+ // Core transaction methods
+ sendTransaction(address: string, amount: number): Promise;
+ signTransaction(transaction: Transaction, reason: string): Promise;
+ signMessage(message: string, reason: string): Promise;
+
+ // Utility methods
+ getBalance(): Promise;
+ disconnect(): Promise;
+}
+```
+
+## Using the Wallet
+
+### Sending Transactions
+
+You have two options for sending transactions:
+
+#### Option 1: Simple Transfer (Recommended)
+
+Use `sendTransaction` for quick SOL transfers. It handles transaction creation, signing, and broadcasting:
+
+```tsx
+// 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:
+
+```tsx
+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
+
+```tsx
+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
+
+```tsx
+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:
+
+```tsx
+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;
+ signOut?: () => Promise;
+ web3Client?: Web3Client | null | undefined;
+};
+
+export const AuthContext = createContext({
+ state: initialState,
+});
+
+// This is needed to close the webview after a complete login
+WebBrowser.maybeCompleteAuthSession();
+
+export const AuthProvider = ({
+ config,
+ children,
+}: {
+ config?: Partial;
+ 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 (
+ {children}
+ );
+};
+```
+
+## 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](https://docs.expo.dev/versions/latest/sdk/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.