From 6fbfbab4adb0ac762393190dddfbdc7ffb6c3549 Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 18 Apr 2026 21:04:49 +0100 Subject: [PATCH] build-tera-mobile-foundation --- mobile/README.md | 61 +++ mobile/app.json | 11 +- mobile/app/(app)/_layout.tsx | 54 --- mobile/app/(app)/chat.tsx | 296 -------------- mobile/app/(app)/settings.tsx | 194 --------- mobile/app/(app)/tool/[id].tsx | 231 ----------- mobile/app/(app)/tools.tsx | 151 ------- mobile/app/(auth)/_layout.tsx | 25 -- mobile/app/(auth)/forgot-password.tsx | 72 ++++ mobile/app/(auth)/sign-in.tsx | 74 ++++ mobile/app/(auth)/sign-up.tsx | 75 ++++ mobile/app/(auth)/signin.tsx | 207 ---------- mobile/app/(auth)/signup.tsx | 237 ----------- mobile/app/(onboarding)/index.tsx | 81 ++++ mobile/app/(tabs)/_layout.tsx | 30 ++ mobile/app/(tabs)/history.tsx | 59 +++ mobile/app/(tabs)/index.tsx | 97 +++++ mobile/app/(tabs)/profile.tsx | 126 ++++++ mobile/app/(tabs)/saved.tsx | 48 +++ mobile/app/_layout.tsx | 69 +--- mobile/app/conversation/[id]/index.tsx | 128 ++++++ mobile/app/index.tsx | 21 + mobile/components/ChatBubble.tsx | 169 -------- mobile/components/ui/Button.tsx | 79 ++++ mobile/components/ui/Chip.tsx | 31 ++ mobile/components/ui/Divider.tsx | 13 + mobile/components/ui/EmptyState.tsx | 30 ++ mobile/components/ui/ListRow.tsx | 46 +++ mobile/components/ui/LoadingState.tsx | 25 ++ mobile/components/ui/Screen.tsx | 49 +++ mobile/components/ui/SegmentedControl.tsx | 70 ++++ mobile/components/ui/Text.tsx | 64 +++ mobile/components/ui/TextField.tsx | 46 +++ mobile/components/ui/index.ts | 10 + mobile/constants/theme.ts | 71 ++++ mobile/docs/ARCHITECTURE.md | 81 ++++ mobile/docs/PLAN.md | 52 +++ mobile/docs/TASKLIST.md | 48 +++ mobile/features/auth/schemas.ts | 18 + mobile/features/auth/useAuthActions.ts | 47 +++ mobile/features/chat/Composer.tsx | 91 +++++ mobile/features/chat/ConversationPreview.tsx | 19 + mobile/features/chat/MessageBubble.tsx | 54 +++ mobile/features/chat/chat-data.ts | 34 ++ .../features/onboarding/onboarding-content.ts | 14 + mobile/hooks/useKeyboardInsets.ts | 8 + mobile/hooks/useSessionBootstrap.ts | 30 ++ mobile/lib/api.ts | 236 ----------- mobile/lib/api/client.ts | 36 ++ mobile/lib/api/mock.ts | 144 +++++++ mobile/lib/storage.ts | 136 ------- mobile/lib/storage/secureSession.ts | 24 ++ mobile/lib/types.ts | 29 -- mobile/package.json | 10 +- mobile/pnpm-lock.yaml | 373 +++++++++--------- mobile/store/app-store.ts | 50 +++ mobile/types/domain.ts | 43 ++ 57 files changed, 2416 insertions(+), 2211 deletions(-) create mode 100644 mobile/README.md delete mode 100644 mobile/app/(app)/_layout.tsx delete mode 100644 mobile/app/(app)/chat.tsx delete mode 100644 mobile/app/(app)/settings.tsx delete mode 100644 mobile/app/(app)/tool/[id].tsx delete mode 100644 mobile/app/(app)/tools.tsx delete mode 100644 mobile/app/(auth)/_layout.tsx create mode 100644 mobile/app/(auth)/forgot-password.tsx create mode 100644 mobile/app/(auth)/sign-in.tsx create mode 100644 mobile/app/(auth)/sign-up.tsx delete mode 100644 mobile/app/(auth)/signin.tsx delete mode 100644 mobile/app/(auth)/signup.tsx create mode 100644 mobile/app/(onboarding)/index.tsx create mode 100644 mobile/app/(tabs)/_layout.tsx create mode 100644 mobile/app/(tabs)/history.tsx create mode 100644 mobile/app/(tabs)/index.tsx create mode 100644 mobile/app/(tabs)/profile.tsx create mode 100644 mobile/app/(tabs)/saved.tsx create mode 100644 mobile/app/conversation/[id]/index.tsx create mode 100644 mobile/app/index.tsx delete mode 100644 mobile/components/ChatBubble.tsx create mode 100644 mobile/components/ui/Button.tsx create mode 100644 mobile/components/ui/Chip.tsx create mode 100644 mobile/components/ui/Divider.tsx create mode 100644 mobile/components/ui/EmptyState.tsx create mode 100644 mobile/components/ui/ListRow.tsx create mode 100644 mobile/components/ui/LoadingState.tsx create mode 100644 mobile/components/ui/Screen.tsx create mode 100644 mobile/components/ui/SegmentedControl.tsx create mode 100644 mobile/components/ui/Text.tsx create mode 100644 mobile/components/ui/TextField.tsx create mode 100644 mobile/components/ui/index.ts create mode 100644 mobile/constants/theme.ts create mode 100644 mobile/docs/ARCHITECTURE.md create mode 100644 mobile/docs/PLAN.md create mode 100644 mobile/docs/TASKLIST.md create mode 100644 mobile/features/auth/schemas.ts create mode 100644 mobile/features/auth/useAuthActions.ts create mode 100644 mobile/features/chat/Composer.tsx create mode 100644 mobile/features/chat/ConversationPreview.tsx create mode 100644 mobile/features/chat/MessageBubble.tsx create mode 100644 mobile/features/chat/chat-data.ts create mode 100644 mobile/features/onboarding/onboarding-content.ts create mode 100644 mobile/hooks/useKeyboardInsets.ts create mode 100644 mobile/hooks/useSessionBootstrap.ts delete mode 100644 mobile/lib/api.ts create mode 100644 mobile/lib/api/client.ts create mode 100644 mobile/lib/api/mock.ts delete mode 100644 mobile/lib/storage.ts create mode 100644 mobile/lib/storage/secureSession.ts delete mode 100644 mobile/lib/types.ts create mode 100644 mobile/store/app-store.ts create mode 100644 mobile/types/domain.ts 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} +