Autentifikatsiya oqimi — himoyalangan ekranlar
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
/buyurtmalarochilsa, 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 tugagunchatrue
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 ekranlarguardfalsebo'lsa — ekranlar mavjud emas: ularga o'tishga urinish avtomatik ravishda ochiq ekranga yo'naltiriladifoydalanuvchio'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-storeda <Stack.Protected guard={kirgan}>— deklarativ himoya; holat o'zgarsa navigatsiya o'zi o'tadi- Kirishdan keyin
router.replaceshart 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