From b2979311bba848ea3ecf9322737c984f9805edff Mon Sep 17 00:00:00 2001
From: lafay <2021211506@stu.hit.edu.cn>
Date: Tue, 16 Jun 2026 19:03:26 +0800
Subject: [PATCH] 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.
---
app.json | 2 +-
app/(app)/_layout.tsx | 42 ++++++++++++++++++++++++-----
app/(app)/_layout.web.tsx | 41 +++++++++++++++++++++++-----
app/_layout.tsx | 57 ++++++++-------------------------------
package.json | 2 +-
5 files changed, 83 insertions(+), 61 deletions(-)
diff --git a/app.json b/app.json
index 5aaca0c..b256186 100644
--- a/app.json
+++ b/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",
diff --git a/app/(app)/_layout.tsx b/app/(app)/_layout.tsx
index 45da32a..09b6e0a 100644
--- a/app/(app)/_layout.tsx
+++ b/app/(app)/_layout.tsx
@@ -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 ;
+ // 持久化状态显示已登录则立即渲染(后台静默校验)
+ if (isAuthenticated) {
+ return ;
}
- return ;
+ // 未登录且校验未完成,显示 loading
+ if (!verified) {
+ return (
+
+
+
+ );
+ }
+
+ // 校验完成仍未登录,跳转登录页
+ return ;
}
diff --git a/app/(app)/_layout.web.tsx b/app/(app)/_layout.web.tsx
index 93122a5..65bfb88 100644
--- a/app/(app)/_layout.web.tsx
+++ b/app/(app)/_layout.web.tsx
@@ -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 ;
+ // 持久化状态显示已登录则立即渲染(后台静默校验)
+ if (isAuthenticated) {
+ if (useDesktopShell) {
+ return ;
+ }
+ return ;
}
- if (useDesktopShell) {
- return ;
+ // 未登录且校验未完成,显示 loading
+ if (!verified) {
+ return (
+
+
+
+ );
}
- return ;
+ // 校验完成仍未登录,跳转登录页
+ return ;
}
diff --git a/app/_layout.tsx b/app/_layout.tsx
index a37a86e..8069ae5 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -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 (
-
-
-
- );
- }
- // If verified and not authenticated, the Redirect in (app)/_layout.tsx handles it
- return <>{children}>;
-}
-
function NotificationBootstrap() {
const appState = useRef(AppState.currentState);
const permissionRequested = useRef(false);
@@ -280,18 +247,16 @@ function ThemedStack() {
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/package.json b/package.json
index 92824d9..bcba61a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "with_you",
- "version": "0.0.2",
+ "version": "1.0.0",
"main": "expo-router/entry",
"scripts": {
"start": "expo start",