feat(auth): add cold-start token verification with loading state to app layout
Move token verification from root layout SessionGate to (app) group layout, enabling cold-start verification without blocking public pages (privacy/terms). Show ActivityIndicator while verification is in progress, then redirect to login if unverified. BREAKING CHANGE: Authentication flow changed - token verification now occurs in the (app) layout group instead of root layout SessionGate wrapper.
This commit is contained in:
2
app.json
2
app.json
@@ -2,7 +2,7 @@
|
||||
"expo": {
|
||||
"name": "威友",
|
||||
"slug": "qojo",
|
||||
"version": "0.0.2",
|
||||
"version": "1.0.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
|
||||
@@ -1,24 +1,54 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
import { Redirect } from 'expo-router';
|
||||
|
||||
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
|
||||
import { messageManager, useAuthStore } from '../../src/stores';
|
||||
import { useRegisterPushDevice } from '../../src/hooks';
|
||||
import { hrefAuthLogin } from '../../src/navigation/hrefs';
|
||||
import { useAppColors } from '../../src/theme';
|
||||
|
||||
export default function AppLayout() {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
||||
const userID = useAuthStore((s) => s.currentUser?.id);
|
||||
const colors = useAppColors();
|
||||
const [verified, setVerified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 冷启动 token 校验:仅鉴权分组 (app) 需要。
|
||||
// 公开页(/privacy、/terms)和 (auth) 分组在根布局直接渲染,不受此校验影响,
|
||||
// 因此冷启动 401 不会把公开页顶到 /login。
|
||||
fetchCurrentUser().finally(() => setVerified(true));
|
||||
}, [fetchCurrentUser]);
|
||||
|
||||
useRegisterPushDevice(isAuthenticated, userID);
|
||||
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
}, []);
|
||||
|
||||
useRegisterPushDevice(isAuthenticated, userID);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Redirect href={hrefAuthLogin()} />;
|
||||
// 持久化状态显示已登录则立即渲染(后台静默校验)
|
||||
if (isAuthenticated) {
|
||||
return <AppRouteStack />;
|
||||
}
|
||||
|
||||
return <AppRouteStack />;
|
||||
// 未登录且校验未完成,显示 loading
|
||||
if (!verified) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 校验完成仍未登录,跳转登录页
|
||||
return <Redirect href={hrefAuthLogin()} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, View, useWindowDimensions } from 'react-native';
|
||||
import { Redirect } from 'expo-router';
|
||||
|
||||
import { AppDesktopShell } from '../../src/app-navigation/AppDesktopShell';
|
||||
@@ -7,23 +7,50 @@ import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
|
||||
import { BREAKPOINTS } from '../../src/hooks';
|
||||
import { messageManager, useAuthStore } from '../../src/stores';
|
||||
import { hrefAuthLogin } from '../../src/navigation/hrefs';
|
||||
import { useAppColors } from '../../src/theme';
|
||||
|
||||
export default function AppLayoutWeb() {
|
||||
const { width } = useWindowDimensions();
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
||||
const useDesktopShell = width >= BREAKPOINTS.desktop;
|
||||
const colors = useAppColors();
|
||||
const [verified, setVerified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 冷启动 token 校验:仅鉴权分组 (app) 需要。
|
||||
// 公开页(/privacy、/terms)和 (auth) 分组在根布局直接渲染,不受此校验影响。
|
||||
fetchCurrentUser().finally(() => setVerified(true));
|
||||
}, [fetchCurrentUser]);
|
||||
|
||||
useEffect(() => {
|
||||
messageManager.initialize();
|
||||
}, []);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Redirect href={hrefAuthLogin()} />;
|
||||
// 持久化状态显示已登录则立即渲染(后台静默校验)
|
||||
if (isAuthenticated) {
|
||||
if (useDesktopShell) {
|
||||
return <AppDesktopShell />;
|
||||
}
|
||||
return <AppRouteStack />;
|
||||
}
|
||||
|
||||
if (useDesktopShell) {
|
||||
return <AppDesktopShell />;
|
||||
// 未登录且校验未完成,显示 loading
|
||||
if (!verified) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <AppRouteStack />;
|
||||
// 校验完成仍未登录,跳转登录页
|
||||
return <Redirect href={hrefAuthLogin()} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '../src/polyfills';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { AppState, AppStateStatus, Platform, View, ActivityIndicator } from 'react-native';
|
||||
import { Stack, useRouter } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
@@ -11,8 +11,6 @@ import * as SystemUI from 'expo-system-ui';
|
||||
import { useFonts } from 'expo-font';
|
||||
|
||||
import { registerNotificationPresentationHandler } from '@/services/notification';
|
||||
import { api } from '@/services/core';
|
||||
import { wsService } from '@/services/core';
|
||||
import { EventSubscriber } from '../src/infrastructure/EventSubscriber';
|
||||
import {
|
||||
ThemeBootstrap,
|
||||
@@ -24,7 +22,6 @@ import {
|
||||
import AppPromptBar from '../src/components/common/AppPromptBar';
|
||||
import AppDialogHost from '../src/components/common/AppDialogHost';
|
||||
import { installAlertOverride } from '@/services/ui';
|
||||
import { useAuthStore } from '../src/stores';
|
||||
import { checkForAPKUpdate } from '@/services/platform';
|
||||
import { CallScreen, IncomingCallModal, FloatingCallWindow } from '../src/components/call';
|
||||
import { jpushService } from '@/services/notification/jpushService';
|
||||
@@ -104,36 +101,6 @@ function SystemChrome() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function SessionGate({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
||||
const colors = useAppColors();
|
||||
const [verified, setVerified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentUser().finally(() => setVerified(true));
|
||||
}, [fetchCurrentUser]);
|
||||
|
||||
// If persisted state shows authenticated, render immediately (background verify)
|
||||
// If not authenticated and not yet verified, show loading
|
||||
if (!isAuthenticated && !verified) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.background.default,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
// If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function NotificationBootstrap() {
|
||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||
const permissionRequested = useRef(false);
|
||||
@@ -280,18 +247,16 @@ function ThemedStack() {
|
||||
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
|
||||
<SystemChrome />
|
||||
<EventSubscriber />
|
||||
<SessionGate>
|
||||
<NotificationBootstrap />
|
||||
<APKUpdateBootstrap />
|
||||
<CallKeepBootstrap />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</SessionGate>
|
||||
<NotificationBootstrap />
|
||||
<APKUpdateBootstrap />
|
||||
<CallKeepBootstrap />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "with_you",
|
||||
"version": "0.0.2",
|
||||
"version": "1.0.0",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
|
||||
Reference in New Issue
Block a user