Защищённые экраны и авторизация
Паттерн: экраны логина и экраны приложения разделены. Пользователь видит одни или другие в зависимости от состояния.
Структура
RootStack
├── Auth (Stack)
│ ├── Login
│ └── Register
└── App (Stack)
├── Tabs
├── Details
└── Settings
Реализация
function RootNavigator() {
const { user, isLoading } = useAuth();
if (isLoading) return <SplashScreen />;
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{user ? (
<Stack.Group>
<Stack.Screen name="App" component={AppNavigator} />
<Stack.Screen name="Modal" component={ModalScreen} options={{ presentation: 'modal' }} />
</Stack.Group>
) : (
<Stack.Group>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Group>
)}
</Stack.Navigator>
);
}
Когда user меняется — навигатор автоматически переключается.
Auth Context
type AuthContextValue = {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function load() {
const token = await SecureStore.getItemAsync('token');
if (token) {
const user = await fetchUser(token);
setUser(user);
}
setIsLoading(false);
}
load();
}, []);
async function login(email: string, password: string) {
const { user, token } = await api.login(email, password);
await SecureStore.setItemAsync('token', token);
setUser(user);
}
async function logout() {
await SecureStore.deleteItemAsync('token');
setUser(null);
}
return (
<AuthContext.Provider value={{ user, isLoading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Экран логина
function LoginScreen() {
const { login } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit() {
setLoading(true);
setError('');
try {
await login(email, password);
} catch (e) {
setError((e as Error).message);
} finally {
setLoading(false);
}
}
return (
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={{ flex: 1 }}>
<View style={styles.container}>
<Text style={styles.title}>Вход</Text>
<TextInput value={email} onChangeText={setEmail} placeholder="Email" keyboardType="email-address" autoCapitalize="none" />
<TextInput value={password} onChangeText={setPassword} placeholder="Пароль" secureTextEntry />
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button label="Войти" onPress={handleSubmit} loading={loading} />
</View>
</KeyboardAvoidingView>
);
}
SecureStore
Для токенов:
npx expo install expo-secure-store
import * as SecureStore from 'expo-secure-store';
await SecureStore.setItemAsync('token', token);
const token = await SecureStore.getItemAsync('token');
await SecureStore.deleteItemAsync('token');
Токены в Keychain (iOS) / Keystore (Android). Не в AsyncStorage.
Logout
function SettingsScreen() {
const { logout } = useAuth();
return (
<Button
label="Выйти"
onPress={() => {
Alert.alert('Выйти?', 'Вы уверены?', [
{ text: 'Отмена', style: 'cancel' },
{ text: 'Выйти', style: 'destructive', onPress: logout },
]);
}}
/>
);
}
Навигатор сам переключится на экраны авторизации.
Обновление токена
Если используешь short-lived токены:
async function apiCall(url: string, options?: RequestInit) {
const token = await SecureStore.getItemAsync('token');
const res = await fetch(url, {
...options,
headers: { ...options?.headers, Authorization: `Bearer ${token}` },
});
if (res.status === 401) {
const refreshed = await refreshToken();
if (!refreshed) {
await logout();
return res;
}
return apiCall(url, options);
}
return res;
}
Защита по роли
function AdminScreen() {
const { user } = useAuth();
if (user?.role !== 'admin') {
return <NoAccessScreen />;
}
return <AdminPanel />;
}
Но это только UX. Настоящая защита — на сервере.
Итоги
- Два стека: Auth и App
userопределяет, какой показать- Auth Context с
loginиlogout SecureStoreдля токенов- Серверная защита обязательна