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