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.