diff --git a/app/_layout.tsx b/app/_layout.tsx index a31eaf3..9a360af 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -51,11 +51,12 @@ if (__DEV__) { function Bootstrapper({ children }: { children: React.ReactNode }) { const dispatch = useAppDispatch(); - const { profile } = useAppSelector((state) => state.user); + const { profile, onboardingCompleted } = useAppSelector((state) => state.user); const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule); const [showPrivacyModal, setShowPrivacyModal] = React.useState(false); const { isLoggedIn } = useAuthGuard() const fastingHydrationRequestedRef = React.useRef(false); + const permissionInitializedRef = React.useRef(false); // 初始化快捷动作处理 useQuickActions(); @@ -94,11 +95,11 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } }, [isLoggedIn]); + // ==================== 基础服务初始化(不需要权限,总是执行)==================== React.useEffect(() => { - // ==================== 第一优先级:立即执行(关键路径,0-500ms)==================== - const initializeCriticalServices = async () => { + const initializeBasicServices = async () => { try { - logger.info('🚀 开始初始化关键服务...'); + logger.info('🚀 开始初始化基础服务(不需要权限)...'); // 1. 加载用户数据(首屏展示需要) await dispatch(fetchMyProfile()); @@ -108,57 +109,65 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { initializeHealthPermissions(); logger.info('✅ HealthKit 权限系统初始化完成'); - // 3. 初始化通知服务基础功能 - await notificationService.initialize(); - logger.info('✅ 通知服务初始化完成'); - - // 4. 初始化快捷动作(用户可能立即使用) + // 3. 初始化快捷动作(用户可能立即使用) await setupQuickActions(); logger.info('✅ 快捷动作初始化完成'); - // 5. 清空 AI 教练会话缓存(轻量操作) + // 4. 清空 AI 教练会话缓存(轻量操作) clearAiCoachSessionCache(); logger.info('✅ AI 教练缓存清理完成'); - logger.info('🎉 关键服务初始化完成'); + // 5. 初始化喝水记录 Bridge + initializeWaterRecordBridge(); + logger.info('✅ 喝水记录 Bridge 初始化完成'); + + logger.info('🎉 基础服务初始化完成'); } catch (error) { - logger.error('❌ 关键服务初始化失败:', error); + logger.error('❌ 基础服务初始化失败:', error); } }; - // ==================== 第二优先级:短延迟(1-2秒后执行)==================== - const initializeSecondaryServices = () => { - setTimeout(async () => { - try { - logger.info('⏰ 开始初始化次要服务...'); + initializeBasicServices(); + }, [dispatch]); - // 1. 请求 HealthKit 权限(延迟到 3 秒,避免打断用户浏览) - setTimeout(async () => { - try { - await ensureHealthPermissions(); - logger.info('✅ HealthKit 权限请求完成'); - } catch (error) { - logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error); - } - }, 3000); + // ==================== 权限相关服务初始化(仅在 onboarding 完成后执行)==================== + React.useEffect(() => { + // 如果还没完成 onboarding,或已经初始化过权限,则跳过 + if (!onboardingCompleted || permissionInitializedRef.current) { + return; + } - // 2. 初始化喝水记录 Bridge - initializeWaterRecordBridge(); - logger.info('✅ 喝水记录 Bridge 初始化完成'); + permissionInitializedRef.current = true; - // 3. 异步同步 Widget 数据(不阻塞主流程) - syncWidgetDataInBackground(); + const initializePermissionServices = async () => { + try { + logger.info('🔐 开始初始化需要权限的服务(onboarding 已完成)...'); - logger.info('🎉 次要服务初始化完成'); - } catch (error) { - logger.error('❌ 次要服务初始化失败:', error); - } - }, 2000); + // 1. 初始化通知服务(包含权限请求) + await notificationService.initialize(); + logger.info('✅ 通知服务初始化完成'); + + // 2. 延迟请求 HealthKit 权限(避免立即弹窗打断用户) + setTimeout(async () => { + try { + await ensureHealthPermissions(); + logger.info('✅ HealthKit 权限请求完成'); + } catch (error) { + logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error); + } + }, 2000); + + // 3. 异步同步 Widget 数据 + syncWidgetDataInBackground(); + + logger.info('🎉 权限相关服务初始化完成'); + } catch (error) { + logger.error('❌ 权限相关服务初始化失败:', error); + } }; - // ==================== 第三优先级:中延迟(3-5秒后或交互完成后执行)==================== + // ==================== 后台服务初始化(延迟执行)==================== const initializeBackgroundServices = () => { - // 使用 InteractionManager 确保在所有交互和动画完成后再执行 const { InteractionManager } = require('react-native'); InteractionManager.runAfterInteractions(() => { @@ -183,7 +192,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { }); }; - // ==================== 第四优先级:空闲时执行(不影响用户体验)==================== + // ==================== 空闲服务初始化==================== const initializeIdleServices = () => { setTimeout(async () => { try { @@ -202,7 +211,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } catch (error) { logger.error('❌ 空闲服务初始化失败:', error); } - }, 8000); // 8秒后执行,确保不影响用户体验 + }, 8000); }; // ==================== 辅助函数 ==================== @@ -350,13 +359,8 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { } }; - // ==================== 执行初始化流程 ==================== - - // 立即执行关键服务 - initializeCriticalServices(); - - // 延迟执行次要服务 - initializeSecondaryServices(); + // 执行权限相关初始化 + initializePermissionServices(); // 交互完成后执行后台服务 initializeBackgroundServices(); @@ -364,7 +368,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { // 空闲时执行非关键服务 initializeIdleServices(); - }, [dispatch, profile.name]); + }, [onboardingCompleted, profile.name]); React.useEffect(() => { diff --git a/app/index.tsx b/app/index.tsx index 0ae6da5..be993cb 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -6,9 +6,6 @@ import { preloadUserData } from '@/store/userSlice'; import { router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, View } from 'react-native'; -import AsyncStorage from '@/utils/kvStore'; -import { STORAGE_KEYS } from '@/services/api'; -const ONBOARDING_COMPLETED_KEY = STORAGE_KEYS.onboardingCompleted; export default function SplashScreen() { const backgroundColor = useThemeColor({}, 'background'); @@ -22,27 +19,28 @@ export default function SplashScreen() { const checkOnboardingStatus = async () => { try { - // 先预加载用户数据,这样进入应用时就有正确的 token 状态 - console.log('开始预加载用户数据...'); - await preloadUserData(); - console.log('用户数据预加载完成'); + // 先预加载用户数据,包括 onboarding 状态 + console.log('开始预加载用户数据(包含 onboarding 状态)...'); + const userData = await preloadUserData(); + console.log('用户数据预加载完成,onboarding 状态:', userData.onboardingCompleted); - // 初始化推送通知(不阻塞应用启动) - console.log('开始初始化推送通知...'); + // 初始化推送通知(不阻塞应用启动,且不会请求权限) + console.log('开始初始化推送通知基础服务...'); initializePushNotifications().catch((error) => { console.warn('推送通知初始化失败,但不影响应用正常使用:', error); }); - const onboardingCompleted = await AsyncStorage.getItem(ONBOARDING_COMPLETED_KEY); - - if (onboardingCompleted === 'true') { + // 根据预加载的状态决定跳转 + if (userData.onboardingCompleted) { + console.log('用户已完成引导,跳转到统计页面'); router.replace(ROUTES.TAB_STATISTICS); } else { + console.log('用户未完成引导,跳转到引导页面'); router.replace(ROUTES.ONBOARDING); } } catch (error) { console.error('检查引导状态或预加载用户数据失败:', error); - // 如果出现错误,仍然进入应用,但可能会有状态更新 + // 如果出现错误,默认进入主应用(假设已完成引导) router.replace(ROUTES.TAB_STATISTICS); } setIsLoading(false); diff --git a/app/onboarding.tsx b/app/onboarding.tsx index 87af3a5..60dc897 100644 --- a/app/onboarding.tsx +++ b/app/onboarding.tsx @@ -22,10 +22,8 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { palette } from '@/constants/Colors'; import { ROUTES } from '@/constants/Routes'; -import { STORAGE_KEYS } from '@/services/api'; -import AsyncStorage from '@/utils/kvStore'; - -const ONBOARDING_COMPLETED_KEY = STORAGE_KEYS.onboardingCompleted; +import { useAppDispatch } from '@/hooks/redux'; +import { setOnboardingCompleted } from '@/store/userSlice'; type OnboardingSlide = { key: string; @@ -63,6 +61,7 @@ const SLIDES: OnboardingSlide[] = [ export default function OnboardingScreen() { const router = useRouter(); + const dispatch = useAppDispatch(); const { width } = useWindowDimensions(); const [currentIndex, setCurrentIndex] = useState(0); const listRef = useRef>(null); @@ -92,9 +91,10 @@ export default function OnboardingScreen() { ); const completeOnboarding = useCallback(async () => { - await AsyncStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); + // 通过 Redux 更新 onboarding 状态(会自动保存到 AsyncStorage) + await dispatch(setOnboardingCompleted()); router.replace(ROUTES.TAB_STATISTICS); - }, [router]); + }, [dispatch, router]); const handleSkip = useCallback(() => { completeOnboarding(); diff --git a/store/userSlice.ts b/store/userSlice.ts index a25a6ba..9986111 100644 --- a/store/userSlice.ts +++ b/store/userSlice.ts @@ -10,15 +10,17 @@ let preloadedUserData: { token: string | null; profile: UserProfile; privacyAgreed: boolean; + onboardingCompleted: boolean; } | null = null; // 预加载用户数据的函数 export async function preloadUserData() { try { - const [profileStr, privacyAgreedStr, token] = await Promise.all([ + const [profileStr, privacyAgreedStr, token, onboardingCompletedStr] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEYS.userProfile), AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed), AsyncStorage.getItem(STORAGE_KEYS.authToken), + AsyncStorage.getItem(STORAGE_KEYS.onboardingCompleted), ]); let profile: UserProfile = { @@ -35,20 +37,24 @@ export async function preloadUserData() { } const privacyAgreed = privacyAgreedStr === 'true'; + const onboardingCompleted = onboardingCompletedStr === 'true'; // 如果有 token,需要设置到 API 客户端 if (token) { await setAuthToken(token); } - preloadedUserData = { token, profile, privacyAgreed }; + preloadedUserData = { token, profile, privacyAgreed, onboardingCompleted }; return preloadedUserData; } catch (error) { console.error('预加载用户数据失败:', error); preloadedUserData = { - token: null, profile: { + token: null, + profile: { memberNumber: 0 - }, privacyAgreed: false + }, + privacyAgreed: false, + onboardingCompleted: false }; return preloadedUserData; } @@ -56,7 +62,7 @@ export async function preloadUserData() { // 获取预加载的用户数据 function getPreloadedUserData() { - return preloadedUserData || { token: null, profile: {}, privacyAgreed: false }; + return preloadedUserData || { token: null, profile: {}, privacyAgreed: false, onboardingCompleted: false }; } export type Gender = 'male' | 'female' | ''; @@ -108,6 +114,7 @@ export type UserState = { error: string | null; weightHistory: WeightHistoryItem[]; activityHistory: ActivityHistoryItem[]; + onboardingCompleted: boolean; // 是否完成引导流程 }; export const DEFAULT_MEMBER_NAME = '朋友'; @@ -128,6 +135,7 @@ const getInitialState = (): UserState => { error: null, weightHistory: [], activityHistory: [], + onboardingCompleted: preloaded.onboardingCompleted, // 引导完成状态 }; }; @@ -207,6 +215,12 @@ export const setPrivacyAgreed = createAsyncThunk('user/setPrivacyAgreed', async return true; }); +// 设置 onboarding 完成状态 +export const setOnboardingCompleted = createAsyncThunk('user/setOnboardingCompleted', async () => { + await AsyncStorage.setItem(STORAGE_KEYS.onboardingCompleted, 'true'); + return true; +}); + export const logout = createAsyncThunk('user/logout', async () => { await Promise.all([ AsyncStorage.removeItem(STORAGE_KEYS.authToken), @@ -364,6 +378,9 @@ const userSlice = createSlice({ }) .addCase(setPrivacyAgreed.fulfilled, (state) => { }) + .addCase(setOnboardingCompleted.fulfilled, (state) => { + state.onboardingCompleted = true; + }) .addCase(fetchWeightHistory.fulfilled, (state, action) => { state.weightHistory = action.payload; })