身份验证流程与受保护路由
实现注册、登录和会话持久化,设置一个可在身份验证堆栈与应用堆栈之间切换的根导航器,并保护所有数据页面,防止未通过身份验证的访问。
身份验证流程与受保护路由 是 CoddyKit 上的免费 React Native Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 React Native Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 React Native Academy 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Authentication Architecture Overview
A production auth flow has three states your navigator must handle: loading (checking if a session exists from storage), unauthenticated (show auth screens), and authenticated (show app screens). The key insight is that your root navigator switches between the auth stack and the app stack based on session state — users cannot access data screens without a valid session, and authenticated users cannot land on the login screen.
// Auth states:
// 'loading' -> Show a splash/loading screen
// 'unauthenticated' -> Show auth stack (Sign In, Sign Up)
// 'authenticated' -> Show app stack (tabs, data screens)
// The navigator reads session from AuthContext:
const { session, isLoading } = useAuth();
if (isLoading) return <SplashScreen />;
return (
<NavigationContainer>
{session ? <AppNavigator /> : <AuthNavigator />}
</NavigationContainer>
);AuthContext Setup
Create an AuthContext that provides the current session, user object, sign-in, sign-up, and sign-out functions to the entire component tree. On mount, check AsyncStorage or Supabase's auto-restored session to determine if the user is already logged in from a previous app launch. This prevents the auth screens from flashing before the session is restored.
// src/context/AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { Session } from '@supabase/supabase-js';
import { supabase } from '../services/supabase';
interface AuthContextType {
session: Session | null;
isLoading: boolean;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType>(null!);
export function AuthProvider({ children }) {
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
setSession(data.session);
setIsLoading(false);
});
const { data: listener } = supabase.auth.onAuthStateChange(
(_event, session) => setSession(session)
);
return () => listener.subscription.unsubscribe();
}, []);
const signOut = async () => { await supabase.auth.signOut(); };
return (
<AuthContext.Provider value={{ session, isLoading, signOut }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);Wrapping the App with AuthProvider
Wrap your entire application in AuthProvider at the entry point so every component can access the session. Place it outside NavigationContainer since the navigator itself reads from auth context. Also wrap with QueryClientProvider if using React Query. The order matters: data providers should wrap navigation, not be nested inside specific screens.
// App.tsx (entry point)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from './src/context/AuthContext';
import { RootNavigator } from './src/navigation/RootNavigator';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<RootNavigator />
</AuthProvider>
</QueryClientProvider>
);
}Sign-Up Screen Implementation
The sign-up screen collects email and password, validates them locally, and calls supabase.auth.signUp(). On success, Supabase may require email confirmation — handle this case by showing a 'Check your email' message instead of immediately navigating. Use react-hook-form for validation to avoid manual state management for each field's error state.
// src/screens/auth/SignUpScreen.tsx
import { useForm, Controller } from 'react-hook-form';
import { supabase } from '../../services/supabase';
export function SignUpScreen({ navigation }) {
const { control, handleSubmit, formState: { errors } } = useForm();
const [message, setMessage] = useState('');
const onSubmit = async ({ email, password }) => {
const { error } = await supabase.auth.signUp({ email, password });
if (error) {
setMessage(error.message);
} else {
setMessage('Check your email to confirm your account!');
}
};
return (
<View>
<Controller
control={control}
name='email'
rules={{ required: true, pattern: /^[^@]+@[^@]+$/ }}
render={({ field: { onChange, value } }) => (
<TextInput
value={value}
onChangeText={onChange}
placeholder='Email'
keyboardType='email-address'
/>
)}
/>
{/* Password field similar pattern */}
<Button title='Sign Up' onPress={handleSubmit(onSubmit)} />
{message ? <Text>{message}</Text> : null}
</View>
);
}Sign-In Screen and Session Persistence
The sign-in screen calls supabase.auth.signInWithPassword(). If successful, Supabase stores the session in AsyncStorage (configured during client setup) and the onAuthStateChange listener in AuthContext fires, updating the session state. The root navigator detects the new session and automatically navigates to the app stack — you do not need to call navigation.navigate manually after sign-in.
// Sign-in logic (simplified)
const signIn = async (email, password) => {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
setLoading(false);
if (error) {
// Show error to user
Alert.alert('Sign In Error', error.message);
return;
}
// No navigation.navigate() needed!
// AuthContext.onAuthStateChange fires automatically
// -> session is set -> root navigator switches to AppNavigator
};Root Navigator with Auth Guard
The root navigator is the gatekeeper that decides which navigation tree to show based on session state. It does not render any UI itself — it delegates to either AuthNavigator or AppNavigator. Because React Navigation re-renders when the navigator tree changes (auth → app), users are automatically redirected to the correct screens without explicit navigation calls on sign-in or sign-out.
// src/navigation/RootNavigator.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { useAuth } from '../context/AuthContext';
import { AuthNavigator } from './AuthNavigator';
import { AppTabNavigator } from './AppTabNavigator';
import { SplashScreen } from '../screens/SplashScreen';
const Stack = createStackNavigator();
export function RootNavigator() {
const { session, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />; // Full-screen loading state
}
return (
<NavigationContainer>
{session ? (
<AppTabNavigator /> // Authenticated: show app screens
) : (
<AuthNavigator /> // Unauthenticated: show auth screens
)}
</NavigationContainer>
);
}Protecting Individual Screens
The root navigator pattern protects entire navigation trees. But sometimes you need in-screen protection — for example, a feature only available to premium users. Create a useRequireAuth hook that checks the session and navigates to login if missing. Or create a ProtectedRoute wrapper component. This pattern is useful for deep-linked screens that might be accessed before authentication.
// src/hooks/useRequireAuth.ts
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '../context/AuthContext';
export function useRequireAuth() {
const { session, isLoading } = useAuth();
const navigation = useNavigation();
useEffect(() => {
if (!isLoading && !session) {
navigation.navigate('SignIn' as never);
}
}, [session, isLoading, navigation]);
return { session, isLoading };
}
// Usage in a screen:
export function PremiumFeatureScreen() {
const { session } = useRequireAuth();
if (!session) return null; // Navigation already happening
return <View>...</View>;
}Handling Token Expiry
Supabase JWT tokens expire (typically after 1 hour). The Supabase client configured with autoRefreshToken: true handles this automatically — it refreshes the token in the background before it expires. If refresh fails (e.g., the refresh token was revoked), onAuthStateChange fires with a SIGNED_OUT event, and your AuthContext clears the session. The navigator then automatically shows the auth screens, prompting the user to sign in again.
// Token expiry handling is automatic with Supabase:
// supabase client config (already set):
// {
// auth: {
// autoRefreshToken: true, <- handles renewal automatically
// persistSession: true,
// storage: AsyncStorage,
// }
// }
// Your AuthContext listener handles SIGNED_OUT:
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
// session is null -> navigator shows auth screens
setSession(null);
}
if (event === 'TOKEN_REFRESHED') {
// New session with fresh tokens
setSession(session);
}
});Password Reset Flow
Implement Forgot Password using Supabase's resetPasswordForEmail(). The user enters their email, Supabase sends a reset link, and when clicked, the link deep-links back to the app with a token. You configure the redirect URL in your Supabase dashboard's auth settings. On the app side, listen for the PASSWORD_RECOVERY auth event and navigate to a new password screen.
// Step 1: Send reset email
const resetPassword = async (email) => {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'habittracker://reset-password',
});
if (!error) Alert.alert('Email sent', 'Check your inbox.');
};
// Step 2: Handle deep link in AuthContext
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'PASSWORD_RECOVERY') {
// Navigate to update password screen
navigationRef.current?.navigate('UpdatePassword');
}
});
// Step 3: Update password screen
const updatePassword = async (newPassword) => {
const { error } = await supabase.auth.updateUser({
password: newPassword,
});
if (!error) navigation.navigate('Home');
};Sign Out and Session Cleanup
Sign out must clear all local state, not just the Supabase session. Call supabase.auth.signOut(), which removes the session from AsyncStorage and fires the SIGNED_OUT event. Also clear your React Query cache so the next user who logs in on the same device does not see the previous user's data. Reset navigation state if you have any screen-specific state in the navigator.
// Complete sign-out
const signOut = async () => {
// 1. Sign out from Supabase (clears AsyncStorage session)
await supabase.auth.signOut();
// 2. Clear React Query cache
queryClient.clear();
// 3. AuthContext detects SIGNED_OUT -> session = null
// 4. RootNavigator switches to AuthNavigator automatically
// No navigation.navigate() needed!
};
// In a Settings screen:
<TouchableOpacity onPress={signOut}>
<Text>Sign Out</Text>
</TouchableOpacity>Testing the Auth Flow
Test every auth path before moving to feature development: new user sign-up (email confirmation flow), existing user sign-in, wrong password error, session persistence after app restart, sign-out, and password reset. Automate the happy path with a Maestro test. Verify the navigator correctly transitions between auth and app states without flickering — the loading state prevents this flicker when checking AsyncStorage on startup.
# Maestro test for sign-in flow
---
appId: com.yourcompany.habittracker
---
- launchApp:
clearState: true
- assertVisible: 'Sign In'
- tapOn: 'Email'
- inputText: 'test@example.com'
- tapOn: 'Password'
- inputText: 'TestPass123!'
- tapOn: 'Sign In'
- waitForAnimationToEnd
- assertVisible: 'My Habits' # First app screen
- tapOn: 'Settings'
- tapOn: 'Sign Out'
- waitForAnimationToEnd
- assertVisible: 'Sign In' # Back to auth screenQuick Check
Test your understanding of React Native Mobile Development concepts from this lesson.
Lesson Recap
In this lesson you learned: how to build an AuthContext that tracks session state and subscribes to Supabase auth events, how the root navigator uses session state to switch between auth and app navigation trees, and how to implement sign-up, sign-in, sign-out, and password reset flows. You also saw how to clear React Query cache on sign-out to prevent data leaks between users. Next up we build the core data feed with offline support.
常见问题解答
「身份验证流程与受保护路由」课时是免费的吗?
是的 — 「身份验证流程与受保护路由」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 React Native Academy 课程的其余内容,请升级到 CoddyKit PRO。 React Native Academy 课程共包含 4 节课。
「身份验证流程与受保护路由」这节课中我会学到什么?
实现注册、登录和会话持久化,设置一个可在身份验证堆栈与应用堆栈之间切换的根导航器,并保护所有数据页面,防止未通过身份验证的访问。 你通过在浏览器中直接运行的动手代码来练习 React Native Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 React Native Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 React Native Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「身份验证流程与受保护路由」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 React Native Academy 课中编写并运行代码吗?
能。每节 React Native Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 规划架构与技术栈
- 身份验证流程与受保护路由
- 核心功能:支持离线的数据流
- 完善、测试并发布到两大应用商店