Autentificare în Expo Router: Ghid Complet pentru Rute Protejate cu Stack.Protected

Ghid practic pentru implementarea autentificării în Expo Router folosind Stack.Protected și Tabs.Protected, cu sesiuni securizate, RBAC, deep link-uri și OAuth PKCE.

De Ce Contează Rutele Protejate în Aplicațiile Mobile

Dacă ai construit vreodată o aplicație React Native cu ecrane de login, dashboard-uri și profile de utilizator, știi exact despre ce vorbesc. Una dintre cele mai mari bătăi de cap e gestionarea accesului: cine vede ce ecran, ce se întâmplă când un utilizator neautentificat dă click pe un deep link către un ecran privat, cum eviți ca logica de autentificare să se împrăștie prin zeci de fișiere.

Sincer, până recent, răspunsul implica redirect-uri manuale, useEffect-uri complicate și un volum considerabil de cod duplicat. Dar odată cu lansarea Expo SDK 53 și a Expo Router v5, lucrurile s-au schimbat fundamental. Noua componentă Stack.Protected oferă o abordare declarativă, elegantă și robustă pentru protejarea rutelor — atât în Stack-uri, cât și în Tab-uri.

În acest ghid parcurgem tot ce ai nevoie pentru a implementa un sistem complet de autentificare cu Expo Router în 2026. De la structura proiectului și configurarea SessionProvider-ului, până la controlul accesului bazat pe roluri (RBAC) și stocarea securizată a token-urilor cu expo-secure-store.

Ce Este Expo Router și De Ce Contează

Expo Router este un router bazat pe sistemul de fișiere (file-based routing) pentru aplicații React Native și web. Inspirat de Next.js, transformă structura de foldere din directorul app/ în rute de navigare — fără configurări manuale, fără fișiere de mapping.

Fiecare fișier din app/ devine automat o rută. Fiecare folder devine un segment de URL. Deep linking-ul funcționează din oficiu, fără cod suplimentar. Și, începând cu SDK 55, Expo Router rulează pe Noua Arhitectură React Native cu suport nativ complet pentru stack-uri, tab-uri, tranziții și chiar split views pe iPad.

Dar ce lipsea până acum? Un mecanism nativ pentru protejarea rutelor. Dezvoltatorii se bazau pe pattern-uri fragile care puteau fi ocolite prin deep link-uri sau condiții de concurență. Nu era ideal, dar era tot ce aveam.

Abordarea Veche: Redirect-uri Manuale și Problemele Lor

Înainte de Stack.Protected, cel mai comun pattern pentru autentificare în Expo Router arăta cam așa:

// app/(app)/_layout.tsx — abordarea veche (SDK 52 și anterior)
import { Redirect, Stack } from 'expo-router';
import { useSession } from '@/ctx';
import { Text } from 'react-native';

export default function AppLayout() {
  const { session, isLoading } = useSession();

  if (isLoading) {
    return <Text>Se încarcă...</Text>;
  }

  if (!session) {
    return <Redirect href="/sign-in" />;
  }

  return <Stack />;
}

Deși funcțional la prima vedere, acest pattern avea probleme serioase:

  • Logică dispersată: Fiecare grup de rute protejate avea nevoie de propriul _layout.tsx cu verificarea de autentificare. Într-o aplicație cu 5+ grupuri protejate, duplicai aceeași logică peste tot
  • Vulnerabil la deep link-uri: Redirect-urile manuale puteau fi ocolite prin navigare directă sau link-uri externe, mai ales în cazuri de concurență (race conditions)
  • Experiență de utilizator inconsistentă: Flash-uri vizuale apăreau când redirect-ul se executa cu un mic delay — utilizatorul vedea pentru o fracțiune de secundă ecranul protejat, ceea ce nu era deloc plăcut
  • Greu de gestionat roluri: Adăugarea de logică RBAC (admin vs. utilizator normal) complica exponențial codul

Stack.Protected: Noua Paradigmă de Protecție a Rutelor

Odată cu SDK 53, Expo Router a introdus componentele Stack.Protected și Tabs.Protected. Acestea oferă un mecanism declarativ, integrat direct în navigator, pentru controlul accesului la rute.

Cum Funcționează

Componenta Protected primește un prop guard de tip boolean:

  • Când guard={true}, rutele din interior sunt accesibile
  • Când guard={false}, rutele sunt blocate, iar navigarea către ele eșuează silențios, cu redirecționare automată

Hai să vedem ce se întâmplă concret când un utilizator încearcă să acceseze o rută protejată cu guard={false}:

  1. Navigarea este interceptată la nivel de navigator — nu ajunge la ecranul țintă
  2. Utilizatorul este redirecționat automat către ancora (de regulă, ruta index) sau primul ecran disponibil
  3. Dacă utilizatorul era deja pe un ecran care devine protejat (de exemplu, sesiunea expiră), este redirecționat imediat
  4. Toate intrările din istoricul de navigare pentru rutele protejate sunt eliminate automat

Avantajele față de Redirect-uri

Diferența fundamentală este că protecția se aplică la nivel de navigator, nu la nivel de ecran. Pare un detaliu mic, dar schimbă totul:

  • Un singur loc centralizat pentru toată logica de acces
  • Deep link-urile sunt verificate automat — un utilizator neautentificat care accesează un deep link către un ecran protejat este redirecționat instant către ecranul de login
  • Zero boilerplate per ecran — nu mai ai nevoie de if (!session) return <Redirect /> în fiecare layout
  • Tranziții curate, fără flash-uri vizuale

Implementare Pas cu Pas: Sistem Complet de Autentificare

Pasul 1: Structura Proiectului

O structură de foldere bine gândită e esențială (și te va scuti de multe dureri de cap pe parcurs). Recomandarea oficială este separarea clară între rutele publice și cele private:

app/
├── _layout.tsx          # Root layout cu SessionProvider
├── (app)/
│   ├── _layout.tsx      # Layout-ul aplicației (tabs/stack)
│   ├── index.tsx        # Ecran principal (protejat)
│   ├── profile.tsx      # Profil utilizator (protejat)
│   └── settings.tsx     # Setări (protejat)
├── (auth)/
│   ├── _layout.tsx      # Layout autentificare
│   ├── index.tsx        # Ecran de login
│   └── register.tsx     # Ecran de înregistrare
src/
├── context/
│   └── AuthContext.tsx   # Session provider
├── hooks/
│   └── useStorageState.ts  # Hook pentru stocare securizată
└── services/
    └── api.ts           # Servicii API

Observă că grupurile (app) și (auth) sunt între paranteze — aceasta e convenția Expo Router pentru grupuri de rute care nu afectează URL-ul. Adică ruta (app)/profile.tsx va avea URL-ul /profile, nu /(app)/profile.

Pasul 2: Hook-ul de Stocare Securizată

Primul lucru de implementat este un hook care persistă starea de autentificare în mod securizat. Pe platforma nativă, folosim expo-secure-store (care utilizează Keychain pe iOS și KeyStore pe Android), iar pe web, localStorage:

// src/hooks/useStorageState.ts
import * as SecureStore from 'expo-secure-store';
import { useCallback, useEffect, useReducer } from 'react';
import { Platform } from 'react-native';

type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void];

function useAsyncState<T>(
  initialValue: [boolean, T | null] = [true, null]
): UseStateHook<T> {
  return useReducer(
    (
      state: [boolean, T | null],
      action: T | null = null
    ): [boolean, T | null] => [false, action],
    initialValue
  ) as UseStateHook<T>;
}

export async function setStorageItemAsync(key: string, value: string | null) {
  if (Platform.OS === 'web') {
    try {
      if (value === null) {
        localStorage.removeItem(key);
      } else {
        localStorage.setItem(key, value);
      }
    } catch (e) {
      console.error('Eroare la stocarea locală:', e);
    }
  } else {
    if (value == null) {
      await SecureStore.deleteItemAsync(key);
    } else {
      await SecureStore.setItemAsync(key, value);
    }
  }
}

export function useStorageState(key: string): UseStateHook<string> {
  const [state, setState] = useAsyncState<string>();

  useEffect(() => {
    if (Platform.OS === 'web') {
      try {
        const value = localStorage.getItem(key);
        setState(value);
      } catch (e) {
        console.error('Eroare la citirea stocării:', e);
      }
    } else {
      SecureStore.getItemAsync(key).then((value) => {
        setState(value);
      });
    }
  }, [key]);

  const setValue = useCallback(
    (value: string | null) => {
      setState(value);
      setStorageItemAsync(key, value);
    },
    [key]
  );

  return [state, setValue];
}

Acest hook returnează un tuplu cu [isLoading, value] și o funcție setter. La prima montare, citește valoarea stocată (asincron pe nativ, sincron pe web), apoi expune starea și un setter care actualizează simultan React state-ul și storage-ul persistent. E un pattern simplu, dar foarte eficient.

Pasul 3: AuthContext — Session Provider

Context-ul de autentificare expune sesiunea, starea de încărcare și funcțiile de login/logout la nivel de întreaga aplicație. Nimic spectaculos aici, dar e piesa centrală a întregului sistem:

// src/context/AuthContext.tsx
import { useContext, createContext, type PropsWithChildren } from 'react';
import { useStorageState } from '@/hooks/useStorageState';

const AuthContext = createContext<{
  signIn: (token: string) => void;
  signOut: () => void;
  session: string | null;
  isLoading: boolean;
}>({
  signIn: () => null,
  signOut: () => null,
  session: null,
  isLoading: false,
});

export function useSession() {
  const value = useContext(AuthContext);
  if (process.env.NODE_ENV !== 'production') {
    if (!value) {
      throw new Error(
        'useSession trebuie folosit în interiorul unui SessionProvider'
      );
    }
  }
  return value;
}

export function SessionProvider({ children }: PropsWithChildren) {
  const [[isLoading, session], setSession] = useStorageState('session');

  return (
    <AuthContext.Provider
      value={{
        signIn: (token: string) => {
          setSession(token);
        },
        signOut: () => {
          setSession(null);
        },
        session,
        isLoading,
      }}>
      {children}
    </AuthContext.Provider>
  );
}

Câteva lucruri de observat aici. Funcția signIn primește un token (de exemplu, un JWT returnat de backend) și îl stochează securizat. Funcția signOut șterge token-ul. Iar verificarea din useSession aruncă o eroare în development dacă hook-ul e folosit în afara provider-ului — un mic debug helper care m-a salvat personal de câteva ori.

Pasul 4: Root Layout cu Stack.Protected

Aici se întâmplă magia. Root layout-ul împachetează toată aplicația în SessionProvider și folosește Stack.Protected pentru a defini ce rute sunt accesibile în funcție de starea de autentificare:

// app/_layout.tsx
import { Stack } from 'expo-router';
import { SessionProvider, useSession } from '@/context/AuthContext';
import { ActivityIndicator, View } from 'react-native';

function RootNavigator() {
  const { session, isLoading } = useSession();

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" color="#6366f1" />
      </View>
    );
  }

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {/* Rute protejate — accesibile DOAR când utilizatorul e autentificat */}
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>

      {/* Rute de autentificare — accesibile DOAR când NU e autentificat */}
      <Stack.Protected guard={!session}>
        <Stack.Screen name="(auth)" />
      </Stack.Protected>
    </Stack>
  );
}

export default function RootLayout() {
  return (
    <SessionProvider>
      <RootNavigator />
    </SessionProvider>
  );
}

Logica e simplă și declarativă: când session există, grupul (app) e accesibil și (auth) e blocat. Când nu există sesiune, se inversează. Tranziția se face automat, fără redirect-uri manuale.

Un detaliu important: observă separarea între RootLayout (care furnizează provider-ul) și RootNavigator (care consumă context-ul). Aceasta e necesară deoarece useSession trebuie apelat într-un component care se află în interiorul SessionProvider. E o greșeală clasică pe care mulți o fac la început.

Pasul 5: Ecranul de Login

Ecranul de autentificare apelează API-ul de login și stochează token-ul primit:

// app/(auth)/index.tsx
import { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';
import { useSession } from '@/context/AuthContext';

export default function SignIn() {
  const { signIn } = useSession();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);

  const handleLogin = async () => {
    if (!email || !password) {
      Alert.alert('Eroare', 'Completează toate câmpurile');
      return;
    }

    setLoading(true);
    try {
      const response = await fetch('https://api.exemplu.com/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      const data = await response.json();

      if (!response.ok) {
        throw new Error(data.message || 'Autentificare eșuată');
      }

      // Stochează token-ul — Stack.Protected redirecționează automat
      signIn(data.accessToken);
    } catch (error: any) {
      Alert.alert('Eroare', error.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      style={styles.container}>
      <View style={styles.form}>
        <Text style={styles.title}>Bine ai venit</Text>
        <Text style={styles.subtitle}>Autentifică-te pentru a continua</Text>

        <TextInput
          style={styles.input}
          placeholder="Email"
          value={email}
          onChangeText={setEmail}
          keyboardType="email-address"
          autoCapitalize="none"
        />
        <TextInput
          style={styles.input}
          placeholder="Parolă"
          value={password}
          onChangeText={setPassword}
          secureTextEntry
        />

        <TouchableOpacity
          style={[styles.button, loading && styles.buttonDisabled]}
          onPress={handleLogin}
          disabled={loading}>
          <Text style={styles.buttonText}>
            {loading ? 'Se autentifică...' : 'Intră în cont'}
          </Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8fafc',
    justifyContent: 'center',
  },
  form: {
    paddingHorizontal: 24,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#1e293b',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#64748b',
    marginBottom: 32,
  },
  input: {
    backgroundColor: '#fff',
    borderWidth: 1,
    borderColor: '#e2e8f0',
    borderRadius: 12,
    padding: 16,
    fontSize: 16,
    marginBottom: 16,
  },
  button: {
    backgroundColor: '#6366f1',
    borderRadius: 12,
    padding: 16,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonDisabled: {
    opacity: 0.7,
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Observă ce simplu e fluxul: după ce signIn(data.accessToken) este apelat, session din context se actualizează, guard-ul pentru (app) devine true, iar guard-ul pentru (auth) devine false. Router-ul redirecționează automat. Nu ai nevoie de router.replace('/home') sau alte hack-uri. Serios, e chiar atât de simplu.

Protejarea Tab-urilor cu Tabs.Protected

Rutele protejate nu funcționează doar cu Stack-uri — sunt disponibile și pentru Tabs (și Drawer). Funcționalitatea asta e extrem de utilă pentru aplicații care au unele tab-uri vizibile doar utilizatorilor autentificați sau cu anumite roluri.

// app/(app)/_layout.tsx
import { Tabs } from 'expo-router';
import { useSession } from '@/context/AuthContext';
import { Ionicons } from '@expo/vector-icons';

export default function AppLayout() {
  const { session } = useSession();

  // Simulăm un rol — în practică, decodezi JWT-ul sau faci un API call
  const userRole = session ? JSON.parse(atob(session.split('.')[1])).role : null;
  const isVip = userRole === 'vip' || userRole === 'admin';

  return (
    <Tabs screenOptions={{ tabBarActiveTintColor: '#6366f1' }}>
      {/* Tab-uri publice — vizibile tuturor utilizatorilor autentificați */}
      <Tabs.Screen
        name="index"
        options={{
          title: 'Acasă',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home-outline" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profil',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person-outline" size={size} color={color} />
          ),
        }}
      />

      {/* Tab VIP — vizibil doar utilizatorilor VIP sau admin */}
      <Tabs.Protected guard={isVip}>
        <Tabs.Screen
          name="vip-area"
          options={{
            title: 'VIP',
            tabBarIcon: ({ color, size }) => (
              <Ionicons name="diamond-outline" size={size} color={color} />
            ),
          }}
        />
      </Tabs.Protected>

      <Tabs.Screen
        name="settings"
        options={{
          title: 'Setări',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="settings-outline" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Tab-ul VIP apare sau dispare automat din bara de tab-uri în funcție de rolul utilizatorului. Navigatorul gestionează totul — nu e nevoie de logică condițională suplimentară sau de componente wrapper.

Control al Accesului Bazat pe Roluri (RBAC)

Pentru aplicații mai complexe — un CMS mobil, o platformă enterprise, sau orice proiect în care nu toți utilizatorii au aceleași drepturi — ai nevoie de control granular al accesului. Vestea bună e că Stack.Protected suportă imbricarea (nesting), ceea ce permite ierarhii de permisiuni elegante:

// app/_layout.tsx — cu roluri
function RootNavigator() {
  const { session } = useSession();
  const user = session ? decodeToken(session) : null;

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {/* Rute publice — toți utilizatorii neautentificați */}
      <Stack.Protected guard={!session}>
        <Stack.Screen name="(auth)" />
      </Stack.Protected>

      {/* Rute pentru utilizatori autentificați */}
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(app)" />

        {/* Rute admin — necesită autentificare + rol admin */}
        <Stack.Protected guard={user?.role === 'admin'}>
          <Stack.Screen name="(admin)" />
        </Stack.Protected>

        {/* Rute moderator — necesită autentificare + rol moderator sau admin */}
        <Stack.Protected guard={
          user?.role === 'moderator' || user?.role === 'admin'
        }>
          <Stack.Screen name="(moderator)" />
        </Stack.Protected>
      </Stack.Protected>
    </Stack>
  );
}

Structura imbricată exprimă clar ierarhia de acces. Trebuie să fii autentificat pentru orice rută din (app), dar trebuie să fii și admin pentru rutele din (admin). Guard-urile exterioare sunt evaluate primele, deci un utilizator neautentificat nu poate accesa niciodată rutele admin — indiferent de logica interioară.

Gestionarea Deep Link-urilor și a Stării de Încărcare

Deep Link-uri Securizate

Unul dintre cele mai mari avantaje ale Stack.Protected față de redirect-urile manuale e comportamentul cu deep link-uri. Să luăm scenariul clasic problematic:

  1. Utilizatorul primește un link myapp://profile/settings într-un mesaj
  2. Aplicația nu e deschisă, deci avem un cold start
  3. Utilizatorul nu e autentificat

Cu redirect-uri manuale, existau cazuri în care ecranul protejat apărea pentru o fracțiune de secundă înainte de redirect. Cu Stack.Protected, navigarea e interceptată la nivel de navigator — ecranul protejat nu se montează niciodată, iar utilizatorul e redirecționat direct la login. Mult mai curat.

După autentificare, utilizatorul poate fi redirecționat către destinația inițială prin păstrarea URL-ului de deep link:

// Exemplu: salvarea URL-ului de destinație
import { useURL } from 'expo-linking';
import { useEffect, useRef } from 'react';

export function useDeepLinkRedirect() {
  const url = useURL();
  const pendingUrl = useRef<string | null>(null);
  const { session } = useSession();

  useEffect(() => {
    if (url && !session) {
      // Salvează URL-ul pentru după autentificare
      pendingUrl.current = url;
    }
  }, [url, session]);

  useEffect(() => {
    if (session && pendingUrl.current) {
      // După login, redirecționează la destinația originală
      router.replace(pendingUrl.current);
      pendingUrl.current = null;
    }
  }, [session]);
}

Starea de Încărcare

Un aspect pe care mulți îl neglijează e gestionarea stării de încărcare inițiale. Când aplicația pornește, token-ul trebuie citit din SecureStore — o operație asincronă. Până nu știi dacă utilizatorul e autentificat sau nu, nu poți decide ce rute să arăți.

Recomandarea oficială e să afișezi un ecran de încărcare (splash screen sau ActivityIndicator) în RootNavigator până când isLoading devine false. Am implementat deja acest pattern în Pasul 4.

Dar pentru o experiență și mai bună, poți folosi expo-splash-screen pentru a menține splash screen-ul nativ vizibil până la finalizarea verificării:

import * as SplashScreen from 'expo-splash-screen';
import { useEffect } from 'react';

SplashScreen.preventAutoHideAsync();

function RootNavigator() {
  const { session, isLoading } = useSession();

  useEffect(() => {
    if (!isLoading) {
      SplashScreen.hideAsync();
    }
  }, [isLoading]);

  if (isLoading) return null;

  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Protected guard={!!session}>
        <Stack.Screen name="(app)" />
      </Stack.Protected>
      <Stack.Protected guard={!session}>
        <Stack.Screen name="(auth)" />
      </Stack.Protected>
    </Stack>
  );
}

Bune Practici de Securitate

Implementarea corectă a autentificării în aplicații mobile necesită atenție la câteva aspecte esențiale. Nu sări peste secțiunea asta — e probabil cea mai importantă din tot ghidul.

1. Stocarea Token-urilor

  • Folosește întotdeauna expo-secure-store pentru token-uri pe dispozitive native. Nu folosi AsyncStorage — datele sunt stocate în text clar și pot fi accesate de alte aplicații pe dispozitive root-ate
  • Pe iOS, expo-secure-store folosește Keychain Services, iar pe Android, Android Keystore — ambele oferă criptare la nivel de hardware
  • Un detaliu care m-a surprins la început: pe iOS, datele din Keychain persistă chiar și după dezinstalarea aplicației (dacă se reinstalează cu același bundle ID). Ia în calcul acest comportament în logica ta de autentificare

2. Validarea pe Server

  • Rutele protejate sunt evaluate exclusiv pe client. Ele nu înlocuiesc validarea pe server. Orice API call trebuie verificat independent cu token-ul de autentificare
  • Nu te baza pe faptul că un ecran e „ascuns" — un utilizator tehnic poate accesa JavaScript-ul sau HTML-ul rutelor protejate direct. Protecția pe client e doar pentru UX, nu pentru securitate reală

3. OAuth cu PKCE

Pentru autentificarea cu furnizori externi (Google, Apple, Facebook), folosește expo-auth-session cu flow-ul Authorization Code + PKCE. Flow-ul implicit (Implicit Grant) nu mai e recomandat în 2026 din cauza riscurilor de injecție de token:

import * as AuthSession from 'expo-auth-session';
import * as Google from 'expo-auth-session/providers/google';

export function useGoogleAuth() {
  const [request, response, promptAsync] = Google.useAuthRequest({
    clientId: 'YOUR_CLIENT_ID',
    // PKCE este activat implicit
  });

  useEffect(() => {
    if (response?.type === 'success') {
      const { id_token } = response.params;
      // Trimite id_token la backend-ul tău pentru validare
      authenticateWithBackend(id_token);
    }
  }, [response]);

  return { request, promptAsync };
}

4. Expirarea și Refresh-ul Token-urilor

Implementează un mecanism de refresh token pentru a evita delogarea frecventă a utilizatorilor. Un pattern pe care îl folosesc des e interceptarea răspunsurilor 401 și refacerea automată a cererii cu un token proaspăt:

// src/services/api.ts
async function fetchWithAuth(url: string, options: RequestInit = {}) {
  let token = await SecureStore.getItemAsync('session');

  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: `Bearer ${token}`,
    },
  });

  if (response.status === 401) {
    // Încearcă refresh-ul token-ului
    const refreshToken = await SecureStore.getItemAsync('refreshToken');
    const refreshResponse = await fetch('https://api.exemplu.com/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });

    if (refreshResponse.ok) {
      const { accessToken } = await refreshResponse.json();
      await SecureStore.setItemAsync('session', accessToken);

      // Reîncearcă cererea originală cu noul token
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          Authorization: `Bearer ${accessToken}`,
        },
      });
    }
  }

  return response;
}

Testarea Fluxului de Autentificare

Testarea corectă a fluxului de autentificare e un pas pe care mulți îl sar, dar care merită din plin efortul. Iată cum arată câteva teste de bază:

// __tests__/auth-flow.test.tsx
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { renderRouter } from 'expo-router/testing-library';

describe('Flux de autentificare', () => {
  it('redirecționează utilizatorii neautentificați la login', async () => {
    const { getByText } = renderRouter(
      {
        '(auth)/index': () => <Text>Ecran Login</Text>,
        '(app)/index': () => <Text>Ecran Principal</Text>,
      },
      { initialUrl: '/(app)' }
    );

    await waitFor(() => {
      expect(getByText('Ecran Login')).toBeTruthy();
    });
  });

  it('permite accesul utilizatorilor autentificați la rute protejate', async () => {
    // Simulează o sesiune activă
    mockSecureStore.setItem('session', 'valid-jwt-token');

    const { getByText } = renderRouter(
      {
        '(auth)/index': () => <Text>Ecran Login</Text>,
        '(app)/index': () => <Text>Ecran Principal</Text>,
      },
      { initialUrl: '/(app)' }
    );

    await waitFor(() => {
      expect(getByText('Ecran Principal')).toBeTruthy();
    });
  });
});

Migrare de la Redirect-uri la Stack.Protected

Dacă ai deja o aplicație cu autentificare bazată pe redirect-uri, migrarea la Stack.Protected e surprinzător de simplă. Am făcut-o într-un proiect cu peste 30 de ecrane și a durat mai puțin de o oră. Iată pașii:

  1. Păstrează SessionProvider-ul — contextul de autentificare rămâne identic
  2. Mută logica de guard în root layout: Elimină toate verificările if (!session) return <Redirect /> din layout-urile grupurilor și înlocuiește-le cu Stack.Protected în app/_layout.tsx
  3. Simplifică layout-urile grupurilor: Layout-urile din (app)/_layout.tsx devin simple, fără logică de autentificare — se ocupă doar de configurarea navigatorului
  4. Testează deep link-urile: Verifică că link-urile externe către rute protejate redirecționează corect la login

Exemplu concret de simplificare — asta e partea satisfăcătoare:

// ÎNAINTE — app/(app)/_layout.tsx
export default function AppLayout() {
  const { session, isLoading } = useSession();
  if (isLoading) return <Text>Se încarcă...</Text>;
  if (!session) return <Redirect href="/sign-in" />;
  return <Tabs />;
}

// DUPĂ — app/(app)/_layout.tsx (simplificat)
export default function AppLayout() {
  return <Tabs />; // Protecția e gestionată de Stack.Protected în root
}

Întrebări Frecvente (FAQ)

Stack.Protected funcționează cu deep link-uri?

Da, și acesta e unul dintre avantajele principale. Spre deosebire de redirect-urile manuale, Stack.Protected interceptează navigarea la nivel de navigator, astfel încât un deep link către o rută protejată redirecționează automat la ecranul de login fără a monta ecranul protejat. După autentificare, poți redirecționa utilizatorul la destinația inițială.

Pot folosi Stack.Protected pentru controlul accesului bazat pe roluri?

Absolut. Stack.Protected acceptă orice expresie booleană ca guard, deci poți verifica roluri, permisiuni sau orice altă condiție. Componentele Protected pot fi imbricate pentru a crea ierarhii de acces — de exemplu, un utilizator trebuie să fie autentificat ȘI admin pentru a accesa rutele admin.

Stack.Protected înlocuiește validarea pe server?

Nu, și asta e important de reținut. Rutele protejate funcționează exclusiv pe client. Ele previn navigarea în interfață, dar nu securizează datele. Orice API call trebuie validat independent pe server cu un token de autentificare valid. Gândește-te la Stack.Protected ca la o componentă de UX, nu de securitate.

Ce se întâmplă când sesiunea expiră în timp ce utilizatorul e pe un ecran protejat?

Dacă starea sesiunii din context se schimbă (de exemplu, session devine null), guard-ul se reevaluează automat. Utilizatorul e redirecționat imediat la primul ecran disponibil (de regulă, login), iar toate intrările din istoricul de navigare pentru rutele protejate sunt eliminate.

Cum gestionez starea de încărcare inițială fără să apară un ecran gol?

Cea mai bună practică e să folosești expo-splash-screen pentru a menține splash screen-ul nativ vizibil cât timp se citește token-ul din SecureStore. Apelează SplashScreen.preventAutoHideAsync() la pornirea aplicației și SplashScreen.hideAsync() când isLoading devine false. Astfel, utilizatorul vede splash screen-ul nativ (rapid și fluid) în loc de un ecran alb sau un spinner.

Despre Autor Editorial Team

Our team of expert writers and editors.