Autentifikatsiya oqimi — himoyalangan ekranlar

~20 daq · React Native · Navigatsiya — Expo Router

Muammodan boshlaylik

Ilova ochilganda:

  • Foydalanuvchi kirgan bo'lsa — darhol bosh sahifa
  • Kirmagan bo'lsa — kirish ekrani
  • Kirgandan keyin "orqaga" bosilsa — kirish ekraniga qaytmasligi kerak
  • Chiqqandan keyin — "orqaga" bilan profilga qaytib bo'lmasligi kerak
  • Deep link bilan /buyurtmalar ochilsa, lekin foydalanuvchi kirmagan — avval kirish, keyin buyurtmalar

Har bir ekranda "kirganmi?" tekshiruvini yozish — takrorlanish va xatolar. Kerak — markazlashgan yechim.


Tuzilma

app/
├── _layout.tsx             ← AuthProvider + himoya mantig'i
├── kirish.tsx              ← ochiq
├── royxat.tsx              ← ochiq
└── (app)/
    ├── _layout.tsx         ← himoyalangan qism
    └── (tabs)/
        ├── _layout.tsx
        ├── index.tsx
        └── profil.tsx

Ochiq ekranlar — tashqarida, himoyalanganlar — (app) guruhida.


1. Auth holati — Context

// context/auth.tsx
import { createContext, useContext, useEffect, useState, PropsWithChildren } from 'react';
import * as SecureStore from 'expo-secure-store';

type Foydalanuvchi = { id: number; ism: string; email: string };

type AuthContextTuri = {
  foydalanuvchi: Foydalanuvchi | null;
  yuklanmoqda: boolean;
  kirish: (email: string, parol: string) => Promise<void>;
  chiqish: () => Promise<void>;
};

const AuthContext = createContext<AuthContextTuri | null>(null);

export function AuthProvider({ children }: PropsWithChildren) {
  const [foydalanuvchi, setFoydalanuvchi] = useState<Foydalanuvchi | null>(null);
  const [yuklanmoqda, setYuklanmoqda] = useState(true);

  // Ilova ochilganda — saqlangan tokenni tekshirish
  useEffect(() => {
    (async () => {
      try {
        const token = await SecureStore.getItemAsync('token');
        if (token) {
          const javob = await fetch('https://api.dokon.uz/men', { headers: { Authorization: `Bearer ${token}` } });
          if (javob.ok) setFoydalanuvchi(await javob.json());
          else await SecureStore.deleteItemAsync('token');     // muddati o'tgan
        }
      } finally {
        setYuklanmoqda(false);
      }
    })();
  }, []);

  async function kirish(email: string, parol: string) {
    const javob = await fetch('https://api.dokon.uz/kirish', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, parol }),
    });
    if (!javob.ok) throw new Error("Email yoki parol noto'g'ri");
    const { token, foydalanuvchi } = await javob.json();
    await SecureStore.setItemAsync('token', token);
    setFoydalanuvchi(foydalanuvchi);
  }

  async function chiqish() {
    await SecureStore.deleteItemAsync('token');
    setFoydalanuvchi(null);
  }

  return (
    <AuthContext.Provider value={{ foydalanuvchi, yuklanmoqda, kirish, chiqish }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth faqat AuthProvider ichida');
  return ctx;
}
  • Token — SecureStore da (35-dars): qurilmaning shifrlangan xotirasida, AsyncStorage da emas
  • yuklanmoqda — ilova ochilgandagi tekshiruv tugaguncha true

2. Himoya — Stack.Protected

Expo Router ning zamonaviy versiyalarida himoyalangan ekranlar deklarativ belgilanadi:

// app/_layout.tsx
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { AuthProvider, useAuth } from '@/context/auth';

SplashScreen.preventAutoHideAsync();

function Navigatsiya() {
  const { foydalanuvchi, yuklanmoqda } = useAuth();

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

  if (yuklanmoqda) return null;           // splash ekran hali ko'rinib turadi

  const kirgan = !!foydalanuvchi;

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

      <Stack.Protected guard={!kirgan}>
        <Stack.Screen name="kirish" />
        <Stack.Screen name="royxat" />
      </Stack.Protected>
    </Stack>
  );
}

export default function RootLayout() {
  return (
    <AuthProvider>
      <Navigatsiya />
    </AuthProvider>
  );
}
  • guard={kirgan} — faqat kirgan foydalanuvchiga ochiq ekranlar
  • guard false bo'lsa — ekranlar mavjud emas: ularga o'tishga urinish avtomatik ravishda ochiq ekranga yo'naltiriladi
  • foydalanuvchi o'zgarishi bilan (kirish/chiqish) — navigatsiya o'zi to'g'ri ekranga o'tadi, stack tozalanadi

Splash ekran

Ilova ochilganda token tekshirilayotgan 0.5 soniya ichida nima ko'rsatish kerak? Kirish ekranini ko'rsatib, keyin bosh sahifaga "sakrash" — yomon tajriba. SplashScreen.preventAutoHideAsync() — tekshiruv tugaguncha tizim splash ekrani (logotip) ko'rinib turadi.


3. Kirish ekrani

// app/kirish.tsx
import { useAuth } from '@/context/auth';

export default function Kirish() {
  const { kirish } = useAuth();
  const [xato, setXato] = useState('');
  const { control, handleSubmit, formState: { isSubmitting } } = useForm<KirishForma>({
    resolver: zodResolver(kirishSxema),
  });

  async function yuborish(d: KirishForma) {
    setXato('');
    try {
      await kirish(d.email, d.parol);
      // router.replace shart EMAS — Stack.Protected o'zi (app) ga o'tkazadi
    } catch (e) {
      setXato((e as Error).message);
    }
  }

  return (/* forma — 21-22 darslar */);
}

Kirish muvaffaqiyatli → foydalanuvchi o'rnatildi → guard o'zgardi → navigatsiya o'zi bosh sahifaga o'tadi. Kirish ekrani stack dan butunlay olib tashlanadi — "orqaga" bilan qaytib bo'lmaydi.

Chiqish

// app/(app)/(tabs)/profil.tsx
const { foydalanuvchi, chiqish } = useAuth();

<Tugma matn="Chiqish" turi="xavfli" onPress={() =>
  Alert.alert('Chiqish', 'Hisobdan chiqmoqchimisiz?', [
    { text: 'Bekor', style: 'cancel' },
    { text: 'Chiqish', style: 'destructive', onPress: chiqish },
  ])
} />

Eski usul: Redirect

Ba'zi loyihalarda Stack.Protected o'rniga layoutda yo'naltirish ishlatiladi:

// app/(app)/_layout.tsx
import { Redirect, Stack } from 'expo-router';

export default function AppLayout() {
  const { foydalanuvchi, yuklanmoqda } = useAuth();
  if (yuklanmoqda) return <ActivityIndicator />;
  if (!foydalanuvchi) return <Redirect href="/kirish" />;
  return <Stack />;
}

Ishlaydi, lekin har bir himoyalangan guruhda takrorlanadi. Yangi kodda — Stack.Protected.


Rollar

Admin va oddiy foydalanuvchi:

<Stack.Protected guard={kirgan}>
  <Stack.Screen name="(app)" />
  <Stack.Protected guard={foydalanuvchi?.rol === 'admin'}>
    <Stack.Screen name="admin" />
  </Stack.Protected>
</Stack.Protected>

[!WARNING] Ilovadagi himoya — faqat interfeys uchun: foydalanuvchiga keraksiz ekranni ko'rsatmaslik. Haqiqiy himoya — serverda: har bir API so'rov tokenni va huquqlarni tekshirishi kerak. Ilova kodini ochib ko'rish va API ga to'g'ridan-to'g'ri so'rov yuborish qiyin emas (PHP kursi 43-dars, MySQL 50-dars).


Nima uchun shunday?

Nima uchun kirgandan keyin router.replace('/bosh') yozish o'rniga holatga tayanamiz?

Chunki "kirgan / kirmagan" — bu holat, va navigatsiya shu holatdan kelib chiqishi kerak. Agar yo'naltirishni qo'lda qilsangiz, uni har joyda esda tutish kerak: kirishda, ro'yxatdan o'tishda, token muddati o'tganda, boshqa qurilmadan chiqarilganda, deep link bilan kelganda... Bittasi unutilsa — foydalanuvchi himoyalangan ekranda "osilib" qoladi. Deklarativ himoya bilan esa qoida bitta joyda: holat o'zgardi — navigatsiya o'zi moslashadi.


Xulosa

Eslab qoling:

  • Ochiq ekranlar — tashqarida, himoyalanganlar — (app) guruhida
  • AuthProvider + useAuth() — foydalanuvchi, yuklanish, kirish, chiqish
  • Token — expo-secure-store da
  • <Stack.Protected guard={kirgan}> — deklarativ himoya; holat o'zgarsa navigatsiya o'zi o'tadi
  • Kirishdan keyin router.replace shart emas
  • SplashScreen.preventAutoHideAsync() — token tekshirilguncha logotip
  • Ichma-ich Protected — rollar
  • Ilovadagi himoya — faqat UI; haqiqiy himoya — serverda

Keyingi dars

31-dars: fetch bilan API — yuklanish, xato, bekor qilish