From a0f2892454f8af900bea9f5e6c675c4cbe7b3e3a Mon Sep 17 00:00:00 2001 From: dinesh Date: Sun, 24 May 2026 09:39:53 +0530 Subject: [PATCH 1/3] fix: implement persistent auth session using expo-secure-store --- apps/mobile/src/context/AuthContext.tsx | 89 +++++++++++-------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 77559e4c..234c8916 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -1,29 +1,22 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import { API_BASE_URL } from '../config'; +import * as SecureStore from 'expo-secure-store'; -interface User { +const TOKEN_KEY = 'devcard_auth_token'; +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000'; + +type User = { id: string; - email: string; username: string; - displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; - company: string | null; - avatarUrl: string | null; - accentColor: string; - defaultCardId: string | null; -} + email: string; +}; -interface AuthContextType { +type AuthContextType = { user: User | null; token: string | null; - isAuthenticated: boolean; isLoading: boolean; login: (token: string) => Promise; - logout: () => void; - refreshUser: () => Promise; -} + logout: () => Promise; +}; const AuthContext = createContext(undefined); @@ -33,13 +26,33 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - // TODO: Load token from secure storage on app start - setIsLoading(false); + const loadToken = async () => { + try { + const savedToken = await SecureStore.getItemAsync(TOKEN_KEY); + if (savedToken) { + setToken(savedToken); + const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { + headers: { Authorization: `Bearer ${savedToken}` }, + }); + if (res.ok) { + const userData = await res.json(); + setUser(userData); + } else { + await SecureStore.deleteItemAsync(TOKEN_KEY); + } + } + } catch (err) { + console.error('Failed to restore session:', err); + } finally { + setIsLoading(false); + } + }; + loadToken(); }, []); const login = async (newToken: string) => { setToken(newToken); - // TODO: Save token to secure storage + await SecureStore.setItemAsync(TOKEN_KEY, newToken); try { const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { headers: { Authorization: `Bearer ${newToken}` }, @@ -53,38 +66,14 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }; - const logout = () => { + const logout = async () => { setToken(null); setUser(null); - // TODO: Clear token from secure storage - }; - - const refreshUser = async () => { - if (!token) return; - try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (res.ok) { - const userData = await res.json(); - setUser(userData); - } - } catch (err) { - console.error('Failed to refresh user:', err); - } + await SecureStore.deleteItemAsync(TOKEN_KEY); }; return ( - + {children} ); @@ -92,8 +81,6 @@ export function AuthProvider({ children }: { children: ReactNode }) { export function useAuth() { const context = useContext(AuthContext); - if (!context) { - throw new Error('useAuth must be used within an AuthProvider'); - } + if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; -} +} \ No newline at end of file From 98a48dfd195be47b1120ba19bc393d382276306b Mon Sep 17 00:00:00 2001 From: Midoriya-w Date: Tue, 2 Jun 2026 00:15:25 +0530 Subject: [PATCH 2/3] fix: replace expo secure store with AsyncStorage --- apps/mobile/src/context/AuthContext.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 234c8916..9a19eeac 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import * as SecureStore from 'expo-secure-store'; +import AsyncStorage from '@react-native-async-storage/async-storage'; const TOKEN_KEY = 'devcard_auth_token'; const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000'; @@ -28,7 +28,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { useEffect(() => { const loadToken = async () => { try { - const savedToken = await SecureStore.getItemAsync(TOKEN_KEY); + const savedToken = await AsyncStorage.getItem(TOKEN_KEY); if (savedToken) { setToken(savedToken); const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { @@ -38,7 +38,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const userData = await res.json(); setUser(userData); } else { - await SecureStore.deleteItemAsync(TOKEN_KEY); + await AsyncStorage.removeItem(TOKEN_KEY); } } } catch (err) { @@ -52,7 +52,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const login = async (newToken: string) => { setToken(newToken); - await SecureStore.setItemAsync(TOKEN_KEY, newToken); + await AsyncStorage.setItem(TOKEN_KEY, newToken); try { const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { headers: { Authorization: `Bearer ${newToken}` }, @@ -69,7 +69,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { const logout = async () => { setToken(null); setUser(null); - await SecureStore.deleteItemAsync(TOKEN_KEY); + await AsyncStorage.removeItem(TOKEN_KEY); }; return ( From 2da8d6938e999f4c254cbdbab2ed49dfb234ea85 Mon Sep 17 00:00:00 2001 From: Midoriya-w Date: Sat, 6 Jun 2026 17:48:21 +0530 Subject: [PATCH 3/3] fix: correct AuthContextType definition to match provider implementation --- apps/mobile/src/context/AuthContext.tsx | 141 ++++++++++++++++++------ 1 file changed, 105 insertions(+), 36 deletions(-) diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 9a19eeac..5b21b2d6 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -1,22 +1,42 @@ -import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { get } from '../services/api'; +import { DEMO_TOKEN } from '../services/api'; -const TOKEN_KEY = 'devcard_auth_token'; -const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000'; +// ── Storage Keys ────────────────────────────────────────────────────────── -type User = { +const TOKEN_KEY = 'devcard.auth.token'; +const FIRST_LAUNCH_KEY = 'devcard.firstLaunch'; + +// ── Types ──────────────────────────────────────────────────────────── + +interface User { id: string; - username: string; email: string; -}; + username: string; + displayName: string; + bio: string | null; + pronouns: string | null; + role: string | null; + company: string | null; + avatarUrl: string | null; + accentColor: string; + defaultCardId: string | null; +} -type AuthContextType = { +interface AuthContextType { user: User | null; token: string | null; + isAuthenticated: boolean; isLoading: boolean; + isFirstLaunch: boolean; login: (token: string) => Promise; logout: () => Promise; -}; + refreshUser: () => Promise; + enterDemoMode: () => Promise; +} + +// ── Context ─────────────────────────────────────────────────────────── const AuthContext = createContext(undefined); @@ -24,63 +44,112 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [isFirstLaunch, setIsFirstLaunch] = useState(false); + + // ── Hydrate token from AsyncStorage on mount ── useEffect(() => { - const loadToken = async () => { + const hydrate = async () => { try { - const savedToken = await AsyncStorage.getItem(TOKEN_KEY); - if (savedToken) { - setToken(savedToken); - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${savedToken}` }, - }); - if (res.ok) { - const userData = await res.json(); + const [storedToken, launchFlag] = await Promise.all([ + AsyncStorage.getItem(TOKEN_KEY), + AsyncStorage.getItem(FIRST_LAUNCH_KEY), + ]); + + if (launchFlag === null) { + setIsFirstLaunch(true); + await AsyncStorage.setItem(FIRST_LAUNCH_KEY, 'false'); + } + + if (storedToken) { + setToken(storedToken); + // Validate token by fetching profile + const userData = await get('/api/profiles/me', storedToken).catch(() => null); + if (userData) { setUser(userData); } else { + // Token expired or invalid — clear it await AsyncStorage.removeItem(TOKEN_KEY); + setToken(null); } } - } catch (err) { - console.error('Failed to restore session:', err); + } catch (error) { + console.error('Auth hydration failed:', error); } finally { setIsLoading(false); } }; - loadToken(); + + hydrate(); }, []); - const login = async (newToken: string) => { + // ── Login ── + + const login = useCallback(async (newToken: string) => { setToken(newToken); - await AsyncStorage.setItem(TOKEN_KEY, newToken); try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - headers: { Authorization: `Bearer ${newToken}` }, - }); - if (res.ok) { - const userData = await res.json(); + await AsyncStorage.setItem(TOKEN_KEY, newToken); + const userData = await get('/api/profiles/me', newToken).catch(() => null); + if (userData) { setUser(userData); } - } catch (err) { - console.error('Failed to fetch user:', err); + } catch (error) { + console.error('Failed to persist token or fetch user:', error); } - }; + }, []); + + // ── Logout ── - const logout = async () => { + const logout = useCallback(async () => { setToken(null); setUser(null); - await AsyncStorage.removeItem(TOKEN_KEY); - }; + try { + await AsyncStorage.removeItem(TOKEN_KEY); + } catch (error) { + console.error('Failed to clear stored token:', error); + } + }, []); + + // ── Refresh User ── + + const refreshUser = useCallback(async () => { + if (!token) return; + try { + const userData = await get('/api/profiles/me', token).catch(() => null); + if (userData) { + setUser(userData); + } + } catch (error) { + console.error('Failed to refresh user:', error); + } + }, [token]); + + const enterDemoMode = useCallback(async () => { + await login(DEMO_TOKEN); + }, [login]); return ( - + {children} ); } -export function useAuth() { +export function useAuth(): AuthContextType { const context = useContext(AuthContext); - if (!context) throw new Error('useAuth must be used within AuthProvider'); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } return context; } \ No newline at end of file