Skip to main content

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

app/_layout.tsx
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

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

app/_layout.tsx
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 CodeDescription
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

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

Problem: Component using useAuth() is not wrapped in <AuthProvider>Solution: Ensure your root layout includes the AuthProvider:
<AuthProvider>
  <YourApp />
</AuthProvider>
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()

Build docs developers (and LLMs) love