diff --git a/mobile/README.md b/mobile/README.md
new file mode 100644
index 0000000..e909c54
--- /dev/null
+++ b/mobile/README.md
@@ -0,0 +1,61 @@
+# Tera Mobile
+
+Tera Mobile is the Android-first Expo app for TeraAI, an AI learning companion for learning deeply, researching clearly, and turning knowledge into action.
+
+This app is not a web wrapper. It is the mobile foundation for a standalone Play Store product with a chat-first home, onboarding, auth flow, history, saved work, profile settings, and typed boundaries for real backend integration.
+
+## Stack
+
+- Expo React Native with TypeScript
+- Expo Router for file-based navigation
+- TanStack Query for server-state orchestration
+- Zustand for small client state such as onboarding, mode, session, and preferences
+- Expo SecureStore for sensitive session storage
+- AsyncStorage for non-sensitive app state
+- Zod for form validation
+- Typed `fetch` boundary prepared for a real API
+
+NativeWind is intentionally not included in this foundation. The app uses a typed theme and reusable primitives so the styling system stays small, explicit, and easy to scale.
+
+## Project Structure
+
+```text
+mobile/
+ app/ Expo Router routes and route groups
+ components/ui/ Reusable UI primitives
+ constants/ Theme, spacing, typography, layout constants
+ docs/ Product and engineering docs
+ features/ Feature-oriented UI, hooks, schemas, and data
+ hooks/ Shared app hooks
+ lib/ API and storage boundaries
+ store/ Zustand client state
+ types/ Shared domain types
+ assets/ Expo icons and images
+```
+
+## Run Locally
+
+```powershell
+cd C:\Users\Hp\Documents\Github\Tera\mobile
+pnpm install
+pnpm start
+```
+
+Press `a` in the Expo terminal to open Android, or scan the QR code with Expo Go.
+
+## Current Scope
+
+This foundation includes:
+
+- Onboarding for the TeraAI value proposition
+- Mock sign in, sign up, and forgot password screens
+- Chat-first home with Learn, Research, and Build modes
+- Conversation detail screen with streaming-ready state shape
+- History and saved screens backed by typed mock data
+- Profile/settings screen with preferences and sign out
+- Reusable design primitives and a calm Android-first theme
+- Typed API, storage, and session boundaries
+
+## Roadmap Summary
+
+Next phases should connect real authentication, stream AI responses, sync conversations, add voice input, support file/image upload, introduce push notifications, and prepare subscriptions and Play Store release workflows.
diff --git a/mobile/app.json b/mobile/app.json
index b7ec591..f841f45 100644
--- a/mobile/app.json
+++ b/mobile/app.json
@@ -17,19 +17,12 @@
"ios": {
"supportsTabletMode": true,
"bundleIdentifier": "com.teraai.app",
- "infoPlist": {
- "NSMicrophoneUsageDescription": "Tera needs access to your microphone for voice input",
- "NSCameraRollUsageDescription": "Tera needs access to your photos for uploads",
- "NSPhotoLibraryUsageDescription": "Tera needs access to your photo library"
- }
+ "infoPlist": {}
},
"android": {
"package": "com.teraai.app",
"versionCode": 1,
"permissions": [
- "RECORD_AUDIO",
- "READ_EXTERNAL_STORAGE",
- "WRITE_EXTERNAL_STORAGE",
"INTERNET",
"ACCESS_NETWORK_STATE"
],
@@ -47,4 +40,4 @@
],
"scheme": "tera"
}
-}
\ No newline at end of file
+}
diff --git a/mobile/app/(app)/_layout.tsx b/mobile/app/(app)/_layout.tsx
deleted file mode 100644
index add01a6..0000000
--- a/mobile/app/(app)/_layout.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Tabs } from 'expo-router';
-import { StyleSheet } from 'react-native';
-
-export default function AppLayout() {
- return (
-
-
-
-
-
- );
-}
diff --git a/mobile/app/(app)/chat.tsx b/mobile/app/(app)/chat.tsx
deleted file mode 100644
index da40238..0000000
--- a/mobile/app/(app)/chat.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react';
-import {
- View,
- FlatList,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- ActivityIndicator,
- KeyboardAvoidingView,
- Platform,
- SafeAreaView,
- Text,
-} from 'react-native';
-import { useFocusEffect } from 'expo-router';
-import * as SecureStore from 'expo-secure-store';
-import { teraAPI } from '@/lib/api';
-import { saveMessage, getMessages, saveSession } from '@/lib/storage';
-import ChatBubble from '@/components/ChatBubble';
-
-interface Message {
- id: string;
- role: 'user' | 'assistant';
- content: string;
- timestamp: number;
-}
-
-export default function ChatScreen() {
- const [messages, setMessages] = useState([]);
- const [inputText, setInputText] = useState('');
- const [loading, setLoading] = useState(false);
- const [sessionId, setSessionId] = useState('');
- const [initialized, setInitialized] = useState(false);
- const flatListRef = useRef(null);
-
- useFocusEffect(
- React.useCallback(() => {
- if (!initialized) {
- initializeChat();
- }
- }, [initialized])
- );
-
- const initializeChat = async () => {
- try {
- const userId = await SecureStore.getItemAsync('user_id');
-
- if (!userId) {
- console.error('No user ID found');
- return;
- }
-
- // Create new session
- const session = await teraAPI.createSession('New Chat');
-
- if (session.success && session.data) {
- const newSessionId = session.data[0]?.id || Math.random().toString();
- setSessionId(newSessionId);
-
- // Save session locally
- await saveSession({
- id: newSessionId,
- title: 'New Chat',
- createdAt: Date.now(),
- updatedAt: Date.now(),
- });
-
- setMessages([]);
- setInitialized(true);
- }
- } catch (error) {
- console.error('Failed to initialize chat:', error);
- setInitialized(true);
- }
- };
-
- const handleSendMessage = async () => {
- if (!inputText.trim() || !sessionId) return;
-
- const userMessage = inputText.trim();
- setInputText('');
-
- // Create message object
- const messageId = Math.random().toString();
- const userMsg: Message = {
- id: messageId,
- role: 'user',
- content: userMessage,
- timestamp: Date.now(),
- };
-
- // Add to UI immediately
- setMessages(prev => [...prev, userMsg]);
-
- // Save to local storage
- await saveMessage(sessionId, userMsg);
-
- // Scroll to bottom
- setTimeout(() => {
- flatListRef.current?.scrollToEnd({ animated: true });
- }, 100);
-
- try {
- setLoading(true);
-
- // Send to API
- const response = await teraAPI.sendMessage(
- sessionId,
- userMessage,
- messages.map(m => ({
- role: m.role,
- content: m.content,
- }))
- );
-
- if (response.success && response.data?.message) {
- const assistantMsg: Message = {
- id: Math.random().toString(),
- role: 'assistant',
- content: response.data.message,
- timestamp: Date.now(),
- };
-
- setMessages(prev => [...prev, assistantMsg]);
-
- // Save to local storage
- await saveMessage(sessionId, assistantMsg);
-
- setTimeout(() => {
- flatListRef.current?.scrollToEnd({ animated: true });
- }, 100);
- } else {
- // Show error message
- const errorMsg: Message = {
- id: Math.random().toString(),
- role: 'assistant',
- content: 'Sorry, I could not process your message. Please try again.',
- timestamp: Date.now(),
- };
- setMessages(prev => [...prev, errorMsg]);
- }
- } catch (error) {
- console.error('Failed to send message:', error);
-
- const errorMsg: Message = {
- id: Math.random().toString(),
- role: 'assistant',
- content: 'Connection error. Please check your internet and try again.',
- timestamp: Date.now(),
- };
- setMessages(prev => [...prev, errorMsg]);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
- {messages.length === 0 && !initialized ? (
-
-
- Starting chat...
-
- ) : messages.length === 0 ? (
-
- Start a conversation
-
- Ask me anything about learning or teaching
-
-
- ) : (
- (
-
- )}
- keyExtractor={item => item.id}
- contentContainerStyle={styles.messages}
- scrollEnabled={true}
- onContentSizeChange={() => {
- flatListRef.current?.scrollToEnd({ animated: true });
- }}
- />
- )}
-
-
-
-
- {loading ? (
-
- ) : (
- Send
- )}
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- messages: {
- paddingVertical: 12,
- },
- loadingContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- loadingText: {
- color: '#fff',
- marginTop: 12,
- fontSize: 14,
- },
- emptyContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingHorizontal: 24,
- },
- emptyTitle: {
- fontSize: 24,
- fontWeight: '600',
- color: '#fff',
- marginBottom: 8,
- textAlign: 'center',
- },
- emptySubtitle: {
- fontSize: 14,
- color: '#999',
- textAlign: 'center',
- },
- inputContainer: {
- flexDirection: 'row',
- paddingHorizontal: 12,
- paddingVertical: 12,
- backgroundColor: '#0a0a0a',
- borderTopColor: '#222',
- borderTopWidth: 1,
- alignItems: 'flex-end',
- gap: 8,
- },
- input: {
- flex: 1,
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 20,
- paddingHorizontal: 16,
- paddingVertical: 10,
- color: '#fff',
- fontSize: 16,
- maxHeight: 100,
- },
- sendButton: {
- backgroundColor: '#00d4ff',
- width: 44,
- height: 44,
- borderRadius: 22,
- justifyContent: 'center',
- alignItems: 'center',
- },
- sendButtonDisabled: {
- opacity: 0.5,
- },
- sendButtonText: {
- color: '#000',
- fontWeight: '700',
- fontSize: 18,
- },
-});
diff --git a/mobile/app/(app)/settings.tsx b/mobile/app/(app)/settings.tsx
deleted file mode 100644
index 53f7d64..0000000
--- a/mobile/app/(app)/settings.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- View,
- Text,
- TouchableOpacity,
- StyleSheet,
- SafeAreaView,
- ScrollView,
- Alert,
-} from 'react-native';
-import * as SecureStore from 'expo-secure-store';
-import { router } from 'expo-router';
-import { getUser, clearAllData } from '@/lib/storage';
-
-interface User {
- id: string;
- email: string;
- name: string;
- provider: string;
-}
-
-export default function SettingsScreen() {
- const [user, setUser] = useState(null);
-
- useEffect(() => {
- loadUser();
- }, []);
-
- const loadUser = async () => {
- const userData = await getUser();
- setUser(userData);
- };
-
- const handleSignOut = async () => {
- Alert.alert(
- 'Sign Out',
- 'Are you sure you want to sign out?',
- [
- {
- text: 'Cancel',
- onPress: () => { },
- style: 'cancel',
- },
- {
- text: 'Sign Out',
- onPress: async () => {
- try {
- // Clear secure storage
- await SecureStore.deleteItemAsync('auth_token');
- await SecureStore.deleteItemAsync('user_id');
-
- // Clear local data
- await clearAllData();
-
- // Navigate to sign in
- router.replace('/(auth)/signin');
- } catch (error) {
- console.error('Error signing out:', error);
- Alert.alert('Error', 'Failed to sign out');
- }
- },
- style: 'destructive',
- },
- ]
- );
- };
-
- return (
-
-
- {user && (
-
- Account
-
-
- Name
- {user.name}
-
-
- Email
- {user.email}
-
-
- Provider
- {user.provider}
-
-
-
- )}
-
-
- App Version
- Tera Mobile 1.0.0
-
-
-
- About
-
- Tera is your AI Learning Companion for anything — with unlimited free conversations.
- Upgrade for more file uploads, web searches, and advanced features.
-
-
-
-
-
- Sign Out
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- scrollContent: {
- paddingHorizontal: 16,
- paddingVertical: 20,
- gap: 24,
- },
- section: {
- gap: 12,
- },
- sectionTitle: {
- fontSize: 14,
- fontWeight: '700',
- color: '#00d4ff',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- accountInfo: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- padding: 16,
- gap: 16,
- },
- accountField: {
- gap: 4,
- },
- label: {
- fontSize: 12,
- color: '#999',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- value: {
- fontSize: 16,
- color: '#fff',
- fontWeight: '500',
- },
- versionText: {
- fontSize: 16,
- color: '#fff',
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 12,
- },
- aboutText: {
- fontSize: 14,
- color: '#aaa',
- lineHeight: 22,
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 12,
- },
- dangerZone: {
- marginTop: 20,
- },
- dangerButton: {
- backgroundColor: '#ff6b6b',
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- },
- dangerButtonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/(app)/tool/[id].tsx b/mobile/app/(app)/tool/[id].tsx
deleted file mode 100644
index 2336109..0000000
--- a/mobile/app/(app)/tool/[id].tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- View,
- Text,
- StyleSheet,
- TextInput,
- TouchableOpacity,
- ScrollView,
- ActivityIndicator,
- SafeAreaView,
- KeyboardAvoidingView,
- Platform,
-} from 'react-native';
-import { useLocalSearchParams, router } from 'expo-router';
-import { teraAPI } from '@/lib/api';
-
-export default function ToolDetailScreen() {
- const { id } = useLocalSearchParams();
- const [tool, setTool] = useState(null);
- const [input, setInput] = useState('');
- const [loading, setLoading] = useState(true);
- const [processing, setProcessing] = useState(false);
- const [result, setResult] = useState(null);
-
- useEffect(() => {
- loadToolData();
- }, [id]);
-
- const loadToolData = async () => {
- try {
- setLoading(true);
- const response = await teraAPI.getTools();
- if (response.success && response.data) {
- const foundTool = response.data.find((t: any) => t.id === id);
- setTool(foundTool);
- }
- } catch (error) {
- console.error('Failed to load tool:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const handleProcess = async () => {
- if (!input.trim() || processing) return;
-
- try {
- setProcessing(true);
- setResult(null);
- const response = await teraAPI.processTool(id as string, { input });
-
- if (response.success && response.data?.result) {
- setResult(response.data.result);
- } else {
- setResult('Failed to process. Please try again.');
- }
- } catch (error) {
- console.error('Processing error:', error);
- setResult('An error occurred. Check your connection.');
- } finally {
- setProcessing(false);
- }
- };
-
- if (loading) {
- return (
-
-
-
- );
- }
-
- if (!tool) {
- return (
-
- Tool not found
- router.back()}>
- Go Back
-
-
- );
- }
-
- return (
-
-
-
-
- {tool.name}
- {tool.description}
-
-
-
- Your Input
-
-
- {processing ? (
-
- ) : (
- Generate Content
- )}
-
-
-
- {result && (
-
- Result
-
- {result}
-
-
- )}
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- centerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#000',
- },
- scrollContent: {
- padding: 20,
- gap: 24,
- },
- header: {
- gap: 8,
- },
- title: {
- fontSize: 24,
- fontWeight: '700',
- color: '#00d4ff',
- },
- description: {
- fontSize: 16,
- color: '#aaa',
- lineHeight: 24,
- },
- inputSection: {
- gap: 12,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- color: '#fff',
- textTransform: 'uppercase',
- },
- input: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- padding: 16,
- color: '#fff',
- fontSize: 16,
- textAlignVertical: 'top',
- minHeight: 120,
- },
- button: {
- backgroundColor: '#00d4ff',
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- marginTop: 12,
- },
- buttonDisabled: {
- opacity: 0.5,
- },
- buttonText: {
- color: '#000',
- fontSize: 16,
- fontWeight: '700',
- },
- resultContainer: {
- gap: 12,
- },
- resultLabel: {
- fontSize: 14,
- fontWeight: '600',
- color: '#00d4ff',
- textTransform: 'uppercase',
- },
- resultBox: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- padding: 16,
- },
- resultText: {
- color: '#fff',
- fontSize: 16,
- lineHeight: 24,
- },
- errorText: {
- color: '#ff6b6b',
- fontSize: 18,
- marginBottom: 20,
- },
- backButton: {
- color: '#00d4ff',
- fontSize: 16,
- fontWeight: '600',
- },
-});
diff --git a/mobile/app/(app)/tools.tsx b/mobile/app/(app)/tools.tsx
deleted file mode 100644
index 5ec7137..0000000
--- a/mobile/app/(app)/tools.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- View,
- Text,
- FlatList,
- TouchableOpacity,
- StyleSheet,
- ActivityIndicator,
- SafeAreaView,
- RefreshControl,
-} from 'react-native';
-import { router } from 'expo-router';
-import { teraAPI } from '@/lib/api';
-
-interface Tool {
- id: string;
- name: string;
- description: string;
- category: string;
-}
-
-export default function ToolsScreen() {
- const [tools, setTools] = useState([]);
- const [loading, setLoading] = useState(true);
- const [refreshing, setRefreshing] = useState(false);
-
- useEffect(() => {
- loadTools();
- }, []);
-
- const loadTools = async () => {
- try {
- setLoading(true);
- const response = await teraAPI.getTools();
-
- if (response.success && response.data) {
- setTools(response.data);
- }
- } catch (error) {
- console.error('Failed to load tools:', error);
- } finally {
- setLoading(false);
- }
- };
-
- const onRefresh = async () => {
- setRefreshing(true);
- await loadTools();
- setRefreshing(false);
- };
-
- const handleToolPress = (toolId: string) => {
- router.push(`/(app)/tool/${toolId}`);
- };
-
- if (loading) {
- return (
-
-
-
-
-
- );
- }
-
- return (
-
- (
- handleToolPress(item.id)}
- >
-
- {item.name}
-
- {item.category}
-
-
- {item.description}
-
- )}
- keyExtractor={item => item.id}
- contentContainerStyle={styles.listContent}
- scrollEnabled={true}
- refreshControl={
-
- }
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- centerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- listContent: {
- paddingHorizontal: 12,
- paddingVertical: 12,
- gap: 12,
- },
- toolCard: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- padding: 16,
- marginBottom: 8,
- },
- toolHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 8,
- },
- toolName: {
- fontSize: 16,
- fontWeight: '600',
- color: '#00d4ff',
- flex: 1,
- },
- badge: {
- backgroundColor: '#00d4ff',
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 6,
- },
- badgeText: {
- color: '#000',
- fontSize: 12,
- fontWeight: '600',
- textTransform: 'capitalize',
- },
- toolDescription: {
- fontSize: 14,
- color: '#aaa',
- lineHeight: 20,
- },
-});
diff --git a/mobile/app/(auth)/_layout.tsx b/mobile/app/(auth)/_layout.tsx
deleted file mode 100644
index d2bd0b2..0000000
--- a/mobile/app/(auth)/_layout.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Stack } from 'expo-router';
-
-export default function AuthLayout() {
- return (
-
-
-
-
- );
-}
diff --git a/mobile/app/(auth)/forgot-password.tsx b/mobile/app/(auth)/forgot-password.tsx
new file mode 100644
index 0000000..9f79e3f
--- /dev/null
+++ b/mobile/app/(auth)/forgot-password.tsx
@@ -0,0 +1,72 @@
+import { Link } from 'expo-router';
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Button, Screen, Text, TextField } from '@/components/ui';
+import { colors, spacing } from '@/constants/theme';
+import { forgotPasswordSchema } from '@/features/auth/schemas';
+import { useAuthActions } from '@/features/auth/useAuthActions';
+
+export default function ForgotPasswordScreen() {
+ const { resetPassword } = useAuthActions();
+ const [email, setEmail] = useState('');
+ const [message, setMessage] = useState('');
+ const [error, setError] = useState('');
+
+ function submit() {
+ const result = forgotPasswordSchema.safeParse({ email });
+ if (!result.success) {
+ setError(result.error.issues[0]?.message ?? 'Enter your email.');
+ return;
+ }
+ setError('');
+ resetPassword.mutate(undefined, {
+ onSuccess: () => setMessage('Password reset delivery is mocked for this foundation build.'),
+ onError: () => setError('Could not start password reset.'),
+ });
+ }
+
+ return (
+
+
+ Reset password
+ Enter your email. This screen is ready for backend email delivery.
+
+
+
+ {error ? {error} : null}
+ {message ? {message} : null}
+
+ Back to sign in
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xxl,
+ marginTop: spacing.xl,
+ },
+ form: {
+ gap: spacing.lg,
+ },
+ error: {
+ color: colors.danger,
+ },
+ message: {
+ color: colors.accent,
+ },
+ link: {
+ color: colors.accent,
+ textAlign: 'center',
+ fontSize: 16,
+ paddingVertical: spacing.sm,
+ },
+});
diff --git a/mobile/app/(auth)/sign-in.tsx b/mobile/app/(auth)/sign-in.tsx
new file mode 100644
index 0000000..9c0e408
--- /dev/null
+++ b/mobile/app/(auth)/sign-in.tsx
@@ -0,0 +1,74 @@
+import { Link } from 'expo-router';
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Button, Screen, Text, TextField } from '@/components/ui';
+import { colors, spacing } from '@/constants/theme';
+import { signInSchema } from '@/features/auth/schemas';
+import { useAuthActions } from '@/features/auth/useAuthActions';
+
+export default function SignInScreen() {
+ const { signIn } = useAuthActions();
+ const [email, setEmail] = useState('learner@tera.ai');
+ const [password, setPassword] = useState('password');
+ const [error, setError] = useState('');
+
+ function submit() {
+ const result = signInSchema.safeParse({ email, password });
+ if (!result.success) {
+ setError(result.error.issues[0]?.message ?? 'Check your details.');
+ return;
+ }
+ setError('');
+ signIn.mutate(result.data, {
+ onError: () => setError('Sign in failed. Try again.'),
+ });
+ }
+
+ return (
+
+
+ Welcome back
+ Sign in to continue learning with Tera.
+
+
+
+
+ {error ? {error} : null}
+
+ Forgot password?
+ Create an account
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xxl,
+ marginTop: spacing.xl,
+ },
+ form: {
+ gap: spacing.lg,
+ },
+ error: {
+ color: colors.danger,
+ },
+ link: {
+ color: colors.accent,
+ textAlign: 'center',
+ fontSize: 16,
+ paddingVertical: spacing.sm,
+ },
+});
diff --git a/mobile/app/(auth)/sign-up.tsx b/mobile/app/(auth)/sign-up.tsx
new file mode 100644
index 0000000..585329d
--- /dev/null
+++ b/mobile/app/(auth)/sign-up.tsx
@@ -0,0 +1,75 @@
+import { Link } from 'expo-router';
+import { useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Button, Screen, Text, TextField } from '@/components/ui';
+import { colors, spacing } from '@/constants/theme';
+import { signUpSchema } from '@/features/auth/schemas';
+import { useAuthActions } from '@/features/auth/useAuthActions';
+
+export default function SignUpScreen() {
+ const { signUp } = useAuthActions();
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+
+ function submit() {
+ const result = signUpSchema.safeParse({ name, email, password });
+ if (!result.success) {
+ setError(result.error.issues[0]?.message ?? 'Check your details.');
+ return;
+ }
+ setError('');
+ signUp.mutate(result.data, {
+ onError: () => setError('Could not create your account. Try again.'),
+ });
+ }
+
+ return (
+
+
+ Create your Tera account
+ Start with a focused AI companion for learning, research, and building.
+
+
+
+
+
+ {error ? {error} : null}
+
+ I already have an account
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xxl,
+ marginTop: spacing.xl,
+ },
+ form: {
+ gap: spacing.lg,
+ },
+ error: {
+ color: colors.danger,
+ },
+ link: {
+ color: colors.accent,
+ textAlign: 'center',
+ fontSize: 16,
+ paddingVertical: spacing.sm,
+ },
+});
diff --git a/mobile/app/(auth)/signin.tsx b/mobile/app/(auth)/signin.tsx
deleted file mode 100644
index d05b348..0000000
--- a/mobile/app/(auth)/signin.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import * as React from 'react';
-import { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- ActivityIndicator,
- SafeAreaView,
- KeyboardAvoidingView,
- Platform,
- ScrollView,
-} from 'react-native';
-import * as SecureStore from 'expo-secure-store';
-import { router } from 'expo-router';
-import { teraAPI } from '@/lib/api';
-import { saveUser } from '@/lib/storage';
-
-export default function SignInScreen() {
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
-
- const handleSignIn = async () => {
- if (!email.trim() || !password.trim()) {
- setError('Email and password are required');
- return;
- }
-
- try {
- setLoading(true);
- setError('');
-
- const response = await teraAPI.signIn(email, password);
-
- if (!response.success || !response.data) {
- setError(response.error || 'Sign in failed');
- return;
- }
-
- // Store token securely
- await SecureStore.setItemAsync('auth_token', response.data.token);
- await SecureStore.setItemAsync('user_id', response.data.user.id);
-
- // Store user locally
- await saveUser(response.data.user.id, response.data.user);
-
- // Navigate to app
- router.replace('/(app)/chat');
- } catch (err: any) {
- setError(err.message || 'Sign in failed');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- Tera
-
- Your AI Learning Companion for Anything
-
-
-
-
-
- Email
-
-
-
-
- Password
-
-
-
- {error && {error}}
-
-
- {loading ? (
-
- ) : (
- Sign In
- )}
-
-
- router.push('/(auth)/signup')}
- disabled={loading}
- >
-
- Don't have an account? Sign Up
-
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- scrollContent: {
- flexGrow: 1,
- paddingHorizontal: 24,
- paddingVertical: 40,
- },
- header: {
- marginBottom: 60,
- marginTop: 40,
- },
- logo: {
- fontSize: 48,
- fontWeight: 'bold',
- color: '#00d4ff',
- marginBottom: 8,
- },
- tagline: {
- fontSize: 16,
- color: '#999',
- },
- formContainer: {
- gap: 20,
- },
- inputGroup: {
- gap: 8,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- color: '#fff',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- input: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 14,
- color: '#fff',
- fontSize: 16,
- },
- button: {
- backgroundColor: '#00d4ff',
- borderRadius: 12,
- paddingVertical: 16,
- alignItems: 'center',
- marginTop: 24,
- },
- buttonDisabled: {
- opacity: 0.6,
- },
- buttonText: {
- color: '#000',
- fontSize: 16,
- fontWeight: '700',
- },
- error: {
- color: '#ff6b6b',
- fontSize: 14,
- textAlign: 'center',
- marginTop: 8,
- },
- link: {
- color: '#00d4ff',
- textAlign: 'center',
- fontSize: 14,
- marginTop: 16,
- },
-});
diff --git a/mobile/app/(auth)/signup.tsx b/mobile/app/(auth)/signup.tsx
deleted file mode 100644
index a4748ca..0000000
--- a/mobile/app/(auth)/signup.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-import * as React from 'react';
-import { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- ActivityIndicator,
- SafeAreaView,
- KeyboardAvoidingView,
- Platform,
- ScrollView,
- Alert,
-} from 'react-native';
-import { router } from 'expo-router';
-import { teraAPI } from '@/lib/api';
-
-export default function SignUpScreen() {
- const [name, setName] = useState('');
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
-
- const handleSignUp = async () => {
- if (!name.trim() || !email.trim() || !password.trim()) {
- setError('All fields are required');
- return;
- }
-
- if (password !== confirmPassword) {
- setError('Passwords do not match');
- return;
- }
-
- if (password.length < 6) {
- setError('Password must be at least 6 characters');
- return;
- }
-
- try {
- setLoading(true);
- setError('');
-
- const response = await teraAPI.signUp(email, name, password);
-
- if (!response.success) {
- setError(response.error || 'Sign up failed');
- return;
- }
-
- // Show success message and redirect to sign in
- Alert.alert('Success', 'Account created! Please sign in.');
- router.replace('/(auth)/signin');
- } catch (err: any) {
- setError(err.message || 'Sign up failed');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- router.back()}
- disabled={loading}
- >
- ← Back
-
- Create Account
-
-
-
-
- Name
-
-
-
-
- Email
-
-
-
-
- Password
-
-
-
-
- Confirm Password
-
-
-
- {error && {error}}
-
-
- {loading ? (
-
- ) : (
- Create Account
- )}
-
-
- router.back()}
- disabled={loading}
- >
- Already have an account? Sign In
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#000',
- },
- scrollContent: {
- flexGrow: 1,
- paddingHorizontal: 24,
- paddingVertical: 40,
- },
- header: {
- marginBottom: 40,
- gap: 16,
- },
- backButton: {
- color: '#00d4ff',
- fontSize: 14,
- fontWeight: '600',
- },
- title: {
- fontSize: 32,
- fontWeight: 'bold',
- color: '#fff',
- },
- formContainer: {
- gap: 20,
- },
- inputGroup: {
- gap: 8,
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- color: '#fff',
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- input: {
- backgroundColor: '#1a1a1a',
- borderColor: '#333',
- borderWidth: 1,
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 14,
- color: '#fff',
- fontSize: 16,
- },
- button: {
- backgroundColor: '#00d4ff',
- borderRadius: 12,
- paddingVertical: 16,
- alignItems: 'center',
- marginTop: 24,
- },
- buttonDisabled: {
- opacity: 0.6,
- },
- buttonText: {
- color: '#000',
- fontSize: 16,
- fontWeight: '700',
- },
- error: {
- color: '#ff6b6b',
- fontSize: 14,
- textAlign: 'center',
- },
- link: {
- color: '#00d4ff',
- textAlign: 'center',
- fontSize: 14,
- marginTop: 16,
- },
-});
diff --git a/mobile/app/(onboarding)/index.tsx b/mobile/app/(onboarding)/index.tsx
new file mode 100644
index 0000000..2a3cf0a
--- /dev/null
+++ b/mobile/app/(onboarding)/index.tsx
@@ -0,0 +1,81 @@
+import { router } from 'expo-router';
+import { StyleSheet, View } from 'react-native';
+import { Button, Screen, Text } from '@/components/ui';
+import { colors, radii, spacing } from '@/constants/theme';
+import { onboardingSlides } from '@/features/onboarding/onboarding-content';
+import { useAppStore } from '@/store/app-store';
+
+export default function OnboardingScreen() {
+ const completeOnboarding = useAppStore((state) => state.completeOnboarding);
+
+ async function continueToAuth() {
+ await completeOnboarding();
+ router.replace('/(auth)/sign-in');
+ }
+
+ return (
+
+
+ TeraAI
+
+
+ Learn clearly. Build from what you know.
+
+ Tera is an AI learning companion for explanations, research, and turning ideas into action.
+
+
+
+ {onboardingSlides.map((slide, index) => (
+
+ 0{index + 1}
+
+ {slide.title}
+ {slide.body}
+
+
+ ))}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ heroMark: {
+ width: 76,
+ height: 76,
+ borderRadius: radii.xl,
+ backgroundColor: colors.accentMuted,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderWidth: 1,
+ borderColor: colors.border,
+ marginBottom: spacing.xxl,
+ },
+ markText: {
+ color: colors.accent,
+ },
+ header: {
+ gap: spacing.lg,
+ marginBottom: spacing.xxl,
+ },
+ slides: {
+ gap: spacing.md,
+ marginBottom: spacing.xxl,
+ },
+ slide: {
+ flexDirection: 'row',
+ gap: spacing.lg,
+ paddingVertical: spacing.lg,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderMuted,
+ },
+ step: {
+ color: colors.accent,
+ width: 28,
+ },
+ slideCopy: {
+ flex: 1,
+ gap: spacing.xs,
+ },
+});
diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx
new file mode 100644
index 0000000..6161de6
--- /dev/null
+++ b/mobile/app/(tabs)/_layout.tsx
@@ -0,0 +1,30 @@
+import { Tabs } from 'expo-router';
+import { colors } from '@/constants/theme';
+
+export default function TabsLayout() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/mobile/app/(tabs)/history.tsx b/mobile/app/(tabs)/history.tsx
new file mode 100644
index 0000000..fe489bb
--- /dev/null
+++ b/mobile/app/(tabs)/history.tsx
@@ -0,0 +1,59 @@
+import { useQuery } from '@tanstack/react-query';
+import { useMemo, useState } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { EmptyState, LoadingState, Screen, Text, TextField } from '@/components/ui';
+import { spacing } from '@/constants/theme';
+import { ConversationPreview } from '@/features/chat/ConversationPreview';
+import { teraApi } from '@/lib/api/client';
+
+export default function HistoryScreen() {
+ const [query, setQuery] = useState('');
+ const conversations = useQuery({
+ queryKey: ['conversations'],
+ queryFn: teraApi.getConversations,
+ });
+
+ const filtered = useMemo(() => {
+ const normalized = query.trim().toLowerCase();
+ if (!normalized) return conversations.data ?? [];
+ return (conversations.data ?? []).filter((conversation) =>
+ `${conversation.title} ${conversation.summary}`.toLowerCase().includes(normalized),
+ );
+ }, [conversations.data, query]);
+
+ return (
+
+
+ History
+ Return to explanations, research threads, and build plans.
+
+
+ {conversations.isLoading ? (
+
+ ) : filtered.length ? (
+
+ {filtered.map((conversation) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xl,
+ },
+ list: {
+ gap: spacing.md,
+ marginTop: spacing.xl,
+ },
+});
diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx
new file mode 100644
index 0000000..c8cdc7a
--- /dev/null
+++ b/mobile/app/(tabs)/index.tsx
@@ -0,0 +1,97 @@
+import { useQuery } from '@tanstack/react-query';
+import { StyleSheet, View } from 'react-native';
+import { Button, Chip, EmptyState, LoadingState, Screen, SegmentedControl, Text } from '@/components/ui';
+import { colors, radii, spacing } from '@/constants/theme';
+import { modeOptions, starterPrompts } from '@/features/chat/chat-data';
+import { Composer } from '@/features/chat/Composer';
+import { ConversationPreview } from '@/features/chat/ConversationPreview';
+import { teraApi } from '@/lib/api/client';
+import { useAppStore } from '@/store/app-store';
+
+export default function HomeScreen() {
+ const selectedMode = useAppStore((state) => state.selectedMode);
+ const setSelectedMode = useAppStore((state) => state.setSelectedMode);
+ const user = useAppStore((state) => state.session?.user);
+ const conversations = useQuery({
+ queryKey: ['conversations'],
+ queryFn: teraApi.getConversations,
+ });
+
+ return (
+
+
+ TeraAI
+ Good to see you{user?.name ? `, ${user.name.split(' ')[0]}` : ''}.
+ Ask, learn, research, or turn an idea into a concrete next step.
+
+
+
+
+
+ undefined} placeholder={`Ask Tera in ${selectedMode} mode...`} />
+ Conversation creation will connect to the backend in the next pass.
+
+
+
+ Starter prompts
+
+ {starterPrompts[selectedMode].map((prompt) => (
+
+ ))}
+
+
+
+
+ Recent conversations
+
+
+ {conversations.isLoading ? (
+
+ ) : conversations.data?.length ? (
+
+ {conversations.data.slice(0, 3).map((conversation) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xl,
+ },
+ brand: {
+ color: colors.accent,
+ },
+ composerBlock: {
+ marginTop: spacing.xl,
+ gap: spacing.sm,
+ },
+ section: {
+ marginTop: spacing.xxl,
+ gap: spacing.md,
+ },
+ chips: {
+ gap: spacing.md,
+ },
+ sectionHeader: {
+ marginTop: spacing.xxl,
+ marginBottom: spacing.md,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: spacing.lg,
+ },
+ newButton: {
+ borderRadius: radii.pill,
+ paddingHorizontal: spacing.lg,
+ },
+ list: {
+ gap: spacing.md,
+ },
+});
diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx
new file mode 100644
index 0000000..1f0bfbd
--- /dev/null
+++ b/mobile/app/(tabs)/profile.tsx
@@ -0,0 +1,126 @@
+import { Switch, StyleSheet, View } from 'react-native';
+import { Button, Divider, ListRow, Screen, Text } from '@/components/ui';
+import { colors, radii, spacing } from '@/constants/theme';
+import { useAuthActions } from '@/features/auth/useAuthActions';
+import { useAppStore } from '@/store/app-store';
+
+export default function ProfileScreen() {
+ const { signOut } = useAuthActions();
+ const session = useAppStore((state) => state.session);
+ const preferences = useAppStore((state) => state.preferences);
+ const setPreferences = useAppStore((state) => state.setPreferences);
+
+ return (
+
+
+ Profile
+ Manage your Tera account and app preferences.
+
+
+
+
+
+ {session?.user.name?.[0] ?? 'T'}
+
+
+
+ {session?.user.name ?? 'Tera Learner'}
+ {session?.user.email ?? 'learner@tera.ai'}
+ {session?.user.plan ?? 'free'} plan
+
+
+
+
+
+
+ Concise answers
+ Prefer shorter responses by default.
+
+ setPreferences({ conciseAnswers: value })}
+ thumbColor={preferences.conciseAnswers ? colors.accent : colors.textMuted}
+ />
+
+
+
+
+ Notifications
+ Prepared for study reminders and updates.
+
+ setPreferences({ notificationsEnabled: value })}
+ thumbColor={preferences.notificationsEnabled ? colors.accent : colors.textMuted}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xl,
+ },
+ userPanel: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.lg,
+ padding: spacing.lg,
+ borderRadius: radii.lg,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.borderMuted,
+ marginBottom: spacing.xl,
+ },
+ avatar: {
+ width: 58,
+ height: 58,
+ borderRadius: radii.pill,
+ backgroundColor: colors.accentMuted,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ avatarText: {
+ color: colors.accent,
+ },
+ userCopy: {
+ flex: 1,
+ gap: spacing.xs,
+ },
+ plan: {
+ color: colors.accent,
+ },
+ section: {
+ borderRadius: radii.lg,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.borderMuted,
+ marginBottom: spacing.xl,
+ overflow: 'hidden',
+ },
+ preferenceRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.lg,
+ padding: spacing.lg,
+ },
+ preferenceCopy: {
+ flex: 1,
+ gap: spacing.xs,
+ },
+ list: {
+ gap: spacing.md,
+ marginBottom: spacing.xl,
+ },
+});
diff --git a/mobile/app/(tabs)/saved.tsx b/mobile/app/(tabs)/saved.tsx
new file mode 100644
index 0000000..dcb47a4
--- /dev/null
+++ b/mobile/app/(tabs)/saved.tsx
@@ -0,0 +1,48 @@
+import { useQuery } from '@tanstack/react-query';
+import { StyleSheet, View } from 'react-native';
+import { EmptyState, ListRow, LoadingState, Screen, Text } from '@/components/ui';
+import { spacing } from '@/constants/theme';
+import { formatRelativeTime } from '@/features/chat/chat-data';
+import { teraApi } from '@/lib/api/client';
+
+export default function SavedScreen() {
+ const saved = useQuery({
+ queryKey: ['saved-items'],
+ queryFn: teraApi.getSavedItems,
+ });
+
+ return (
+
+
+ Saved
+ Keep the conversations and outputs worth returning to.
+
+ {saved.isLoading ? (
+
+ ) : saved.data?.length ? (
+
+ {saved.data.map((item) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ header: {
+ gap: spacing.md,
+ marginBottom: spacing.xl,
+ },
+ list: {
+ gap: spacing.md,
+ },
+});
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index b7ea978..4bcb665 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -1,61 +1,32 @@
-import * as React from 'react';
-import { useEffect, useState } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Stack } from 'expo-router';
-import * as SecureStore from 'expo-secure-store';
+import { StatusBar } from 'react-native';
+import { useMemo } from 'react';
+import { colors } from '@/constants/theme';
+import { useSessionBootstrap } from '@/hooks/useSessionBootstrap';
-export default function RootLayout() {
- const [isSignedIn, setIsSignedIn] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- checkAuthStatus();
- }, []);
+function RootNavigator() {
+ useSessionBootstrap();
- const checkAuthStatus = async () => {
- try {
- const token = await SecureStore.getItemAsync('auth_token');
- setIsSignedIn(!!token);
- } catch (error) {
- console.error('Error checking auth status:', error);
- setIsSignedIn(false);
- } finally {
- setIsLoading(false);
- }
- };
-
- if (isLoading) {
- return (
+ return (
+ <>
+
- );
- }
+ >
+ );
+}
+
+export default function RootLayout() {
+ const queryClient = useMemo(() => new QueryClient(), []);
return (
-
- {isSignedIn ? (
-
- ) : (
-
- )}
-
+
+
+
);
}
diff --git a/mobile/app/conversation/[id]/index.tsx b/mobile/app/conversation/[id]/index.tsx
new file mode 100644
index 0000000..8c9bbb4
--- /dev/null
+++ b/mobile/app/conversation/[id]/index.tsx
@@ -0,0 +1,128 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Stack, useLocalSearchParams } from 'expo-router';
+import { useMemo, useState } from 'react';
+import { FlatList, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
+import { EmptyState, LoadingState, Screen, Text } from '@/components/ui';
+import { colors, spacing } from '@/constants/theme';
+import { Composer } from '@/features/chat/Composer';
+import { MessageBubble } from '@/features/chat/MessageBubble';
+import { useKeyboardInsets } from '@/hooks/useKeyboardInsets';
+import { teraApi } from '@/lib/api/client';
+import { useAppStore } from '@/store/app-store';
+import { Message } from '@/types/domain';
+
+export default function ConversationScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const selectedMode = useAppStore((state) => state.selectedMode);
+ const keyboard = useKeyboardInsets();
+ const conversation = useQuery({
+ queryKey: ['conversation', id],
+ queryFn: () => teraApi.getConversation(id),
+ enabled: Boolean(id),
+ });
+ const [localMessages, setLocalMessages] = useState([]);
+
+ const messages = useMemo(
+ () => [...(conversation.data?.messages ?? []), ...localMessages],
+ [conversation.data?.messages, localMessages],
+ );
+
+ const sendMessage = useMutation({
+ mutationFn: (prompt: string) => teraApi.sendMessage(id, conversation.data?.mode ?? selectedMode, prompt),
+ onMutate: (prompt) => {
+ const userMessage: Message = {
+ id: `local_${Date.now()}`,
+ conversationId: id,
+ role: 'user',
+ content: prompt,
+ createdAt: new Date().toISOString(),
+ status: 'sent',
+ };
+ const streamingMessage: Message = {
+ id: `streaming_${Date.now()}`,
+ conversationId: id,
+ role: 'assistant',
+ content: 'Tera is preparing a learning-focused answer...',
+ createdAt: new Date().toISOString(),
+ status: 'streaming',
+ };
+ setLocalMessages((current) => [...current, userMessage, streamingMessage]);
+ },
+ onSuccess: (assistantMessage) => {
+ setLocalMessages((current) => [
+ ...current.filter((message) => message.status !== 'streaming'),
+ assistantMessage,
+ ]);
+ },
+ onError: () => {
+ setLocalMessages((current) =>
+ current.map((message) =>
+ message.status === 'streaming'
+ ? {
+ ...message,
+ content: 'Tera could not answer right now. Check your connection and try again.',
+ status: 'failed',
+ }
+ : message,
+ ),
+ );
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+ {conversation.data?.title ?? 'Conversation'}
+ {conversation.data?.mode ?? selectedMode} mode
+
+ {conversation.isLoading ? (
+
+ ) : !conversation.data ? (
+
+ ) : messages.length ? (
+ item.id}
+ renderItem={({ item }) => }
+ contentContainerStyle={styles.messages}
+ showsVerticalScrollIndicator={false}
+ />
+ ) : (
+
+ )}
+
+ sendMessage.mutate(value)} />
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ keyboard: {
+ flex: 1,
+ },
+ header: {
+ gap: spacing.xs,
+ paddingBottom: spacing.md,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderMuted,
+ },
+ messages: {
+ paddingTop: spacing.md,
+ paddingBottom: spacing.lg,
+ },
+ composer: {
+ paddingTop: spacing.md,
+ paddingBottom: spacing.lg,
+ backgroundColor: colors.background,
+ },
+});
diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx
new file mode 100644
index 0000000..2f369a8
--- /dev/null
+++ b/mobile/app/index.tsx
@@ -0,0 +1,21 @@
+import { Redirect } from 'expo-router';
+import { LoadingState, Screen } from '@/components/ui';
+import { useAppStore } from '@/store/app-store';
+
+export default function IndexRoute() {
+ const hydrated = useAppStore((state) => state.hydrated);
+ const onboardingComplete = useAppStore((state) => state.onboardingComplete);
+ const session = useAppStore((state) => state.session);
+
+ if (!hydrated) {
+ return (
+
+
+
+ );
+ }
+
+ if (!onboardingComplete) return ;
+ if (!session) return ;
+ return ;
+}
diff --git a/mobile/components/ChatBubble.tsx b/mobile/components/ChatBubble.tsx
deleted file mode 100644
index 8a4e912..0000000
--- a/mobile/components/ChatBubble.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-import * as React from 'react';
-import { View, Text, StyleSheet } from 'react-native';
-
-interface Props {
- role: 'user' | 'assistant';
- content: string;
-}
-
-/** Parse tera-ui spec blocks out of assistant content */
-function parseTeraBlocks(content: string): Array<{ type: 'text'; value: string } | { type: 'tera-ui'; spec: any }> {
- const blocks: Array<{ type: 'text'; value: string } | { type: 'tera-ui'; spec: any }> = [];
- const regex = /```json:tera-ui\n([\s\S]*?)```/g;
- let lastIndex = 0;
- let match;
-
- while ((match = regex.exec(content)) !== null) {
- if (match.index > lastIndex) {
- blocks.push({ type: 'text', value: content.slice(lastIndex, match.index) });
- }
- try {
- const spec = JSON.parse(match[1].trim());
- blocks.push({ type: 'tera-ui', spec });
- } catch {
- blocks.push({ type: 'text', value: match[0] });
- }
- lastIndex = regex.lastIndex;
- }
-
- if (lastIndex < content.length) {
- blocks.push({ type: 'text', value: content.slice(lastIndex) });
- }
-
- return blocks;
-}
-
-/** Get a human-readable label for a tera-ui component */
-function getComponentLabel(spec: any): { icon: string; label: string; title?: string } {
- if (!spec?.root || !spec?.elements) return { icon: '📊', label: 'Visual' };
- const rootEl = spec.elements[spec.root];
- if (!rootEl) return { icon: '📊', label: 'Visual' };
- const typeMap: Record = {
- Chart: { icon: '📊', label: 'Chart' },
- MermaidDiagram: { icon: '🔀', label: 'Diagram' },
- Quiz: { icon: '❓', label: 'Quiz' },
- Spreadsheet: { icon: '📋', label: 'Spreadsheet' },
- RichText: { icon: '📝', label: 'Text' },
- };
- const info = typeMap[rootEl.type] || { icon: '📊', label: rootEl.type };
- return { ...info, title: rootEl.props?.title || rootEl.props?.topic };
-}
-
-export default function ChatBubble({ role, content }: Props) {
- const isUser = role === 'user';
-
- // For user messages or non-assistant, render plain
- if (isUser) {
- return (
-
-
- {content}
-
-
- );
- }
-
- // For assistant: detect tera-ui blocks
- const blocks = parseTeraBlocks(content);
-
- return (
-
-
- {blocks.map((block, i) => {
- if (block.type === 'text') {
- return block.value.trim() ? (
-
- {block.value.trim()}
-
- ) : null;
- }
- // tera-ui card
- const { icon, label, title } = getComponentLabel(block.spec);
- return (
-
- {icon}
-
- {label}
- {title ? {title} : null}
- View on web for full interactive visual
-
-
- );
- })}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- marginVertical: 8,
- paddingHorizontal: 12,
- flexDirection: 'row',
- },
- userContainer: {
- justifyContent: 'flex-end',
- },
- assistantContainer: {
- justifyContent: 'flex-start',
- },
- bubble: {
- maxWidth: '85%',
- paddingHorizontal: 16,
- paddingVertical: 12,
- borderRadius: 16,
- },
- userBubble: {
- backgroundColor: '#00d4ff',
- borderBottomRightRadius: 4,
- },
- assistantBubble: {
- backgroundColor: '#1a1a1a',
- borderBottomLeftRadius: 4,
- borderWidth: 1,
- borderColor: '#333',
- },
- text: {
- fontSize: 16,
- lineHeight: 22,
- },
- userText: {
- color: '#000',
- fontWeight: '500',
- },
- assistantText: {
- color: '#fff',
- },
- visualCard: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: '#0d0d0d',
- borderRadius: 12,
- padding: 12,
- marginVertical: 8,
- borderWidth: 1,
- borderColor: '#00d4ff33',
- },
- visualIcon: {
- fontSize: 28,
- marginRight: 12,
- },
- visualInfo: {
- flex: 1,
- },
- visualLabel: {
- color: '#00d4ff',
- fontSize: 14,
- fontWeight: '600',
- },
- visualTitle: {
- color: '#fff',
- fontSize: 13,
- marginTop: 2,
- },
- visualHint: {
- color: '#666',
- fontSize: 11,
- marginTop: 4,
- },
-});
diff --git a/mobile/components/ui/Button.tsx b/mobile/components/ui/Button.tsx
new file mode 100644
index 0000000..3472cbb
--- /dev/null
+++ b/mobile/components/ui/Button.tsx
@@ -0,0 +1,79 @@
+import { ReactNode } from 'react';
+import { ActivityIndicator, Pressable, StyleSheet, ViewStyle } from 'react-native';
+import { colors, layout, radii, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+type Variant = 'primary' | 'secondary' | 'ghost';
+
+interface ButtonProps {
+ label: string;
+ onPress?: () => void;
+ variant?: Variant;
+ disabled?: boolean;
+ loading?: boolean;
+ icon?: ReactNode;
+ style?: ViewStyle;
+}
+
+export function Button({
+ label,
+ onPress,
+ variant = 'primary',
+ disabled,
+ loading,
+ icon,
+ style,
+}: ButtonProps) {
+ return (
+ [
+ styles.base,
+ styles[variant],
+ pressed && styles.pressed,
+ (disabled || loading) && styles.disabled,
+ style,
+ ]}
+ >
+ {loading ? : icon}
+ {label}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ base: {
+ minHeight: layout.minTouchTarget,
+ borderRadius: radii.pill,
+ paddingHorizontal: spacing.xl,
+ paddingVertical: spacing.md,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'row',
+ gap: spacing.sm,
+ },
+ primary: {
+ backgroundColor: colors.accent,
+ },
+ secondary: {
+ backgroundColor: colors.surfaceMuted,
+ borderWidth: 1,
+ borderColor: colors.border,
+ },
+ ghost: {
+ backgroundColor: 'transparent',
+ },
+ label: {
+ fontWeight: '700',
+ },
+ primaryLabel: {
+ color: colors.black,
+ },
+ pressed: {
+ opacity: 0.82,
+ },
+ disabled: {
+ opacity: 0.55,
+ },
+});
diff --git a/mobile/components/ui/Chip.tsx b/mobile/components/ui/Chip.tsx
new file mode 100644
index 0000000..90fb23f
--- /dev/null
+++ b/mobile/components/ui/Chip.tsx
@@ -0,0 +1,31 @@
+import { Pressable, StyleSheet } from 'react-native';
+import { colors, radii, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface ChipProps {
+ label: string;
+ onPress?: () => void;
+}
+
+export function Chip({ label, onPress }: ChipProps) {
+ return (
+
+ {label}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ chip: {
+ borderRadius: radii.pill,
+ borderWidth: 1,
+ borderColor: colors.border,
+ backgroundColor: colors.surface,
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.md,
+ },
+ label: {
+ color: colors.text,
+ fontWeight: '600',
+ },
+});
diff --git a/mobile/components/ui/Divider.tsx b/mobile/components/ui/Divider.tsx
new file mode 100644
index 0000000..8c73bf3
--- /dev/null
+++ b/mobile/components/ui/Divider.tsx
@@ -0,0 +1,13 @@
+import { StyleSheet, View } from 'react-native';
+import { colors } from '@/constants/theme';
+
+export function Divider() {
+ return ;
+}
+
+const styles = StyleSheet.create({
+ divider: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: colors.borderMuted,
+ },
+});
diff --git a/mobile/components/ui/EmptyState.tsx b/mobile/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..87a729e
--- /dev/null
+++ b/mobile/components/ui/EmptyState.tsx
@@ -0,0 +1,30 @@
+import { StyleSheet, View } from 'react-native';
+import { spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface EmptyStateProps {
+ title: string;
+ body: string;
+}
+
+export function EmptyState({ title, body }: EmptyStateProps) {
+ return (
+
+ {title}
+ {body}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: spacing.sm,
+ paddingVertical: spacing.xxxl,
+ paddingHorizontal: spacing.xl,
+ },
+ body: {
+ textAlign: 'center',
+ },
+});
diff --git a/mobile/components/ui/ListRow.tsx b/mobile/components/ui/ListRow.tsx
new file mode 100644
index 0000000..0bbaa16
--- /dev/null
+++ b/mobile/components/ui/ListRow.tsx
@@ -0,0 +1,46 @@
+import { Pressable, StyleSheet, View } from 'react-native';
+import { colors, layout, radii, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface ListRowProps {
+ title: string;
+ subtitle?: string;
+ meta?: string;
+ onPress?: () => void;
+}
+
+export function ListRow({ title, subtitle, meta, onPress }: ListRowProps) {
+ return (
+ [styles.row, pressed && styles.pressed]}
+ >
+
+ {title}
+ {subtitle ? {subtitle} : null}
+
+ {meta ? {meta} : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ row: {
+ minHeight: layout.minTouchTarget,
+ borderRadius: radii.lg,
+ padding: spacing.lg,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.borderMuted,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.md,
+ },
+ content: {
+ flex: 1,
+ gap: spacing.xs,
+ },
+ pressed: {
+ backgroundColor: colors.surfacePressed,
+ },
+});
diff --git a/mobile/components/ui/LoadingState.tsx b/mobile/components/ui/LoadingState.tsx
new file mode 100644
index 0000000..6756a18
--- /dev/null
+++ b/mobile/components/ui/LoadingState.tsx
@@ -0,0 +1,25 @@
+import { ActivityIndicator, StyleSheet, View } from 'react-native';
+import { colors, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface LoadingStateProps {
+ label?: string;
+}
+
+export function LoadingState({ label = 'Loading Tera...' }: LoadingStateProps) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: spacing.md,
+ },
+});
diff --git a/mobile/components/ui/Screen.tsx b/mobile/components/ui/Screen.tsx
new file mode 100644
index 0000000..87399da
--- /dev/null
+++ b/mobile/components/ui/Screen.tsx
@@ -0,0 +1,49 @@
+import { ReactNode } from 'react';
+import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native';
+import { colors, layout } from '@/constants/theme';
+
+interface ScreenProps {
+ children: ReactNode;
+ scroll?: boolean;
+ insetBottom?: boolean;
+}
+
+export function Screen({ children, scroll, insetBottom = true }: ScreenProps) {
+ if (scroll) {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ content: {
+ paddingHorizontal: layout.screenPadding,
+ paddingTop: 18,
+ },
+ flex: {
+ flex: 1,
+ },
+ bottomInset: {
+ paddingBottom: 28,
+ },
+});
diff --git a/mobile/components/ui/SegmentedControl.tsx b/mobile/components/ui/SegmentedControl.tsx
new file mode 100644
index 0000000..ae226c8
--- /dev/null
+++ b/mobile/components/ui/SegmentedControl.tsx
@@ -0,0 +1,70 @@
+import { Pressable, StyleSheet, View } from 'react-native';
+import { colors, layout, radii, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface Option {
+ label: string;
+ value: T;
+}
+
+interface SegmentedControlProps {
+ value: T;
+ options: Option[];
+ onChange: (value: T) => void;
+}
+
+export function SegmentedControl({
+ value,
+ options,
+ onChange,
+}: SegmentedControlProps) {
+ return (
+
+ {options.map((option) => {
+ const selected = option.value === value;
+ return (
+ onChange(option.value)}
+ style={[styles.item, selected && styles.itemSelected]}
+ >
+
+ {option.label}
+
+
+ );
+ })}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ flexDirection: 'row',
+ padding: spacing.xs,
+ borderRadius: radii.pill,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.borderMuted,
+ },
+ item: {
+ flex: 1,
+ minHeight: layout.minTouchTarget,
+ borderRadius: radii.pill,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ itemSelected: {
+ backgroundColor: colors.accentMuted,
+ },
+ label: {
+ color: colors.textMuted,
+ fontWeight: '700',
+ },
+ labelSelected: {
+ color: colors.accent,
+ },
+});
diff --git a/mobile/components/ui/Text.tsx b/mobile/components/ui/Text.tsx
new file mode 100644
index 0000000..d18490c
--- /dev/null
+++ b/mobile/components/ui/Text.tsx
@@ -0,0 +1,64 @@
+import { Text as NativeText, TextProps, StyleSheet } from 'react-native';
+import { colors, typography } from '@/constants/theme';
+
+type Variant = 'hero' | 'h1' | 'h2' | 'h3' | 'body' | 'bodySmall' | 'caption';
+
+interface AppTextProps extends TextProps {
+ variant?: Variant;
+ muted?: boolean;
+}
+
+export function Text({ variant = 'body', muted, style, ...props }: AppTextProps) {
+ return (
+
+ );
+}
+
+const styles = StyleSheet.create({
+ base: {
+ color: colors.text,
+ letterSpacing: 0,
+ },
+ hero: {
+ fontSize: typography.hero,
+ lineHeight: 44,
+ fontWeight: '700',
+ },
+ h1: {
+ fontSize: typography.h1,
+ lineHeight: 36,
+ fontWeight: '700',
+ },
+ h2: {
+ fontSize: typography.h2,
+ lineHeight: 30,
+ fontWeight: '700',
+ },
+ h3: {
+ fontSize: typography.h3,
+ lineHeight: 25,
+ fontWeight: '700',
+ },
+ body: {
+ fontSize: typography.body,
+ lineHeight: 24,
+ fontWeight: '400',
+ },
+ bodySmall: {
+ fontSize: typography.bodySmall,
+ lineHeight: 20,
+ fontWeight: '400',
+ },
+ caption: {
+ fontSize: typography.caption,
+ lineHeight: 16,
+ fontWeight: '600',
+ textTransform: 'uppercase',
+ },
+ muted: {
+ color: colors.textMuted,
+ },
+});
diff --git a/mobile/components/ui/TextField.tsx b/mobile/components/ui/TextField.tsx
new file mode 100644
index 0000000..75dc876
--- /dev/null
+++ b/mobile/components/ui/TextField.tsx
@@ -0,0 +1,46 @@
+import { TextInput, TextInputProps, StyleSheet, View } from 'react-native';
+import { colors, layout, radii, spacing } from '@/constants/theme';
+import { Text } from './Text';
+
+interface TextFieldProps extends TextInputProps {
+ label?: string;
+ error?: string;
+}
+
+export function TextField({ label, error, style, ...props }: TextFieldProps) {
+ return (
+
+ {label ? {label} : null}
+
+ {error ? {error} : null}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ gap: spacing.sm,
+ },
+ input: {
+ minHeight: layout.minTouchTarget,
+ borderRadius: radii.md,
+ borderWidth: 1,
+ borderColor: colors.border,
+ backgroundColor: colors.surface,
+ color: colors.text,
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.md,
+ fontSize: 16,
+ },
+ multiline: {
+ minHeight: 92,
+ textAlignVertical: 'top',
+ },
+ error: {
+ color: colors.danger,
+ },
+});
diff --git a/mobile/components/ui/index.ts b/mobile/components/ui/index.ts
new file mode 100644
index 0000000..f9c49ad
--- /dev/null
+++ b/mobile/components/ui/index.ts
@@ -0,0 +1,10 @@
+export * from './Button';
+export * from './Chip';
+export * from './Divider';
+export * from './EmptyState';
+export * from './ListRow';
+export * from './LoadingState';
+export * from './Screen';
+export * from './SegmentedControl';
+export * from './Text';
+export * from './TextField';
diff --git a/mobile/constants/theme.ts b/mobile/constants/theme.ts
new file mode 100644
index 0000000..6df4189
--- /dev/null
+++ b/mobile/constants/theme.ts
@@ -0,0 +1,71 @@
+export const colors = {
+ background: '#0B0F0E',
+ backgroundElevated: '#101614',
+ surface: '#141C19',
+ surfaceMuted: '#1B2521',
+ surfacePressed: '#22302B',
+ border: '#26352F',
+ borderMuted: '#1C2824',
+ text: '#F4F7F5',
+ textMuted: '#A8B4AE',
+ textSubtle: '#748079',
+ accent: '#6EE7B7',
+ accentStrong: '#34D399',
+ accentMuted: '#14382B',
+ danger: '#F87171',
+ warning: '#FBBF24',
+ white: '#FFFFFF',
+ black: '#020403',
+} as const;
+
+export const spacing = {
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+ xxl: 32,
+ xxxl: 48,
+} as const;
+
+export const radii = {
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+ pill: 999,
+} as const;
+
+export const typography = {
+ hero: 38,
+ h1: 30,
+ h2: 24,
+ h3: 19,
+ body: 16,
+ bodySmall: 14,
+ caption: 12,
+} as const;
+
+export const layout = {
+ screenPadding: 20,
+ minTouchTarget: 44,
+} as const;
+
+export const shadow = {
+ card: {
+ shadowColor: colors.black,
+ shadowOpacity: 0.18,
+ shadowRadius: 18,
+ shadowOffset: { width: 0, height: 12 },
+ elevation: 3,
+ },
+} as const;
+
+export const theme = {
+ colors,
+ spacing,
+ radii,
+ typography,
+ layout,
+ shadow,
+} as const;
diff --git a/mobile/docs/ARCHITECTURE.md b/mobile/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..d27584c
--- /dev/null
+++ b/mobile/docs/ARCHITECTURE.md
@@ -0,0 +1,81 @@
+# Tera Mobile Architecture
+
+## App Architecture
+
+Tera Mobile is an Expo Router app organized around route groups and feature modules. Routes stay thin and compose feature components, hooks, state, and typed services.
+
+The root layout owns app-wide providers:
+
+- TanStack Query for server state and async service orchestration.
+- Zustand for local app state.
+- Session bootstrap from SecureStore.
+- Dark Android-first status bar and background configuration.
+
+## Folder Philosophy
+
+```text
+app/ navigation entrypoints only
+components/ shared primitives without product-specific business logic
+features/ product areas such as auth, onboarding, chat, saved, profile
+lib/ external boundaries like API and storage
+store/ lightweight client state
+types/ cross-feature domain types
+constants/ theme and app constants
+```
+
+Feature code should own product-specific components and hooks. Shared primitives should remain small and generic.
+
+## Navigation Model
+
+- `app/index.tsx` decides the first destination after hydration.
+- `(onboarding)` introduces Tera before auth.
+- `(auth)` contains sign in, sign up, and password reset.
+- `(tabs)` is the main signed-in app shell with Home, History, Saved, and Profile.
+- `conversation/[id]` is a stack route above tabs for focused chat detail.
+
+This structure supports future additions such as upload flows, voice settings, subscription screens, and notification permissions without flattening route logic.
+
+## State Management Model
+
+TanStack Query handles async server-shaped data:
+
+- Conversations
+- Conversation detail
+- Saved items
+- Future profile and subscription data
+
+Zustand handles local client state:
+
+- Session object after bootstrap
+- Onboarding completion
+- Selected chat mode
+- Lightweight preferences
+
+SecureStore stores sensitive auth session data. AsyncStorage stores non-sensitive onboarding and preference data.
+
+## API Boundary Design
+
+`lib/api/client.ts` exposes the app API boundary. Today it points to typed mock services in `lib/api/mock.ts`. The mock implementation is intentionally shallow and mirrors future backend behavior without becoming an in-app backend.
+
+When a real backend is ready:
+
+- Keep route screens and feature components unchanged where possible.
+- Replace mock service calls with typed `fetch` calls.
+- Add auth headers from SecureStore/session state.
+- Validate important response payloads with Zod at the boundary.
+
+## Auth and Session Approach
+
+Auth screens validate user input with Zod and call auth actions. Successful mock auth writes an `AuthSession` to SecureStore and updates Zustand. Sign out clears SecureStore, clears React Query cache, and returns to auth.
+
+This prepares the app for real token-based auth, refresh handling, and account profile sync later.
+
+## Scaling Strategy
+
+- Keep route files thin.
+- Put product behavior in feature modules.
+- Add shared primitives only after repeated UI needs emerge.
+- Keep domain types central and explicit.
+- Use React Query keys consistently by product area.
+- Avoid broad dependencies unless they remove real implementation risk.
+- Keep native permissions minimal until a feature needs them.
diff --git a/mobile/docs/PLAN.md b/mobile/docs/PLAN.md
new file mode 100644
index 0000000..c2db329
--- /dev/null
+++ b/mobile/docs/PLAN.md
@@ -0,0 +1,52 @@
+# Tera Mobile Product Plan
+
+## Mission
+
+Tera Mobile helps people learn deeply, research clearly, and turn knowledge into action from an Android-first mobile experience.
+
+## User Problem
+
+Students, self-learners, and builders often use general AI tools for learning, but the experience is rarely shaped around understanding. They need a fast mobile companion that can explain ideas clearly, structure research, and help convert learning into practical output.
+
+## Target Users
+
+- Students who need concepts explained in plain language.
+- Self-learners researching complex topics.
+- Builders learning while making projects, products, or study systems.
+- Mobile-first users who want quick, conversational support.
+
+## MVP Scope
+
+- Onboarding that explains Tera without marketing clutter.
+- Auth screens ready for real backend integration.
+- Chat-first home with Learn, Research, and Build modes.
+- Conversation detail screen with message states prepared for streaming.
+- History and saved work surfaces.
+- Profile/settings surface for preferences, billing, notifications, help, and sign out.
+- Typed mock data and service boundaries.
+- Reusable UI primitives and theme.
+
+## Non-Goals
+
+- No deep fake backend implementation.
+- No production AI streaming yet.
+- No voice, upload, subscription, or notification implementation yet.
+- No NativeWind configuration until there is a clear team need.
+- No root web app rewrite.
+
+## Product Principles
+
+- Learning-first: answers should explain, check understanding, and support action.
+- Calm and credible: minimal chrome, strong hierarchy, no neon AI startup aesthetic.
+- Mobile-native: thumb-friendly controls, clear navigation, keyboard-aware composition.
+- Scalable foundations: separate UI, domain, state, and data access.
+- Backend-ready: mock services must mirror real API boundaries without pretending to be the backend.
+
+## Phased Roadmap
+
+1. Foundation: navigation, docs, typed mocks, state, auth shell, chat shell.
+2. Backend integration: real auth, conversation sync, profile persistence.
+3. AI chat: streaming responses, mode-specific prompts, error recovery, citations for research.
+4. Learning depth: quizzes, saved outputs, structured study plans, memory controls.
+5. Mobile capabilities: voice input, image/file upload, push notifications.
+6. Monetization and release: subscriptions, billing state, analytics, crash reporting, Play Store readiness.
diff --git a/mobile/docs/TASKLIST.md b/mobile/docs/TASKLIST.md
new file mode 100644
index 0000000..2d58ea2
--- /dev/null
+++ b/mobile/docs/TASKLIST.md
@@ -0,0 +1,48 @@
+# Tera Mobile Tasklist
+
+## Priority 0: Foundation Verification
+
+- Run `pnpm install` in `mobile/`.
+- Run `pnpm typecheck`.
+- Start Expo and smoke-test onboarding, auth, tabs, conversation detail, and sign out.
+- Replace placeholder PNG assets with production-ready app icons and splash assets.
+
+## Priority 1: Auth and Backend
+
+- Connect sign in and sign up to the production auth backend.
+- Add token refresh and expired-session handling.
+- Add account profile fetch and profile update endpoints.
+- Add backend-backed password reset delivery.
+- Add API response validation for auth/session payloads.
+
+## Priority 2: Chat and AI
+
+- Add real conversation creation from the home composer.
+- Add streaming AI response support.
+- Persist user and assistant messages remotely.
+- Add mode-specific system prompts for Learn, Research, and Build.
+- Add retry behavior for failed messages.
+- Add citations and source handling for Research mode.
+
+## Priority 3: History and Saved Work
+
+- Sync history from the backend.
+- Add server-backed search.
+- Save and unsave conversations.
+- Add saved output types beyond conversations.
+- Add empty states for first-run and offline conditions.
+
+## Priority 4: Mobile Capabilities
+
+- Add voice input with explicit microphone permission flow.
+- Add image and file upload with scoped permissions.
+- Add push notification opt-in and reminder settings.
+- Add offline indicators and reconnect behavior.
+
+## Priority 5: Monetization and Release
+
+- Add subscription plan screens and billing state.
+- Add feature gates for paid capabilities.
+- Add analytics and crash reporting.
+- Add release profiles with EAS Build.
+- Prepare Play Store screenshots, privacy labels, and QA checklist.
diff --git a/mobile/features/auth/schemas.ts b/mobile/features/auth/schemas.ts
new file mode 100644
index 0000000..2596659
--- /dev/null
+++ b/mobile/features/auth/schemas.ts
@@ -0,0 +1,18 @@
+import { z } from 'zod';
+
+export const signInSchema = z.object({
+ email: z.string().email('Enter a valid email address.'),
+ password: z.string().min(6, 'Password must be at least 6 characters.'),
+});
+
+export const signUpSchema = signInSchema.extend({
+ name: z.string().min(2, 'Enter your name.'),
+});
+
+export const forgotPasswordSchema = z.object({
+ email: z.string().email('Enter a valid email address.'),
+});
+
+export type SignInInput = z.infer;
+export type SignUpInput = z.infer;
+export type ForgotPasswordInput = z.infer;
diff --git a/mobile/features/auth/useAuthActions.ts b/mobile/features/auth/useAuthActions.ts
new file mode 100644
index 0000000..b15655e
--- /dev/null
+++ b/mobile/features/auth/useAuthActions.ts
@@ -0,0 +1,47 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { router } from 'expo-router';
+import { teraApi } from '@/lib/api/client';
+import { clearStoredSession, saveStoredSession } from '@/lib/storage/secureSession';
+import { useAppStore } from '@/store/app-store';
+
+export function useAuthActions() {
+ const queryClient = useQueryClient();
+ const setSession = useAppStore((state) => state.setSession);
+
+ const signIn = useMutation({
+ mutationFn: ({ email }: { email: string; password: string }) => teraApi.signIn(email),
+ onSuccess: async (session) => {
+ await saveStoredSession(session);
+ setSession(session);
+ router.replace('/(tabs)');
+ },
+ });
+
+ const signUp = useMutation({
+ mutationFn: ({ name, email }: { name: string; email: string; password: string }) =>
+ teraApi.signUp(name, email),
+ onSuccess: async (session) => {
+ await saveStoredSession(session);
+ setSession(session);
+ router.replace('/(tabs)');
+ },
+ });
+
+ const resetPassword = useMutation({
+ mutationFn: () => teraApi.requestPasswordReset(),
+ });
+
+ async function signOut() {
+ await clearStoredSession();
+ setSession(null);
+ queryClient.clear();
+ router.replace('/(auth)/sign-in');
+ }
+
+ return {
+ signIn,
+ signUp,
+ resetPassword,
+ signOut,
+ };
+}
diff --git a/mobile/features/chat/Composer.tsx b/mobile/features/chat/Composer.tsx
new file mode 100644
index 0000000..9b6a9b1
--- /dev/null
+++ b/mobile/features/chat/Composer.tsx
@@ -0,0 +1,91 @@
+import { useState } from 'react';
+import { Pressable, StyleSheet, TextInput, View } from 'react-native';
+import { colors, layout, radii, spacing } from '@/constants/theme';
+import { Text } from '@/components/ui';
+
+interface ComposerProps {
+ placeholder?: string;
+ disabled?: boolean;
+ onSubmit: (value: string) => void;
+}
+
+export function Composer({
+ placeholder = 'Ask Tera anything...',
+ disabled,
+ onSubmit,
+}: ComposerProps) {
+ const [value, setValue] = useState('');
+
+ function submit() {
+ const trimmed = value.trim();
+ if (!trimmed || disabled) return;
+ setValue('');
+ onSubmit(trimmed);
+ }
+
+ return (
+
+
+ [
+ styles.send,
+ pressed && styles.pressed,
+ (!value.trim() || disabled) && styles.disabled,
+ ]}
+ >
+ Go
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrap: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ gap: spacing.sm,
+ borderRadius: radii.xl,
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.border,
+ padding: spacing.sm,
+ },
+ input: {
+ flex: 1,
+ minHeight: layout.minTouchTarget,
+ maxHeight: 116,
+ color: colors.text,
+ fontSize: 16,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.md,
+ textAlignVertical: 'top',
+ },
+ send: {
+ minWidth: 52,
+ minHeight: layout.minTouchTarget,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: radii.pill,
+ backgroundColor: colors.accent,
+ },
+ sendText: {
+ color: colors.black,
+ fontWeight: '800',
+ },
+ pressed: {
+ opacity: 0.8,
+ },
+ disabled: {
+ opacity: 0.45,
+ },
+});
diff --git a/mobile/features/chat/ConversationPreview.tsx b/mobile/features/chat/ConversationPreview.tsx
new file mode 100644
index 0000000..ce592ef
--- /dev/null
+++ b/mobile/features/chat/ConversationPreview.tsx
@@ -0,0 +1,19 @@
+import { router } from 'expo-router';
+import { ListRow } from '@/components/ui';
+import { Conversation } from '@/types/domain';
+import { formatRelativeTime } from './chat-data';
+
+interface ConversationPreviewProps {
+ conversation: Conversation;
+}
+
+export function ConversationPreview({ conversation }: ConversationPreviewProps) {
+ return (
+ router.push(`/conversation/${conversation.id}`)}
+ />
+ );
+}
diff --git a/mobile/features/chat/MessageBubble.tsx b/mobile/features/chat/MessageBubble.tsx
new file mode 100644
index 0000000..d2ae61d
--- /dev/null
+++ b/mobile/features/chat/MessageBubble.tsx
@@ -0,0 +1,54 @@
+import { StyleSheet, View } from 'react-native';
+import { colors, radii, spacing } from '@/constants/theme';
+import { Message } from '@/types/domain';
+import { Text } from '@/components/ui';
+
+interface MessageBubbleProps {
+ message: Message;
+}
+
+export function MessageBubble({ message }: MessageBubbleProps) {
+ const isUser = message.role === 'user';
+
+ return (
+
+
+ {message.content}
+ {message.status === 'streaming' ? (
+ Thinking
+ ) : null}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ row: {
+ flexDirection: 'row',
+ paddingVertical: spacing.sm,
+ },
+ userRow: {
+ justifyContent: 'flex-end',
+ },
+ bubble: {
+ maxWidth: '88%',
+ borderRadius: radii.lg,
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.md,
+ gap: spacing.sm,
+ },
+ userBubble: {
+ backgroundColor: colors.accent,
+ borderBottomRightRadius: radii.sm,
+ },
+ assistantBubble: {
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.borderMuted,
+ borderBottomLeftRadius: radii.sm,
+ },
+ userText: {
+ color: colors.black,
+ fontWeight: '600',
+ },
+});
diff --git a/mobile/features/chat/chat-data.ts b/mobile/features/chat/chat-data.ts
new file mode 100644
index 0000000..fcb52a3
--- /dev/null
+++ b/mobile/features/chat/chat-data.ts
@@ -0,0 +1,34 @@
+import { TeraMode } from '@/types/domain';
+
+export const modeOptions: Array<{ label: string; value: TeraMode }> = [
+ { label: 'Learn', value: 'learn' },
+ { label: 'Research', value: 'research' },
+ { label: 'Build', value: 'build' },
+];
+
+export const starterPrompts: Record = {
+ learn: [
+ 'Explain transformers with a simple analogy.',
+ 'Teach me calculus limits step by step.',
+ 'Quiz me on photosynthesis after explaining it.',
+ ],
+ research: [
+ 'Build a research plan for renewable energy storage.',
+ 'Compare three views on remote learning outcomes.',
+ 'Turn this topic into source questions.',
+ ],
+ build: [
+ 'Help me turn my notes into a study app.',
+ 'Design a project plan for learning Python.',
+ 'Create a launch checklist for a learning product.',
+ ],
+};
+
+export function formatRelativeTime(value: string) {
+ const diff = Date.now() - new Date(value).getTime();
+ const minutes = Math.max(1, Math.floor(diff / 60000));
+ if (minutes < 60) return `${minutes}m`;
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) return `${hours}h`;
+ return `${Math.floor(hours / 24)}d`;
+}
diff --git a/mobile/features/onboarding/onboarding-content.ts b/mobile/features/onboarding/onboarding-content.ts
new file mode 100644
index 0000000..75959e2
--- /dev/null
+++ b/mobile/features/onboarding/onboarding-content.ts
@@ -0,0 +1,14 @@
+export const onboardingSlides = [
+ {
+ title: 'Learn deeply',
+ body: 'Ask Tera for clear explanations, examples, and checks for understanding.',
+ },
+ {
+ title: 'Research clearly',
+ body: 'Shape broad questions into useful plans, source angles, and concise briefs.',
+ },
+ {
+ title: 'Build from knowledge',
+ body: 'Turn what you learn into outlines, projects, study systems, and next actions.',
+ },
+] as const;
diff --git a/mobile/hooks/useKeyboardInsets.ts b/mobile/hooks/useKeyboardInsets.ts
new file mode 100644
index 0000000..6c58c8e
--- /dev/null
+++ b/mobile/hooks/useKeyboardInsets.ts
@@ -0,0 +1,8 @@
+import { Platform } from 'react-native';
+
+export function useKeyboardInsets() {
+ return {
+ behavior: Platform.OS === 'ios' ? 'padding' as const : 'height' as const,
+ keyboardVerticalOffset: Platform.OS === 'android' ? 82 : 0,
+ };
+}
diff --git a/mobile/hooks/useSessionBootstrap.ts b/mobile/hooks/useSessionBootstrap.ts
new file mode 100644
index 0000000..bdbffaf
--- /dev/null
+++ b/mobile/hooks/useSessionBootstrap.ts
@@ -0,0 +1,30 @@
+import { useEffect } from 'react';
+import { getStoredSession } from '@/lib/storage/secureSession';
+import { useAppStore } from '@/store/app-store';
+
+export function useSessionBootstrap() {
+ const loadOnboardingState = useAppStore((state) => state.loadOnboardingState);
+ const setHydrated = useAppStore((state) => state.setHydrated);
+ const setSession = useAppStore((state) => state.setSession);
+
+ useEffect(() => {
+ let mounted = true;
+
+ async function bootstrap() {
+ const [session] = await Promise.all([
+ getStoredSession(),
+ loadOnboardingState(),
+ ]);
+
+ if (!mounted) return;
+ setSession(session);
+ setHydrated(true);
+ }
+
+ bootstrap();
+
+ return () => {
+ mounted = false;
+ };
+ }, [loadOnboardingState, setHydrated, setSession]);
+}
diff --git a/mobile/lib/api.ts b/mobile/lib/api.ts
deleted file mode 100644
index 4ee8c1b..0000000
--- a/mobile/lib/api.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import axios, { AxiosInstance, AxiosError } from 'axios';
-import * as SecureStore from 'expo-secure-store';
-
-const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:5000';
-
-export interface AuthResponse {
- success: boolean;
- data?: {
- token: string;
- user: {
- id: string;
- email: string;
- name: string;
- provider: string;
- };
- };
- error?: string;
-}
-
-export interface ChatResponse {
- success: boolean;
- data?: {
- message: string;
- timestamp: string;
- };
- error?: string;
-}
-
-export interface SessionResponse {
- success: boolean;
- data?: any;
- error?: string;
-}
-
-export class TeraAPI {
- private client: AxiosInstance;
-
- constructor() {
- this.client = axios.create({
- baseURL: `${API_BASE_URL}/api`,
- timeout: 30000,
- });
-
- // Add auth token to requests
- this.client.interceptors.request.use(async config => {
- try {
- const token = await SecureStore.getItemAsync('auth_token');
- if (token) {
- config.headers.Authorization = `Bearer ${token}`;
- }
- } catch (error) {
- console.error('Error retrieving auth token:', error);
- }
- return config;
- });
-
- // Handle response errors
- this.client.interceptors.response.use(
- response => response,
- (error: AxiosError) => {
- console.error('API error:', error.response?.data || error.message);
- return Promise.reject(error);
- }
- );
- }
-
- // ============ AUTH ============
-
- async signIn(email: string, password: string): Promise {
- try {
- const response = await this.client.post('/auth/signin', {
- email,
- password,
- });
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Sign in failed',
- };
- }
- }
-
- async signUp(
- email: string,
- name: string,
- password: string
- ): Promise {
- try {
- const response = await this.client.post('/auth/signup', {
- email,
- name,
- password,
- });
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Sign up failed',
- };
- }
- }
-
- async googleAuth(idToken: string): Promise {
- try {
- const response = await this.client.post('/auth/google', {
- idToken,
- });
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Google authentication failed',
- };
- }
- }
-
- // ============ CHAT ============
-
- async sendMessage(
- sessionId: string,
- message: string,
- chatHistory: Array<{ role: string; content: string }>
- ): Promise {
- try {
- const response = await this.client.post(
- '/chat/messages',
- {
- sessionId,
- message,
- chatHistory,
- }
- );
- return response.data;
- } catch (error) {
- const axiosError = error as AxiosError;
- return {
- success: false,
- error:
- (axiosError.response?.data as any)?.error ||
- 'Failed to send message',
- };
- }
- }
-
- async getChatHistory(sessionId: string): Promise {
- try {
- const response = await this.client.get(
- `/chat/sessions/${sessionId}`
- );
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Failed to fetch chat history',
- };
- }
- }
-
- async createSession(title: string): Promise {
- try {
- const response = await this.client.post(
- '/chat/sessions',
- { title }
- );
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Failed to create session',
- };
- }
- }
-
- async listSessions(): Promise {
- try {
- const response = await this.client.get(
- '/chat/sessions'
- );
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Failed to fetch sessions',
- };
- }
- }
-
- // ============ TOOLS ============
-
- async getTools(): Promise {
- try {
- const response = await this.client.get('/tools');
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Failed to fetch tools',
- };
- }
- }
-
- async processTool(toolId: string, input: any): Promise {
- try {
- const response = await this.client.post(
- `/tools/${toolId}/process`,
- input
- );
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Failed to process tool',
- };
- }
- }
-
- // ============ SEARCH ============
-
- async webSearch(query: string): Promise {
- try {
- const response = await this.client.post(
- '/search/web',
- { query }
- );
- return response.data;
- } catch (error) {
- return {
- success: false,
- error: 'Search failed',
- };
- }
- }
-}
-
-export const teraAPI = new TeraAPI();
diff --git a/mobile/lib/api/client.ts b/mobile/lib/api/client.ts
new file mode 100644
index 0000000..0c50b8d
--- /dev/null
+++ b/mobile/lib/api/client.ts
@@ -0,0 +1,36 @@
+import { mockApi } from './mock';
+
+const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL;
+
+export interface ApiResult {
+ data: T;
+ status: number;
+}
+
+export async function apiRequest(
+ path: string,
+ init?: RequestInit,
+): Promise> {
+ if (!API_BASE_URL) {
+ throw new Error('EXPO_PUBLIC_API_URL is not configured.');
+ }
+
+ const response = await fetch(`${API_BASE_URL}${path}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ ...init?.headers,
+ },
+ ...init,
+ });
+
+ if (!response.ok) {
+ throw new Error(`API request failed with status ${response.status}`);
+ }
+
+ return {
+ data: await response.json() as T,
+ status: response.status,
+ };
+}
+
+export const teraApi = mockApi;
diff --git a/mobile/lib/api/mock.ts b/mobile/lib/api/mock.ts
new file mode 100644
index 0000000..bd6b1ba
--- /dev/null
+++ b/mobile/lib/api/mock.ts
@@ -0,0 +1,144 @@
+import { AuthSession, Conversation, SavedItem, TeraMode, User } from '@/types/domain';
+
+const now = new Date();
+
+export const mockUser: User = {
+ id: 'user_tera_foundation',
+ name: 'Tera Learner',
+ email: 'learner@tera.ai',
+ plan: 'free',
+};
+
+export const mockConversations: Conversation[] = [
+ {
+ id: 'conv_learning_systems',
+ title: 'Understanding spaced repetition',
+ mode: 'learn',
+ summary: 'A clear breakdown of active recall, intervals, and review design.',
+ updatedAt: new Date(now.getTime() - 1000 * 60 * 18).toISOString(),
+ isSaved: true,
+ messages: [
+ {
+ id: 'msg_1',
+ conversationId: 'conv_learning_systems',
+ role: 'user',
+ content: 'Explain spaced repetition like I am building a study plan.',
+ createdAt: new Date(now.getTime() - 1000 * 60 * 20).toISOString(),
+ status: 'sent',
+ },
+ {
+ id: 'msg_2',
+ conversationId: 'conv_learning_systems',
+ role: 'assistant',
+ content: 'Spaced repetition works by reviewing an idea right before you are likely to forget it. For a study plan, treat each concept as a card: learn it once, test yourself soon, then widen the review interval when recall is strong.',
+ createdAt: new Date(now.getTime() - 1000 * 60 * 18).toISOString(),
+ status: 'sent',
+ },
+ ],
+ },
+ {
+ id: 'conv_research_brief',
+ title: 'Research plan for climate adaptation',
+ mode: 'research',
+ summary: 'Questions, source types, and a structure for a focused brief.',
+ updatedAt: new Date(now.getTime() - 1000 * 60 * 60 * 5).toISOString(),
+ isSaved: false,
+ messages: [
+ {
+ id: 'msg_3',
+ conversationId: 'conv_research_brief',
+ role: 'user',
+ content: 'Help me research climate adaptation for coastal cities.',
+ createdAt: new Date(now.getTime() - 1000 * 60 * 60 * 5).toISOString(),
+ status: 'sent',
+ },
+ ],
+ },
+ {
+ id: 'conv_build_learning_app',
+ title: 'Build a quiz flow for biology',
+ mode: 'build',
+ summary: 'A practical outline for turning notes into a quiz experience.',
+ updatedAt: new Date(now.getTime() - 1000 * 60 * 60 * 24).toISOString(),
+ isSaved: true,
+ messages: [
+ {
+ id: 'msg_4',
+ conversationId: 'conv_build_learning_app',
+ role: 'assistant',
+ content: 'Start with learning goals, extract question candidates, then generate feedback for each wrong answer so the quiz teaches rather than only scores.',
+ createdAt: new Date(now.getTime() - 1000 * 60 * 60 * 24).toISOString(),
+ status: 'sent',
+ },
+ ],
+ },
+];
+
+export const mockSavedItems: SavedItem[] = mockConversations
+ .filter((conversation) => conversation.isSaved)
+ .map((conversation) => ({
+ id: `saved_${conversation.id}`,
+ conversationId: conversation.id,
+ title: conversation.title,
+ excerpt: conversation.summary,
+ mode: conversation.mode,
+ savedAt: conversation.updatedAt,
+ }));
+
+function delay(value: T, ms = 280): Promise {
+ return new Promise((resolve) => setTimeout(() => resolve(value), ms));
+}
+
+function modeAnswer(mode: TeraMode, prompt: string) {
+ const opening = {
+ learn: 'Here is the simplest useful model:',
+ research: 'Here is a research-ready way to frame it:',
+ build: 'Here is a practical build path:',
+ }[mode];
+
+ return `${opening} ${prompt.trim()} can be broken into the core idea, the evidence or constraints, and the next action. In the real backend, this response will come from TeraAI with streaming, citations, and memory-aware context.`;
+}
+
+export const mockApi = {
+ async signIn(email: string): Promise {
+ return delay({
+ token: 'mock_tera_mobile_token',
+ user: {
+ ...mockUser,
+ email,
+ },
+ });
+ },
+ async signUp(name: string, email: string): Promise {
+ return delay({
+ token: 'mock_tera_mobile_token',
+ user: {
+ ...mockUser,
+ name,
+ email,
+ },
+ });
+ },
+ async requestPasswordReset(): Promise<{ ok: true }> {
+ return delay({ ok: true });
+ },
+ async getConversations(): Promise {
+ return delay(mockConversations);
+ },
+ async getConversation(id: string): Promise {
+ return delay(mockConversations.find((conversation) => conversation.id === id) ?? null);
+ },
+ async getSavedItems(): Promise {
+ return delay(mockSavedItems);
+ },
+ async sendMessage(conversationId: string, mode: TeraMode, prompt: string) {
+ return delay({
+ id: `msg_${Date.now()}`,
+ conversationId,
+ role: 'assistant' as const,
+ content: modeAnswer(mode, prompt),
+ createdAt: new Date().toISOString(),
+ status: 'sent' as const,
+ }, 520);
+ },
+};
diff --git a/mobile/lib/storage.ts b/mobile/lib/storage.ts
deleted file mode 100644
index 295f3f7..0000000
--- a/mobile/lib/storage.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import AsyncStorage from '@react-native-async-storage/async-storage';
-
-const SESSIONS_KEY = '@tera_sessions';
-const MESSAGES_PREFIX = '@tera_messages_';
-
-export interface Message {
- id: string;
- role: 'user' | 'assistant';
- content: string;
- timestamp: number;
-}
-
-export interface Session {
- id: string;
- title: string;
- createdAt: number;
- updatedAt: number;
-}
-
-// ============ SESSIONS ============
-
-export async function saveSession(session: Session): Promise {
- try {
- const sessions = await getSessions();
- const existing = sessions.findIndex(s => s.id === session.id);
-
- if (existing >= 0) {
- sessions[existing] = session;
- } else {
- sessions.push(session);
- }
-
- await AsyncStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions));
- } catch (error) {
- console.error('Error saving session:', error);
- }
-}
-
-export async function getSessions(): Promise {
- try {
- const data = await AsyncStorage.getItem(SESSIONS_KEY);
- return data ? JSON.parse(data) : [];
- } catch (error) {
- console.error('Error getting sessions:', error);
- return [];
- }
-}
-
-export async function deleteSession(sessionId: string): Promise {
- try {
- const sessions = await getSessions();
- const filtered = sessions.filter(s => s.id !== sessionId);
- await AsyncStorage.setItem(SESSIONS_KEY, JSON.stringify(filtered));
-
- // Also delete messages
- await AsyncStorage.removeItem(MESSAGES_PREFIX + sessionId);
- } catch (error) {
- console.error('Error deleting session:', error);
- }
-}
-
-// ============ MESSAGES ============
-
-export async function saveMessage(
- sessionId: string,
- message: Message
-): Promise {
- try {
- const messages = await getMessages(sessionId);
- messages.push(message);
- await AsyncStorage.setItem(
- MESSAGES_PREFIX + sessionId,
- JSON.stringify(messages)
- );
- } catch (error) {
- console.error('Error saving message:', error);
- }
-}
-
-export async function getMessages(sessionId: string): Promise {
- try {
- const data = await AsyncStorage.getItem(MESSAGES_PREFIX + sessionId);
- return data ? JSON.parse(data) : [];
- } catch (error) {
- console.error('Error getting messages:', error);
- return [];
- }
-}
-
-export async function clearMessages(sessionId: string): Promise {
- try {
- await AsyncStorage.removeItem(MESSAGES_PREFIX + sessionId);
- } catch (error) {
- console.error('Error clearing messages:', error);
- }
-}
-
-// ============ USER ============
-
-export async function saveUser(userId: string, userData: any): Promise {
- try {
- await AsyncStorage.setItem('@tera_user', JSON.stringify({ userId, ...userData }));
- } catch (error) {
- console.error('Error saving user:', error);
- }
-}
-
-export async function getUser(): Promise {
- try {
- const data = await AsyncStorage.getItem('@tera_user');
- return data ? JSON.parse(data) : null;
- } catch (error) {
- console.error('Error getting user:', error);
- return null;
- }
-}
-
-export async function clearUser(): Promise {
- try {
- await AsyncStorage.removeItem('@tera_user');
- } catch (error) {
- console.error('Error clearing user:', error);
- }
-}
-
-// ============ GENERAL ============
-
-export async function clearAllData(): Promise {
- try {
- const keys = await AsyncStorage.getAllKeys();
- const teraKeys = keys.filter(key => key.startsWith('@tera_'));
- await AsyncStorage.multiRemove(teraKeys);
- } catch (error) {
- console.error('Error clearing all data:', error);
- }
-}
diff --git a/mobile/lib/storage/secureSession.ts b/mobile/lib/storage/secureSession.ts
new file mode 100644
index 0000000..841e04b
--- /dev/null
+++ b/mobile/lib/storage/secureSession.ts
@@ -0,0 +1,24 @@
+import * as SecureStore from 'expo-secure-store';
+import { AuthSession } from '@/types/domain';
+
+const SESSION_KEY = 'tera.auth.session';
+
+export async function getStoredSession(): Promise {
+ const raw = await SecureStore.getItemAsync(SESSION_KEY);
+ if (!raw) return null;
+
+ try {
+ return JSON.parse(raw) as AuthSession;
+ } catch {
+ await clearStoredSession();
+ return null;
+ }
+}
+
+export async function saveStoredSession(session: AuthSession): Promise {
+ await SecureStore.setItemAsync(SESSION_KEY, JSON.stringify(session));
+}
+
+export async function clearStoredSession(): Promise {
+ await SecureStore.deleteItemAsync(SESSION_KEY);
+}
diff --git a/mobile/lib/types.ts b/mobile/lib/types.ts
deleted file mode 100644
index bc9a6c0..0000000
--- a/mobile/lib/types.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-export interface User {
- id: string;
- email: string;
- name: string;
- provider: 'google' | 'email';
- plan: 'free' | 'pro' | 'plus';
-}
-
-export interface ChatSession {
- id: string;
- title: string;
- createdAt: Date;
- updatedAt: Date;
-}
-
-export interface ChatMessage {
- id: string;
- role: 'user' | 'assistant';
- content: string;
- timestamp: Date;
-}
-
-export interface Tool {
- id: string;
- name: string;
- description: string;
- icon: string;
- category: string;
-}
diff --git a/mobile/package.json b/mobile/package.json
index 160f438..aa6f1dd 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -9,22 +9,24 @@
"ios": "expo start --ios",
"web": "expo start --web",
"test": "jest",
- "lint": "eslint src --ext .ts,.tsx"
+ "typecheck": "tsc --noEmit"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.23.1",
- "axios": "^1.7.7",
+ "@tanstack/react-query": "^5.62.7",
"expo": "^51.0.0",
"expo-constants": "^16.0.2",
"expo-font": "^12.0.9",
"expo-router": "^3.5.0",
"expo-secure-store": "^13.0.2",
"expo-splash-screen": "^0.27.5",
- "react": "^18.2.0",
+ "react": "18.2.0",
"react-native": "^0.74.1",
"react-native-gesture-handler": "^2.14.1",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
- "react-native-web": "~0.19.10"
+ "react-native-web": "~0.19.10",
+ "zod": "^3.24.1",
+ "zustand": "^5.0.2"
},
"devDependencies": {
"@types/node": "^20.10.6",
diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml
index a6027c8..ac8f91a 100644
--- a/mobile/pnpm-lock.yaml
+++ b/mobile/pnpm-lock.yaml
@@ -10,43 +10,49 @@ importers:
dependencies:
'@react-native-async-storage/async-storage':
specifier: ^1.23.1
- version: 1.24.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))
- axios:
- specifier: ^1.7.7
- version: 1.13.2
+ version: 1.24.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))
+ '@tanstack/react-query':
+ specifier: ^5.62.7
+ version: 5.99.0(react@18.2.0)
expo:
specifier: ^51.0.0
- version: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ version: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
expo-constants:
specifier: ^16.0.2
- version: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ version: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
expo-font:
specifier: ^12.0.9
- version: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ version: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
expo-router:
specifier: ^3.5.0
- version: 3.5.24(42f160f530a514a5eb4fe3dff6eb3195)
+ version: 3.5.24(87c2223808c0ebc7b87d33c7c27d3b97)
expo-secure-store:
specifier: ^13.0.2
- version: 13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ version: 13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
expo-splash-screen:
specifier: ^0.27.5
- version: 0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ version: 0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
react:
- specifier: ^18.2.0
- version: 18.3.1
+ specifier: 18.2.0
+ version: 18.2.0
react-native:
specifier: ^0.74.1
- version: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ version: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
react-native-gesture-handler:
specifier: ^2.14.1
- version: 2.30.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ version: 2.30.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
react-native-keyboard-aware-scroll-view:
specifier: ^0.9.5
- version: 0.9.5(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))
+ version: 0.9.5(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))
react-native-web:
specifier: ~0.19.10
- version: 0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: 0.19.13(react-dom@18.3.1(react@18.2.0))(react@18.2.0)
+ zod:
+ specifier: ^3.24.1
+ version: 3.25.76
+ zustand:
+ specifier: ^5.0.2
+ version: 5.0.12(@types/react@18.3.27)(react@18.2.0)
devDependencies:
'@types/node':
specifier: ^20.10.6
@@ -56,7 +62,7 @@ importers:
version: 18.3.27
'@types/react-native':
specifier: ^0.73.0
- version: 0.73.0(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ version: 0.73.0(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
typescript:
specifier: ^5.5.4
version: 5.9.3
@@ -1230,6 +1236,14 @@ packages:
'@sinonjs/fake-timers@10.3.0':
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
+ '@tanstack/query-core@5.99.0':
+ resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==}
+
+ '@tanstack/react-query@5.99.0':
+ resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==}
+ peerDependencies:
+ react: ^18 || ^19
+
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@@ -1445,9 +1459,6 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
- axios@1.13.2:
- resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
-
babel-core@7.0.0-bridge.0:
resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==}
peerDependencies:
@@ -2178,15 +2189,6 @@ packages:
resolution: {integrity: sha512-M4GVdl9SIKQEGULoEh/PO5K1REnXvHT6XOEthuKMUDWsLCi576mOWo3Xe8BfKdy2e2aMaW5rKGfMDlMDOA9RqA==}
engines: {node: '>=0.4.0'}
- follow-redirects@1.15.11:
- resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
- engines: {node: '>=4.0'}
- peerDependencies:
- debug: '*'
- peerDependenciesMeta:
- debug:
- optional: true
-
fontfaceobserver@2.3.0:
resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
@@ -2202,10 +2204,6 @@ packages:
resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==}
engines: {node: '>= 6'}
- form-data@4.0.5:
- resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
- engines: {node: '>= 6'}
-
freeport-async@2.0.0:
resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==}
engines: {node: '>=8'}
@@ -3413,9 +3411,6 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
- proxy-from-env@1.1.0:
- resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
-
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3541,8 +3536,8 @@ packages:
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
- react@18.3.1:
- resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ react@18.2.0:
+ resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
@@ -4406,6 +4401,24 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
+ zustand@5.0.12:
+ resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
'@babel/code-frame@7.10.4':
@@ -5583,9 +5596,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@expo/metro-runtime@3.2.3(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))':
+ '@expo/metro-runtime@3.2.3(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))':
dependencies:
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
'@expo/osascript@2.3.8':
dependencies:
@@ -5661,11 +5674,11 @@ snapshots:
'@expo/sudo-prompt@9.3.2': {}
- '@expo/vector-icons@14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@expo/vector-icons@14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
- expo-font: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ expo-font: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
'@expo/xcpretty@4.3.2':
dependencies:
@@ -5791,21 +5804,21 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
- '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)':
+ '@radix-ui/react-compose-refs@1.0.0(react@18.2.0)':
dependencies:
'@babel/runtime': 7.28.4
- react: 18.3.1
+ react: 18.2.0
- '@radix-ui/react-slot@1.0.1(react@18.3.1)':
+ '@radix-ui/react-slot@1.0.1(react@18.2.0)':
dependencies:
'@babel/runtime': 7.28.4
- '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1)
- react: 18.3.1
+ '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
+ react: 18.2.0
- '@react-native-async-storage/async-storage@1.24.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))':
+ '@react-native-async-storage/async-storage@1.24.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))':
dependencies:
merge-options: 3.0.4
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
'@react-native-community/cli-clean@13.6.9':
dependencies:
@@ -6181,61 +6194,61 @@ snapshots:
'@react-native/normalize-colors@0.74.89': {}
- '@react-native/virtualized-lists@0.74.89(@types/react@18.3.27)(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@react-native/virtualized-lists@0.74.89(@types/react@18.3.27)(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
invariant: 2.2.4
nullthrows: 1.1.1
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
optionalDependencies:
'@types/react': 18.3.27
- '@react-navigation/bottom-tabs@6.5.20(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@react-navigation/bottom-tabs@6.5.20(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
- '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
color: 4.2.3
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
- react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
warn-once: 0.1.1
- '@react-navigation/core@6.4.17(react@18.3.1)':
+ '@react-navigation/core@6.4.17(react@18.2.0)':
dependencies:
'@react-navigation/routers': 6.1.9
escape-string-regexp: 4.0.0
nanoid: 3.3.11
query-string: 7.1.3
- react: 18.3.1
+ react: 18.2.0
react-is: 16.13.1
- use-latest-callback: 0.2.6(react@18.3.1)
+ use-latest-callback: 0.2.6(react@18.2.0)
- '@react-navigation/elements@1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@react-navigation/elements@1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
- '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
- react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
- '@react-navigation/native-stack@6.9.26(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@react-navigation/native-stack@6.9.26(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
- '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
- react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
warn-once: 0.1.1
- '@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)':
+ '@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)':
dependencies:
- '@react-navigation/core': 6.4.17(react@18.3.1)
+ '@react-navigation/core': 6.4.17(react@18.2.0)
escape-string-regexp: 4.0.0
fast-deep-equal: 3.1.3
nanoid: 3.3.11
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
'@react-navigation/routers@6.1.9':
dependencies:
@@ -6329,6 +6342,13 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
+ '@tanstack/query-core@5.99.0': {}
+
+ '@tanstack/react-query@5.99.0(react@18.2.0)':
+ dependencies:
+ '@tanstack/query-core': 5.99.0
+ react: 18.2.0
+
'@types/cookie@0.6.0': {}
'@types/hammerjs@2.0.46': {}
@@ -6364,9 +6384,9 @@ snapshots:
'@types/prop-types@15.7.15': {}
- '@types/react-native@0.73.0(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)':
+ '@types/react-native@0.73.0(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)':
dependencies:
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
transitivePeerDependencies:
- '@babel/core'
- '@babel/preset-env'
@@ -6541,14 +6561,6 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
- axios@1.13.2:
- dependencies:
- follow-redirects: 1.15.11
- form-data: 4.0.5
- proxy-from-env: 1.1.0
- transitivePeerDependencies:
- - debug
-
babel-core@7.0.0-bridge.0(@babel/core@7.28.5):
dependencies:
'@babel/core': 7.28.5
@@ -7180,51 +7192,51 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
- expo-asset@10.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-asset@10.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
invariant: 2.2.4
md5-file: 3.2.3
transitivePeerDependencies:
- supports-color
- expo-constants@16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-constants@16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
'@expo/config': 9.0.4
'@expo/env': 0.3.0
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
transitivePeerDependencies:
- supports-color
- expo-constants@18.0.12(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)):
+ expo-constants@18.0.12(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)):
dependencies:
'@expo/config': 12.0.13
'@expo/env': 2.0.8
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
transitivePeerDependencies:
- supports-color
- expo-file-system@17.0.1(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-file-system@17.0.1(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
- expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
fontfaceobserver: 2.3.0
- expo-keep-awake@13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-keep-awake@13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
- expo-linking@8.0.11(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ expo-linking@8.0.11(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
- expo-constants: 18.0.12(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))
+ expo-constants: 18.0.12(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))
invariant: 2.2.4
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
transitivePeerDependencies:
- expo
- supports-color
@@ -7243,22 +7255,22 @@ snapshots:
dependencies:
invariant: 2.2.4
- expo-router@3.5.24(42f160f530a514a5eb4fe3dff6eb3195):
+ expo-router@3.5.24(87c2223808c0ebc7b87d33c7c27d3b97):
dependencies:
- '@expo/metro-runtime': 3.2.3(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))
+ '@expo/metro-runtime': 3.2.3(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))
'@expo/server': 0.4.4(typescript@5.9.3)
- '@radix-ui/react-slot': 1.0.1(react@18.3.1)
- '@react-navigation/bottom-tabs': 6.5.20(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- '@react-navigation/native-stack': 6.9.26(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- expo-linking: 8.0.11(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- expo-splash-screen: 0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- expo-status-bar: 3.0.9(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react-native-helmet-async: 2.0.4(react@18.3.1)
- react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
- react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@radix-ui/react-slot': 1.0.1(react@18.2.0)
+ '@react-navigation/bottom-tabs': 6.5.20(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ '@react-navigation/native': 6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ '@react-navigation/native-stack': 6.9.26(@react-navigation/native@6.1.18(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ expo-linking: 8.0.11(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ expo-splash-screen: 0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ expo-status-bar: 3.0.9(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react-native-helmet-async: 2.0.4(react@18.2.0)
+ react-native-safe-area-context: 5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
+ react-native-screens: 4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
schema-utils: 4.3.3
transitivePeerDependencies:
- encoding
@@ -7268,38 +7280,38 @@ snapshots:
- supports-color
- typescript
- expo-secure-store@13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-secure-store@13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
- expo-splash-screen@0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)):
+ expo-splash-screen@0.27.7(expo-modules-autolinking@1.11.3)(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)):
dependencies:
'@expo/prebuild-config': 7.0.9(expo-modules-autolinking@1.11.3)
- expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ expo: 51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
transitivePeerDependencies:
- encoding
- expo-modules-autolinking
- supports-color
- expo-status-bar@3.0.9(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ expo-status-bar@3.0.9(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
- react-native-is-edge-to-edge: 1.2.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
+ react-native-is-edge-to-edge: 1.2.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
- expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.28.4
'@expo/cli': 0.18.31(expo-modules-autolinking@1.11.3)
'@expo/config': 9.0.4
'@expo/config-plugins': 8.0.11
'@expo/metro-config': 0.18.11
- '@expo/vector-icons': 14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@expo/vector-icons': 14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
babel-preset-expo: 11.0.15(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))
- expo-asset: 10.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- expo-file-system: 17.0.1(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- expo-font: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
- expo-keep-awake: 13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))
+ expo-asset: 10.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ expo-file-system: 17.0.1(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ expo-font: 12.0.10(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
+ expo-keep-awake: 13.0.2(expo@51.0.39(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0))
expo-modules-autolinking: 1.11.3
expo-modules-core: 1.12.26
fbemitter: 3.0.0
@@ -7414,8 +7426,6 @@ snapshots:
flow-parser@0.295.0: {}
- follow-redirects@1.15.11: {}
-
fontfaceobserver@2.3.0: {}
for-each@0.3.5:
@@ -7435,14 +7445,6 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
- form-data@4.0.5:
- dependencies:
- asynckit: 0.4.0
- combined-stream: 1.0.8
- es-set-tostringtag: 2.1.0
- hasown: 2.0.2
- mime-types: 2.1.35
-
freeport-async@2.0.0: {}
fresh@0.5.2: {}
@@ -8739,8 +8741,6 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
- proxy-from-env@1.1.0: {}
-
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -8782,17 +8782,17 @@ snapshots:
- bufferutil
- utf-8-validate
- react-dom@18.3.1(react@18.3.1):
+ react-dom@18.3.1(react@18.2.0):
dependencies:
loose-envify: 1.4.0
- react: 18.3.1
+ react: 18.2.0
scheduler: 0.23.2
react-fast-compare@3.2.2: {}
- react-freeze@1.0.4(react@18.3.1):
+ react-freeze@1.0.4(react@18.2.0):
dependencies:
- react: 18.3.1
+ react: 18.2.0
react-is@16.13.1: {}
@@ -8800,49 +8800,49 @@ snapshots:
react-is@18.3.1: {}
- react-native-gesture-handler@2.30.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ react-native-gesture-handler@2.30.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
'@egjs/hammerjs': 2.0.17
hoist-non-react-statics: 3.3.2
invariant: 2.2.4
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
- react-native-helmet-async@2.0.4(react@18.3.1):
+ react-native-helmet-async@2.0.4(react@18.2.0):
dependencies:
invariant: 2.2.4
- react: 18.3.1
+ react: 18.2.0
react-fast-compare: 3.2.2
shallowequal: 1.1.0
- react-native-iphone-x-helper@1.3.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)):
+ react-native-iphone-x-helper@1.3.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)):
dependencies:
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
- react-native-is-edge-to-edge@1.2.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ react-native-is-edge-to-edge@1.2.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
- react-native-keyboard-aware-scroll-view@0.9.5(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)):
+ react-native-keyboard-aware-scroll-view@0.9.5(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)):
dependencies:
prop-types: 15.8.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
- react-native-iphone-x-helper: 1.3.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
+ react-native-iphone-x-helper: 1.3.1(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))
- react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ react-native-safe-area-context@5.6.2(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
- react: 18.3.1
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
- react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1):
+ react-native-screens@4.19.0(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0):
dependencies:
- react: 18.3.1
- react-freeze: 1.0.4(react@18.3.1)
- react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1)
+ react: 18.2.0
+ react-freeze: 1.0.4(react@18.2.0)
+ react-native: 0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0)
warn-once: 0.1.1
- react-native-web@0.19.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ react-native-web@0.19.13(react-dom@18.3.1(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.28.4
'@react-native/normalize-colors': 0.74.89
@@ -8851,13 +8851,13 @@ snapshots:
memoize-one: 6.0.0
nullthrows: 1.1.1
postcss-value-parser: 4.2.0
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
+ react: 18.2.0
+ react-dom: 18.3.1(react@18.2.0)
styleq: 0.1.3
transitivePeerDependencies:
- encoding
- react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1):
+ react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0):
dependencies:
'@jest/create-cache-key-function': 29.7.0
'@react-native-community/cli': 13.6.9
@@ -8869,7 +8869,7 @@ snapshots:
'@react-native/gradle-plugin': 0.74.89
'@react-native/js-polyfills': 0.74.89
'@react-native/normalize-colors': 0.74.89
- '@react-native/virtualized-lists': 0.74.89(@types/react@18.3.27)(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.3.1))(react@18.3.1)
+ '@react-native/virtualized-lists': 0.74.89(@types/react@18.3.27)(react-native@0.74.7(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(react@18.2.0))(react@18.2.0)
abort-controller: 3.0.0
anser: 1.4.10
ansi-regex: 5.0.1
@@ -8888,10 +8888,10 @@ snapshots:
nullthrows: 1.1.1
pretty-format: 26.6.2
promise: 8.3.0
- react: 18.3.1
+ react: 18.2.0
react-devtools-core: 5.3.2
react-refresh: 0.14.2
- react-shallow-renderer: 16.15.0(react@18.3.1)
+ react-shallow-renderer: 16.15.0(react@18.2.0)
regenerator-runtime: 0.13.11
scheduler: 0.24.0-canary-efb381bbf-20230505
stacktrace-parser: 0.1.11
@@ -8910,13 +8910,13 @@ snapshots:
react-refresh@0.14.2: {}
- react-shallow-renderer@16.15.0(react@18.3.1):
+ react-shallow-renderer@16.15.0(react@18.2.0):
dependencies:
object-assign: 4.1.1
- react: 18.3.1
+ react: 18.2.0
react-is: 18.3.1
- react@18.3.1:
+ react@18.2.0:
dependencies:
loose-envify: 1.4.0
@@ -9605,9 +9605,9 @@ snapshots:
url-join@4.0.0: {}
- use-latest-callback@0.2.6(react@18.3.1):
+ use-latest-callback@0.2.6(react@18.2.0):
dependencies:
- react: 18.3.1
+ react: 18.2.0
util-deprecate@1.0.2: {}
@@ -9825,3 +9825,8 @@ snapshots:
zod: 3.25.76
zod@3.25.76: {}
+
+ zustand@5.0.12(@types/react@18.3.27)(react@18.2.0):
+ optionalDependencies:
+ '@types/react': 18.3.27
+ react: 18.2.0
diff --git a/mobile/store/app-store.ts b/mobile/store/app-store.ts
new file mode 100644
index 0000000..4030115
--- /dev/null
+++ b/mobile/store/app-store.ts
@@ -0,0 +1,50 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { create } from 'zustand';
+import { AuthSession, TeraMode } from '@/types/domain';
+
+const ONBOARDING_KEY = 'tera.onboarding.complete';
+
+interface Preferences {
+ conciseAnswers: boolean;
+ notificationsEnabled: boolean;
+}
+
+interface AppState {
+ hydrated: boolean;
+ onboardingComplete: boolean;
+ session: AuthSession | null;
+ selectedMode: TeraMode;
+ preferences: Preferences;
+ setHydrated: (hydrated: boolean) => void;
+ setSession: (session: AuthSession | null) => void;
+ setSelectedMode: (mode: TeraMode) => void;
+ setPreferences: (preferences: Partial) => void;
+ completeOnboarding: () => Promise;
+ loadOnboardingState: () => Promise;
+}
+
+export const useAppStore = create((set) => ({
+ hydrated: false,
+ onboardingComplete: false,
+ session: null,
+ selectedMode: 'learn',
+ preferences: {
+ conciseAnswers: false,
+ notificationsEnabled: false,
+ },
+ setHydrated: (hydrated) => set({ hydrated }),
+ setSession: (session) => set({ session }),
+ setSelectedMode: (selectedMode) => set({ selectedMode }),
+ setPreferences: (preferences) =>
+ set((state) => ({ preferences: { ...state.preferences, ...preferences } })),
+ completeOnboarding: async () => {
+ await AsyncStorage.setItem(ONBOARDING_KEY, 'true');
+ set({ onboardingComplete: true });
+ },
+ loadOnboardingState: async () => {
+ const value = await AsyncStorage.getItem(ONBOARDING_KEY);
+ const onboardingComplete = value === 'true';
+ set({ onboardingComplete });
+ return onboardingComplete;
+ },
+}));
diff --git a/mobile/types/domain.ts b/mobile/types/domain.ts
new file mode 100644
index 0000000..68c6542
--- /dev/null
+++ b/mobile/types/domain.ts
@@ -0,0 +1,43 @@
+export type TeraMode = 'learn' | 'research' | 'build';
+
+export type MessageRole = 'user' | 'assistant' | 'system';
+
+export interface User {
+ id: string;
+ name: string;
+ email: string;
+ plan: 'free' | 'plus' | 'pro';
+}
+
+export interface AuthSession {
+ token: string;
+ user: User;
+}
+
+export interface Message {
+ id: string;
+ conversationId: string;
+ role: MessageRole;
+ content: string;
+ createdAt: string;
+ status?: 'sending' | 'streaming' | 'sent' | 'failed';
+}
+
+export interface Conversation {
+ id: string;
+ title: string;
+ mode: TeraMode;
+ summary: string;
+ updatedAt: string;
+ isSaved: boolean;
+ messages: Message[];
+}
+
+export interface SavedItem {
+ id: string;
+ conversationId: string;
+ title: string;
+ excerpt: string;
+ mode: TeraMode;
+ savedAt: string;
+}