1- import React , { createContext , useContext , useState , useEffect , ReactNode } from 'react' ;
1+ import React , { createContext , useContext , useState , useEffect , useCallback , ReactNode } from 'react' ;
22import AsyncStorage from '@react-native-async-storage/async-storage' ;
3+ import { get } from '../services/api' ;
4+ import { DEMO_TOKEN } from '../services/api' ;
35
4- const TOKEN_KEY = 'devcard_auth_token' ;
5- const API_BASE_URL = process . env . EXPO_PUBLIC_API_URL ?? 'http://localhost:3000' ;
6+ // ── Storage Keys ──────────────────────────────────────────────────────────
67
7- type User = {
8+ const TOKEN_KEY = 'devcard.auth.token' ;
9+ const FIRST_LAUNCH_KEY = 'devcard.firstLaunch' ;
10+
11+ // ── Types ────────────────────────────────────────────────────────────
12+
13+ interface User {
814 id : string ;
9- username : string ;
1015 email : string ;
11- } ;
16+ username : string ;
17+ displayName : string ;
18+ bio : string | null ;
19+ pronouns : string | null ;
20+ role : string | null ;
21+ company : string | null ;
22+ avatarUrl : string | null ;
23+ accentColor : string ;
24+ defaultCardId : string | null ;
25+ }
1226
13- type AuthContextType = {
27+ interface AuthContextType {
1428 user : User | null ;
1529 token : string | null ;
30+ isAuthenticated : boolean ;
1631 isLoading : boolean ;
32+ isFirstLaunch : boolean ;
1733 login : ( token : string ) => Promise < void > ;
1834 logout : ( ) => Promise < void > ;
19- } ;
35+ refreshUser : ( ) => Promise < void > ;
36+ enterDemoMode : ( ) => Promise < void > ;
37+ }
38+
39+ // ── Context ───────────────────────────────────────────────────────────
2040
2141const AuthContext = createContext < AuthContextType | undefined > ( undefined ) ;
2242
2343export function AuthProvider ( { children } : { children : ReactNode } ) {
2444 const [ user , setUser ] = useState < User | null > ( null ) ;
2545 const [ token , setToken ] = useState < string | null > ( null ) ;
2646 const [ isLoading , setIsLoading ] = useState ( true ) ;
47+ const [ isFirstLaunch , setIsFirstLaunch ] = useState ( false ) ;
48+
49+ // ── Hydrate token from AsyncStorage on mount ──
2750
2851 useEffect ( ( ) => {
29- const loadToken = async ( ) => {
52+ const hydrate = async ( ) => {
3053 try {
31- const savedToken = await AsyncStorage . getItem ( TOKEN_KEY ) ;
32- if ( savedToken ) {
33- setToken ( savedToken ) ;
34- const res = await fetch ( `${ API_BASE_URL } /api/profiles/me` , {
35- headers : { Authorization : `Bearer ${ savedToken } ` } ,
36- } ) ;
37- if ( res . ok ) {
38- const userData = await res . json ( ) ;
54+ const [ storedToken , launchFlag ] = await Promise . all ( [
55+ AsyncStorage . getItem ( TOKEN_KEY ) ,
56+ AsyncStorage . getItem ( FIRST_LAUNCH_KEY ) ,
57+ ] ) ;
58+
59+ if ( launchFlag === null ) {
60+ setIsFirstLaunch ( true ) ;
61+ await AsyncStorage . setItem ( FIRST_LAUNCH_KEY , 'false' ) ;
62+ }
63+
64+ if ( storedToken ) {
65+ setToken ( storedToken ) ;
66+ // Validate token by fetching profile
67+ const userData = await get < User > ( '/api/profiles/me' , storedToken ) . catch ( ( ) => null ) ;
68+ if ( userData ) {
3969 setUser ( userData ) ;
4070 } else {
71+ // Token expired or invalid — clear it
4172 await AsyncStorage . removeItem ( TOKEN_KEY ) ;
73+ setToken ( null ) ;
4274 }
4375 }
44- } catch ( err ) {
45- console . error ( 'Failed to restore session :' , err ) ;
76+ } catch ( error ) {
77+ console . error ( 'Auth hydration failed :' , error ) ;
4678 } finally {
4779 setIsLoading ( false ) ;
4880 }
4981 } ;
50- loadToken ( ) ;
82+
83+ hydrate ( ) ;
5184 } , [ ] ) ;
5285
53- const login = async ( newToken : string ) => {
86+ // ── Login ──
87+
88+ const login = useCallback ( async ( newToken : string ) => {
5489 setToken ( newToken ) ;
55- await AsyncStorage . setItem ( TOKEN_KEY , newToken ) ;
5690 try {
57- const res = await fetch ( `${ API_BASE_URL } /api/profiles/me` , {
58- headers : { Authorization : `Bearer ${ newToken } ` } ,
59- } ) ;
60- if ( res . ok ) {
61- const userData = await res . json ( ) ;
91+ await AsyncStorage . setItem ( TOKEN_KEY , newToken ) ;
92+ const userData = await get < User > ( '/api/profiles/me' , newToken ) . catch ( ( ) => null ) ;
93+ if ( userData ) {
6294 setUser ( userData ) ;
6395 }
64- } catch ( err ) {
65- console . error ( 'Failed to fetch user:' , err ) ;
96+ } catch ( error ) {
97+ console . error ( 'Failed to persist token or fetch user:' , error ) ;
6698 }
67- } ;
99+ } , [ ] ) ;
100+
101+ // ── Logout ──
68102
69- const logout = async ( ) => {
103+ const logout = useCallback ( async ( ) => {
70104 setToken ( null ) ;
71105 setUser ( null ) ;
72- await AsyncStorage . removeItem ( TOKEN_KEY ) ;
73- } ;
106+ try {
107+ await AsyncStorage . removeItem ( TOKEN_KEY ) ;
108+ } catch ( error ) {
109+ console . error ( 'Failed to clear stored token:' , error ) ;
110+ }
111+ } , [ ] ) ;
112+
113+ // ── Refresh User ──
114+
115+ const refreshUser = useCallback ( async ( ) => {
116+ if ( ! token ) return ;
117+ try {
118+ const userData = await get < User > ( '/api/profiles/me' , token ) . catch ( ( ) => null ) ;
119+ if ( userData ) {
120+ setUser ( userData ) ;
121+ }
122+ } catch ( error ) {
123+ console . error ( 'Failed to refresh user:' , error ) ;
124+ }
125+ } , [ token ] ) ;
126+
127+ const enterDemoMode = useCallback ( async ( ) => {
128+ await login ( DEMO_TOKEN ) ;
129+ } , [ login ] ) ;
74130
75131 return (
76- < AuthContext . Provider value = { { user, token, isLoading, login, logout } } >
132+ < AuthContext . Provider
133+ value = { {
134+ user,
135+ token,
136+ isAuthenticated : ! ! token && ! ! user ,
137+ isLoading,
138+ isFirstLaunch,
139+ login,
140+ logout,
141+ refreshUser,
142+ enterDemoMode,
143+ } } >
77144 { children }
78145 </ AuthContext . Provider >
79146 ) ;
80147}
81148
82- export function useAuth ( ) {
149+ export function useAuth ( ) : AuthContextType {
83150 const context = useContext ( AuthContext ) ;
84- if ( ! context ) throw new Error ( 'useAuth must be used within AuthProvider' ) ;
151+ if ( ! context ) {
152+ throw new Error ( 'useAuth must be used within an AuthProvider' ) ;
153+ }
85154 return context ;
86155}
0 commit comments