Overview
The RIS Gran Chimú app uses React Context API for global state management. Two primary contexts handle authentication and permissions across the application.
AuthContext
Manages user authentication state, login/logout operations, and session persistence.
Location
src/hooks/useAuth.tsx
Architecture
Context Type
type User = {
id : string ;
name : string ;
role : string ;
};
type AuthContextType = {
user : User | null ;
signIn : ( email : string , password : string ) => Promise < void >;
signOut : () => Promise < void >;
loading : boolean ;
};
Provider Setup
import { AuthProvider } from '@/src/hooks/useAuth' ;
export default function RootLayout () {
return (
< AuthProvider >
< Stack >
< Stack.Screen name = "landing" />
< Stack.Screen name = "(main)" />
</ Stack >
</ AuthProvider >
);
}
Implementation Details
The AuthProvider maintains the following state: const [ user , setUser ] = useState < User | null >( null );
const [ loading , setLoading ] = useState ( true );
const expiryTimeoutRef = useRef < ReturnType < typeof setTimeout > | null >( null );
user: Current authenticated user or null
loading: true during initialization or login
expiryTimeoutRef: Timer for automatic logout on token expiry
User sessions are stored in AsyncStorage: const USER_STORAGE_KEY = '@ris_gran_chimu_user' ;
// Save session
await AsyncStorage . setItem (
USER_STORAGE_KEY ,
JSON . stringify ({ user: mappedUser , token })
);
// Load session
const saved = await AsyncStorage . getItem ( USER_STORAGE_KEY );
if ( saved ) {
const { user , token } = JSON . parse ( saved );
setUser ( user );
setAuthToken ( token );
}
On app startup, the provider validates the stored token: const loadUser = async () => {
const saved = await AsyncStorage . getItem ( USER_STORAGE_KEY );
if ( ! saved ) return ;
const { user , token } = JSON . parse ( saved );
setAuthToken ( token );
try {
// Validate with backend
await apiClient . get ( '/auth/me' );
setUser ( user );
scheduleExpiry ( token );
} catch ( err ) {
// Check local expiry if backend fails
const payload = decodeJwt ( token );
const now = Math . floor ( Date . now () / 1000 );
if ( payload ?. exp && payload . exp < now ) {
// Token expired
signOut ();
} else {
// Use local token
setUser ( user );
}
}
};
The provider automatically logs out users when their JWT expires: const decodeJwt = ( token : string ) : { exp ?: number } | null => {
try {
const payload = token . split ( '.' )[ 1 ];
const decoded = atob ( payload . replace ( /-/ g , '+' ). replace ( /_/ g , '/' ));
return JSON . parse ( decoded );
} catch {
return null ;
}
};
const scheduleExpiry = ( token : string | null ) => {
if ( ! token ) return ;
const payload = decodeJwt ( token );
if ( ! payload ?. exp ) return ;
const now = Math . floor ( Date . now () / 1000 );
const delayMs = ( payload . exp - now ) * 1000 ;
if ( delayMs <= 0 ) {
Alert . alert ( 'Sesión expirada' , 'Tu sesión ha caducado.' , [
{ text: 'Aceptar' , onPress: signOut },
]);
return ;
}
expiryTimeoutRef . current = setTimeout (() => {
Alert . alert ( 'Sesión expirada' , 'Tu sesión ha caducado.' , [
{ text: 'Aceptar' , onPress: signOut },
]);
}, delayMs );
};
Usage Examples
Check Authentication
Login Form
Logout Button
Role-Based Access
import { useAuth } from '@/src/hooks/useAuth' ;
export default function ProtectedScreen () {
const { user , loading } = useAuth ();
if ( loading ) {
return < ActivityIndicator /> ;
}
if ( ! user ) {
return < Redirect href = "/landing" /> ;
}
return (
< View >
< Text > Welcome, { user . name } ! </ Text >
</ View >
);
}
PermissionContext
Manages fine-grained permissions for authenticated users.
Location
src/context/PermissionContext.tsx
Context Type
type PermissionContextType = {
permissions : string [];
loading : boolean ;
hasPermission : ( code : string ) => boolean ;
refreshPermissions : () => Promise < void >;
};
Complete Implementation
src/context/PermissionContext.tsx
import React , { createContext , useContext , useEffect , useState } from 'react' ;
import apiClient from '../services/apiClient' ;
import { useAuth } from '../hooks/useAuth' ;
type PermissionContextType = {
permissions : string [];
loading : boolean ;
hasPermission : ( code : string ) => boolean ;
refreshPermissions : () => Promise < void >;
};
const PermissionContext = createContext < PermissionContextType | undefined >( undefined );
export function PermissionProvider ({ children } : { children : React . ReactNode }) {
const { user } = useAuth ();
const [ permissions , setPermissions ] = useState < string []>([]);
const [ loading , setLoading ] = useState ( true );
const loadPermissions = async () => {
if ( ! user ) {
setPermissions ([]);
setLoading ( false );
return ;
}
try {
const res = await apiClient . get < string []>( '/auth/permissions' );
setPermissions ( res . data );
} catch ( error ) {
console . error ( 'Error loading permissions' , error );
setPermissions ([]);
} finally {
setLoading ( false );
}
};
useEffect (() => {
loadPermissions ();
}, [ user ]);
const hasPermission = ( code : string ) => {
// Admins have all permissions
if ( user ?. role === 'admin' ) return true ;
return permissions . includes ( code );
};
return (
< PermissionContext.Provider
value = { {
permissions ,
loading ,
hasPermission ,
refreshPermissions: loadPermissions
} }
>
{ children }
</ PermissionContext.Provider >
);
}
export function usePermissions () {
const context = useContext ( PermissionContext );
if ( ! context ) {
throw new Error ( 'usePermissions must be used within PermissionProvider' );
}
return context ;
}
Provider Setup
import { AuthProvider } from '@/src/hooks/useAuth' ;
import { PermissionProvider } from '@/src/context/PermissionContext' ;
export default function RootLayout () {
return (
< AuthProvider >
< PermissionProvider >
< Stack >
< Stack.Screen name = "landing" />
< Stack.Screen name = "(main)" />
</ Stack >
</ PermissionProvider >
</ AuthProvider >
);
}
PermissionProvider must be nested inside AuthProvider because it depends on the useAuth hook.
Permission Codes
Common permission codes used in the app:
Permission Code Description read:normasView normas write:normasCreate/edit normas delete:normasDelete normas manage:usersManage user accounts view:analyticsView analytics dashboard export:dataExport data to files
Usage Examples
Check Permission
Conditional Rendering
Disable Actions
Refresh Permissions
import { usePermissions } from '@/src/context/PermissionContext' ;
export default function NormaDetailScreen () {
const { hasPermission } = usePermissions ();
return (
< View >
< Text > Norma Details </ Text >
{ hasPermission ( 'write:normas' ) && (
< Button title = "Edit" onPress = { handleEdit } />
) }
{ hasPermission ( 'delete:normas' ) && (
< Button title = "Delete" onPress = { handleDelete } />
) }
</ View >
);
}
Context Architecture
Provider Hierarchy
< AuthProvider > // Authentication state
< PermissionProvider > // User permissions
< App /> // Your application
</ PermissionProvider >
</ AuthProvider >
Data Flow
Best Practices
Always Check Permissions Use hasPermission() before rendering sensitive UI or performing protected actions
Handle Loading States Check loading from both contexts before rendering permission-based content
Backend Validation Always validate permissions on the backend. Frontend checks are for UX only
Refresh on Role Change Call refreshPermissions() after user role changes to update UI
Admin users automatically have all permissions via the hasPermission check, but specific permissions are still fetched from the backend.
Troubleshooting
useAuth must be used within AuthProvider
Problem: Component using useAuth() is not wrapped in <AuthProvider>Solution: Ensure your root layout includes the AuthProvider:< AuthProvider >
< YourApp />
</ AuthProvider >
usePermissions must be used within PermissionProvider
Problem: Component using usePermissions() is not wrapped in <PermissionProvider>Solution: Add PermissionProvider to your layout:< AuthProvider >
< PermissionProvider >
< YourApp />
</ PermissionProvider >
</ AuthProvider >
Problem: Permission changes not reflected in UISolution: Call refreshPermissions() to reload permissions:const { refreshPermissions } = usePermissions ();
await refreshPermissions ();
Problem: User logged out on app restartSolution: Check that AsyncStorage is properly configured and the token is being saved in signIn()