React Native SDK

Build powerful mobile applications with OAuth authentication, AI API calls, and React hooks - optimized for Expo.

React Native Expo Compatible OAuth2 + PKCE v1.1.0

Step 1: Get Your Client ID

You need a Client ID to use the SDK. Get one for free in 30 seconds:

  1. Go to AI Pass Developer Panel
  2. Sign up or log in
  3. Click "Create OAuth App"
  4. Copy your Client ID
Get Client ID Now

Tip: Your Client ID looks like: aipass_xxxxxxxxxxxx

5-Minute Quick Start

Get a working AI chat app in 5 minutes. Copy and paste these steps:

1. Create new Expo project

npx create-expo-app@latest my-ai-app
cd my-ai-app

2. Install dependencies

npx expo install expo-auth-session expo-crypto expo-linking expo-secure-store expo-web-browser @react-native-async-storage/async-storage buffer

3. Download the SDK

# Create lib folder and download SDK
mkdir -p lib
curl -o lib/AiPassSDK.js https://aipass.one/docs/sdk/reactnative/AiPassSDK.js

Or download manually and save to lib/AiPassSDK.js

4. Configure app.json

Add scheme to your app.json:

{
  "expo": {
    "scheme": "myaiapp"
  }
}

5. Replace App.js with this complete example

import React, { useState } from 'react';
import {
  View, Text, TextInput, Button,
  FlatList, ActivityIndicator, StyleSheet, SafeAreaView
} from 'react-native';
import { AiPassProvider, useAiPass, useAuthState, useBalance, useAiPassEvent } from './lib/AiPassSDK';

// ============================================
// REPLACE WITH YOUR CLIENT ID FROM STEP 1
// ============================================
const CLIENT_ID = 'YOUR_CLIENT_ID_HERE';

function ChatScreen() {
  const { sdk, login, logout, openDashboard } = useAiPass();
  const { isReady, isAuthenticated } = useAuthState();
  const { balance } = useBalance();
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);

  // Handle budget exceeded
  useAiPassEvent('budgetExceeded', ({ message }) => {
    alert('Budget exceeded: ' + message);
  });

  // Loading state
  if (!isReady) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#8a4fff" />
        <Text style={styles.loadingText}>Loading AI Pass...</Text>
      </View>
    );
  }

  // Login screen
  if (!isAuthenticated) {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>AI Chat</Text>
        <Text style={styles.subtitle}>Powered by AI Pass</Text>
        <Button title="Sign in with AI Pass" onPress={login} color="#8a4fff" />
      </View>
    );
  }

  // Send message
  const sendMessage = async () => {
    if (!input.trim() || loading) return;

    const userMessage = { role: 'user', content: input };
    setMessages(prev => [...prev, userMessage]);
    setInput('');
    setLoading(true);

    try {
      const result = await sdk.generateCompletion({
        messages: [...messages, userMessage],
        model: 'gemini/gemini-2.5-flash-lite',
        maxTokens: 1000
      });
      const aiMessage = result.choices[0].message;
      setMessages(prev => [...prev, aiMessage]);
    } catch (error) {
      alert('Error: ' + error.message);
      // Remove the user message if there was an error
      setMessages(prev => prev.slice(0, -1));
    } finally {
      setLoading(false);
    }
  };

  // Chat screen
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.balance}>
          Balance: ${balance?.toFixed(2) || '...'}
        </Text>
        <View style={styles.headerButtons}>
          <Button title="Dashboard" onPress={openDashboard} />
          <Button title="Logout" onPress={logout} color="#ff4444" />
        </View>
      </View>

      <FlatList
        data={messages}
        keyExtractor={(_, i) => i.toString()}
        style={styles.messageList}
        renderItem={({ item }) => (
          <View style={[
            styles.message,
            item.role === 'user' ? styles.userMsg : styles.aiMsg
          ]}>
            <Text style={styles.messageText}>{item.content}</Text>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>Send a message to start chatting!</Text>
        }
      />

      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          value={input}
          onChangeText={setInput}
          placeholder="Type a message..."
          editable={!loading}
          onSubmitEditing={sendMessage}
        />
        <Button
          title={loading ? '...' : 'Send'}
          onPress={sendMessage}
          disabled={loading || !input.trim()}
          color="#8a4fff"
        />
      </View>
    </SafeAreaView>
  );
}

export default function App() {
  return (
    <AiPassProvider config={{ clientId: CLIENT_ID, debug: __DEV__ }}>
      <ChatScreen />
    </AiPassProvider>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  title: { fontSize: 32, fontWeight: 'bold', color: '#8a4fff', marginBottom: 8 },
  subtitle: { fontSize: 16, color: '#666', marginBottom: 24 },
  loadingText: { marginTop: 12, color: '#666' },
  header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  headerButtons: { flexDirection: 'row', gap: 8 },
  balance: { fontSize: 16, fontWeight: '600' },
  messageList: { flex: 1, padding: 16 },
  message: { padding: 12, borderRadius: 12, marginVertical: 4, maxWidth: '80%' },
  userMsg: { backgroundColor: '#8a4fff', alignSelf: 'flex-end' },
  aiMsg: { backgroundColor: '#f0f0f0', alignSelf: 'flex-start' },
  messageText: { fontSize: 16 },
  emptyText: { textAlign: 'center', color: '#999', marginTop: 40 },
  inputRow: { flexDirection: 'row', padding: 16, borderTopWidth: 1, borderTopColor: '#eee', alignItems: 'center' },
  input: { flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 24, paddingHorizontal: 16, paddingVertical: 12, marginRight: 8, fontSize: 16 }
});

6. Run the app

npx expo start

Important: Replace YOUR_CLIENT_ID_HERE with your actual Client ID from Step 1!

Testing Checklist

  • ✅ App loads and shows "Sign in with AI Pass" button
  • ✅ Clicking login opens browser for authentication
  • ✅ After login, chat screen appears with your balance
  • ✅ Can send messages and receive AI responses
  • ✅ Dashboard button opens AI Pass in browser
  • ✅ Logout returns to login screen

Download SDK

Get the AI Pass React Native SDK for your Expo project:

Download AiPassSDK.js

Overview

The AI Pass React Native SDK provides everything you need to integrate AI capabilities into your mobile application:

OAuth2 + PKCE

Secure browser-based authentication with automatic token management

Secure Storage

Tokens stored with expo-secure-store (AsyncStorage fallback)

Auto Refresh

Automatic token refresh with AppState awareness

AI APIs

Chat, image, video, speech, embeddings and more

React Hooks

useAiPass, useAuthState, useBalance, useChat hooks

Streaming Support

Real-time streaming for chat completions

Prerequisites

Required Expo packages:

# Core dependencies
npx expo install expo-auth-session expo-crypto expo-linking expo-secure-store expo-web-browser @react-native-async-storage/async-storage buffer

# Optional: For audio features (TTS, speech-to-text)
npx expo install expo-av expo-file-system

Note: The SDK is designed for Expo projects. For bare React Native, additional configuration may be required.

Installation

1. Download the SDK

Download AiPassSDK.js and add it to your project (e.g., lib/AiPassSDK.js).

2. Configure Deep Linking

Add the scheme to your app.json:

{
  "expo": {
    "scheme": "your-app-scheme",
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp"
    },
    "android": {
      "package": "com.yourcompany.yourapp"
    }
  }
}

3. Wrap Your App with Provider

import { AiPassProvider } from './lib/AiPassSDK';

export default function App() {
  return (
    <AiPassProvider config={{ clientId: 'your_client_id' }}>
      <YourApp />
    </AiPassProvider>
  );
}

Quick Start

Basic Usage with Hooks

import React from 'react';
import { View, Button, Text, ActivityIndicator } from 'react-native';
import { useAiPass, useAuthState, useBalance } from './lib/AiPassSDK';

function HomeScreen() {
  const { login, logout, sdk } = useAiPass();
  const { isReady, isAuthenticated } = useAuthState();
  const { balance } = useBalance();

  if (!isReady) {
    return <ActivityIndicator />;
  }

  if (!isAuthenticated) {
    return (
      <View>
        <Button title="Sign in with AI Pass" onPress={login} />
      </View>
    );
  }

  return (
    <View>
      <Text>Balance: ${balance?.toFixed(2)}</Text>
      <Button title="Generate Text" onPress={async () => {
        const result = await sdk.generateCompletion({
          prompt: 'Hello, AI!',
          model: 'gemini/gemini-2.5-flash-lite'
        });
        console.log(result.choices[0].message.content);
      }} />
      <Button title="Logout" onPress={logout} />
    </View>
  );
}

Direct SDK Usage (Without Provider)

import { AiPass } from './lib/AiPassSDK';

// Initialize once at app startup
await AiPass.initialize({ clientId: 'your_client_id' });

// Check authentication
if (!AiPass.isAuthenticated()) {
  await AiPass.login();
}

// Make API calls
const result = await AiPass.generateCompletion({
  prompt: 'Explain React Native',
  model: 'gemini/gemini-2.5-flash-lite',
  maxTokens: 500
});

Simple Examples

Copy-paste these components into your app. All examples include proper error handling.

1. Login Button

import React from 'react';
import { Button, View, Text } from 'react-native';
import { useAuthState } from './lib/AiPassSDK';

function LoginButton() {
  const { isReady, isAuthenticated, login, logout, error } = useAuthState();

  if (!isReady) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return isAuthenticated
    ? <Button title="Logout" onPress={logout} color="#ff4444" />
    : <Button title="Login with AI Pass" onPress={login} color="#8a4fff" />;
}

2. Balance Display

import React from 'react';
import { Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useBalance } from './lib/AiPassSDK';

function BalanceWidget() {
  const { balance, refreshBalance, isAuthenticated } = useBalance();

  if (!isAuthenticated) return null;

  const isLow = balance !== null && balance < 1;

  return (
    <TouchableOpacity onPress={refreshBalance} style={styles.container}>
      <Text style={[styles.text, isLow && styles.lowBalance]}>
        Balance: ${balance?.toFixed(2) || '...'}
      </Text>
      {isLow && <Text style={styles.warning}>Low balance!</Text>}
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: { padding: 8 },
  text: { fontSize: 16, fontWeight: '600', color: '#333' },
  lowBalance: { color: '#ff4444' },
  warning: { fontSize: 12, color: '#ff4444' }
});

3. Simple Chat (with useChat hook)

import React, { useState } from 'react';
import { View, TextInput, Button, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { useChat } from './lib/AiPassSDK';

function SimpleChat() {
  const { generateCompletion, isLoading, error } = useChat();
  const [input, setInput] = useState('');
  const [response, setResponse] = useState('');

  const askAI = async () => {
    if (!input.trim()) return;
    try {
      const result = await generateCompletion({
        prompt: input,
        model: 'gemini/gemini-2.5-flash-lite'
      });
      setResponse(result.choices[0].message.content);
    } catch (err) {
      setResponse('Error: ' + err.message);
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        value={input}
        onChangeText={setInput}
        placeholder="Ask anything..."
        style={styles.input}
      />
      <Button title={isLoading ? 'Thinking...' : 'Ask'} onPress={askAI} disabled={isLoading} />
      {isLoading && <ActivityIndicator style={styles.loader} />}
      {response ? <Text style={styles.response}>{response}</Text> : null}
      {error && <Text style={styles.error}>{error.message}</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, marginBottom: 12 },
  loader: { marginVertical: 12 },
  response: { marginTop: 12, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8 },
  error: { marginTop: 12, color: '#ff4444' }
});

4. Handle Budget Exceeded

import React from 'react';
import { Alert } from 'react-native';
import { useAiPassEvent, useAiPass } from './lib/AiPassSDK';

function BudgetHandler() {
  const { openDashboard } = useAiPass();

  // This hook auto-subscribes and cleans up
  useAiPassEvent('budgetExceeded', ({ message }) => {
    Alert.alert(
      'Budget Exceeded',
      message,
      [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Add Funds', onPress: () => openDashboard() }
      ]
    );
  });

  return null; // This component just handles events
}

// Usage: Add <BudgetHandler /> anywhere in your app tree

5. Generate Image

import React, { useState } from 'react';
import { View, TextInput, Button, Image, Text, StyleSheet } from 'react-native';
import { useAiPass } from './lib/AiPassSDK';

function ImageGenerator() {
  const { sdk } = useAiPass();
  const [prompt, setPrompt] = useState('');
  const [imageUrl, setImageUrl] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const generate = async () => {
    if (!prompt.trim()) return;
    setLoading(true);
    setError(null);
    try {
      const result = await sdk.generateImage({
        prompt,
        model: 'gpt-image-1',
        size: '1024x1024'
      });
      setImageUrl(result.data[0].url);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        value={prompt}
        onChangeText={setPrompt}
        placeholder="Describe an image..."
        style={styles.input}
      />
      <Button
        title={loading ? 'Generating...' : 'Generate Image'}
        onPress={generate}
        disabled={loading || !prompt.trim()}
      />
      {error && <Text style={styles.error}>{error}</Text>}
      {imageUrl && <Image source={{ uri: imageUrl }} style={styles.image} />}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, marginBottom: 12 },
  image: { width: '100%', height: 300, borderRadius: 8, marginTop: 12 },
  error: { color: '#ff4444', marginTop: 8 }
});

6. Text-to-Speech

Requires: npx expo install expo-av

import React, { useState } from 'react';
import { View, TextInput, Button, Text, StyleSheet } from 'react-native';
import { Audio } from 'expo-av';
import { useAiPass } from './lib/AiPassSDK';

function TextToSpeech() {
  const { sdk } = useAiPass();
  const [text, setText] = useState('Hello from AI Pass!');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const speak = async () => {
    if (!text.trim()) return;
    setLoading(true);
    setError(null);

    try {
      const audioBlob = await sdk.generateSpeech({
        text,
        model: 'tts-1',
        voice: 'nova'
      });

      // Convert blob to base64 and play
      const reader = new FileReader();
      reader.readAsDataURL(audioBlob);
      reader.onloadend = async () => {
        try {
          const base64 = reader.result.split(',')[1];
          const { sound } = await Audio.Sound.createAsync({
            uri: `data:audio/mp3;base64,${base64}`
          });
          await sound.playAsync();
        } catch (playError) {
          setError('Playback error: ' + playError.message);
        }
      };
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        value={text}
        onChangeText={setText}
        placeholder="Enter text to speak..."
        style={styles.input}
        multiline
      />
      <Button
        title={loading ? 'Generating...' : 'Speak'}
        onPress={speak}
        disabled={loading || !text.trim()}
      />
      {error && <Text style={styles.error}>{error}</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 16 },
  input: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, marginBottom: 12, minHeight: 80 },
  error: { color: '#ff4444', marginTop: 8 }
});

Configuration Options

Pass these options to AiPass.initialize() or the AiPassProvider:

Option Type Default Description
clientId string required Your AI Pass client ID
baseUrl string 'https://aipass.one' API base URL
scopes string[] ['api:access', 'profile:read'] OAuth scopes to request
storageKey string 'aipass_oauth_token' Key for token storage
tokenRefreshBuffer number 300000 (5 min) Refresh tokens this many ms before expiry
enableBackgroundRefresh boolean true Enable automatic token refresh
debug boolean false Enable debug logging

React Hooks

useAiPass()

Main hook providing access to SDK instance and common actions.

const {
  sdk,
  isReady,
  isAuthenticated,
  balance,
  error,
  login,
  logout,
  refreshBalance,
  openDashboard,
  showPaymentModal,
  getTokenInfo
} = useAiPass();
  • sdk - The AiPassSDK instance
  • isReady - Whether SDK is initialized
  • isAuthenticated - Whether user is logged in
  • balance - Current user balance
  • error - Initialization error (if any)
  • login() - Start OAuth login flow
  • logout() - Logout and revoke token
  • refreshBalance() - Manually refresh balance
  • openDashboard() - Open AI Pass dashboard in browser
  • showPaymentModal() - Emit payment required event
  • getTokenInfo() - Get token expiration info

useAuthState()

Hook for authentication state with login/logout actions.

const { isReady, isAuthenticated, error, login, logout } = useAuthState();

useBalance()

Hook for tracking user balance.

const { balance, refreshBalance, isAuthenticated, showPaymentModal } = useBalance();

useChat()

Hook for chat completion with loading state management.

const { generateCompletion, isLoading, error, isReady, isAuthenticated } = useChat();

// Usage
const handleSend = async () => {
  const result = await generateCompletion({
    messages: [{ role: 'user', content: 'Hello!' }],
    model: 'gemini/gemini-2.5-flash-lite'
  });
  console.log(result.choices[0].message.content);
};

useAiPassEvent()

Hook for subscribing to SDK events.

import { Alert } from 'react-native';

// Subscribe to budget exceeded events
useAiPassEvent('budgetExceeded', (data) => {
  Alert.alert('Budget Exceeded', data.message);
});

// Subscribe to payment required events
useAiPassEvent('paymentRequired', ({ balance, dashboardUrl }) => {
  navigation.navigate('AddFunds', { balance });
});

API Methods

Authentication & Navigation

Method Description
initialize(config) Initialize the SDK with configuration
login() Start OAuth2 PKCE authentication flow
logout() Logout and revoke tokens
isAuthenticated() Check if user is authenticated
getAccessToken() Get current access token (refreshes if needed)
refreshAccessToken() Manually refresh the access token
getTokenInfo() Get token expiration info
openDashboard() Open AI Pass dashboard in device browser
openDeveloperPanel() Open developer panel in device browser
showPaymentModal(options?) Emit 'paymentRequired' event for your app to handle

Chat Completions

// Simple completion
const result = await sdk.generateCompletion({
  prompt: 'Write a haiku about coding',
  model: 'gemini/gemini-2.5-flash-lite',
  temperature: 0.7,
  maxTokens: 100
});

// With message history
const result = await sdk.generateCompletion({
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: 'Hello!' }
  ],
  model: 'claude/claude-sonnet-4-20250514'
});

// Streaming
for await (const chunk of sdk.generateCompletion({
  prompt: 'Tell me a story',
  stream: true
})) {
  const content = chunk.choices[0]?.delta?.content;
  if (content) console.log(content);
}

Image Generation

// Generate image
const result = await sdk.generateImage({
  prompt: 'A futuristic city at sunset',
  model: 'gpt-image-1',
  size: '1024x1024',
  n: 1
});
console.log(result.data[0].url);

// Edit image (with FormData)
const result = await sdk.editImage({
  image: imageFile,
  prompt: 'Add flying cars',
  model: 'gpt-image-1'
});

Audio

// Text-to-Speech
const audioBlob = await sdk.generateSpeech({
  text: 'Hello, welcome to AI Pass!',
  model: 'tts-1',
  voice: 'alloy',
  responseFormat: 'mp3'
});

// Speech-to-Text
const result = await sdk.transcribeAudio({
  audioFile: audioFile,
  model: 'whisper-1',
  language: 'en'
});

Video Generation

// Generate video
const result = await sdk.generateVideo({
  prompt: 'A rocket launching into space',
  model: 'gemini/veo-3.1-generate-preview',
  size: '1280x720',
  seconds: 4
});

// Check status
const status = await sdk.getVideoStatus(result.id);

// Download when ready
if (status.status === 'completed') {
  const videoBlob = await sdk.downloadVideo(result.id);
}

User & Balance

// Get user info
const userInfo = await sdk.getUserInfo();

// Get balance
const balance = await sdk.getUserBalance();
console.log('Remaining:', balance.data.remainingBudget);

// Get available models
const models = await sdk.getModels();

Events

Subscribe to SDK events for real-time updates:

login logout tokenRefreshed tokenError budgetExceeded balanceUpdated paymentRequired
import { AiPass, useAiPassEvent } from './lib/AiPassSDK';
import { Alert } from 'react-native';

// Option 1: Using the hook (recommended in components)
function MyComponent() {
  useAiPassEvent('budgetExceeded', ({ message }) => {
    Alert.alert('Budget Exceeded', message);
  });

  useAiPassEvent('paymentRequired', ({ balance, dashboardUrl }) => {
    // Navigate to your payment screen
    navigation.navigate('AddFunds', { balance });
  });

  return <YourUI />;
}

// Option 2: Direct subscription
AiPass.on('login', (tokenData) => {
  console.log('User logged in');
});

AiPass.on('logout', ({ reason }) => {
  console.log('User logged out:', reason);
});

AiPass.on('balanceUpdated', ({ balance }) => {
  console.log('New balance:', balance);
});

// Remove listener
const unsubscribe = AiPass.on('tokenRefreshed', () => {});
unsubscribe(); // Call to remove

Complete Example

A full example app with authentication and chat:

// App.js
import React from 'react';
import { AiPassProvider } from './lib/AiPassSDK';
import ChatScreen from './screens/ChatScreen';

export default function App() {
  return (
    <AiPassProvider config={{
      clientId: 'your_client_id',
      debug: __DEV__
    }}>
      <ChatScreen />
    </AiPassProvider>
  );
}

// screens/ChatScreen.js
import React, { useState } from 'react';
import {
  View, Text, TextInput, Button,
  FlatList, ActivityIndicator, StyleSheet, Alert
} from 'react-native';
import { useAiPass, useAuthState, useBalance } from '../lib/AiPassSDK';

export default function ChatScreen() {
  const { sdk, login, logout } = useAiPass();
  const { isReady, isAuthenticated } = useAuthState();
  const { balance } = useBalance();

  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);

  if (!isReady) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (!isAuthenticated) {
    return (
      <View style={styles.center}>
        <Text style={styles.title}>Welcome to AI Chat</Text>
        <Button title="Sign in with AI Pass" onPress={login} />
      </View>
    );
  }

  const sendMessage = async () => {
    if (!input.trim()) return;

    const userMessage = { role: 'user', content: input };
    setMessages(prev => [...prev, userMessage]);
    setInput('');
    setLoading(true);

    try {
      const result = await sdk.generateCompletion({
        messages: [...messages, userMessage],
        model: 'gemini/gemini-2.5-flash-lite',
        maxTokens: 1000
      });

      const aiMessage = result.choices[0].message;
      setMessages(prev => [...prev, aiMessage]);
    } catch (error) {
      Alert.alert('Error', error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text>Balance: ${balance?.toFixed(2) || '...'}</Text>
        <Button title="Logout" onPress={logout} />
      </View>

      <FlatList
        data={messages}
        keyExtractor={(_, i) => i.toString()}
        renderItem={({ item }) => (
          <View style={[
            styles.message,
            item.role === 'user' ? styles.userMsg : styles.aiMsg
          ]}>
            <Text>{item.content}</Text>
          </View>
        )}
      />

      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          value={input}
          onChangeText={setInput}
          placeholder="Type a message..."
          editable={!loading}
        />
        <Button
          title={loading ? '...' : 'Send'}
          onPress={sendMessage}
          disabled={loading}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  title: { fontSize: 24, marginBottom: 20 },
  header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 16 },
  message: { padding: 12, borderRadius: 8, marginVertical: 4, maxWidth: '80%' },
  userMsg: { backgroundColor: '#e3f2fd', alignSelf: 'flex-end' },
  aiMsg: { backgroundColor: '#f5f5f5', alignSelf: 'flex-start' },
  inputRow: { flexDirection: 'row', marginTop: 16 },
  input: { flex: 1, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 12, marginRight: 8 }
});

Troubleshooting

Deep Link Not Working

Make sure your app.json has the correct scheme configured and you've rebuilt the app after changes.

Token Storage Issues

The SDK automatically falls back to AsyncStorage if SecureStore has size limitations (2KB). For large tokens, this is handled automatically.

Expo Go vs Development Build

In Expo Go, deep linking uses exp:// scheme. For production, create a development build with your custom scheme.

Enable Debug Mode

AiPass.initialize({
  clientId: 'your_client_id',
  debug: true  // See detailed logs
});