514 lines
18 KiB
TypeScript
514 lines
18 KiB
TypeScript
import '@/i18n';
|
||
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||
import { useFonts } from 'expo-font';
|
||
import { Stack, useRouter } from 'expo-router';
|
||
import { StatusBar } from 'expo-status-bar';
|
||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||
import 'react-native-reanimated';
|
||
|
||
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||
import { useQuickActions } from '@/hooks/useQuickActions';
|
||
import { hrvMonitorService } from '@/services/hrvMonitor';
|
||
import { clearBadgeCount, notificationService } from '@/services/notifications';
|
||
import { setupQuickActions } from '@/services/quickActions';
|
||
import { sleepMonitorService } from '@/services/sleepMonitor';
|
||
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
|
||
import { WaterRecordSource } from '@/services/waterRecords';
|
||
import { workoutMonitorService } from '@/services/workoutMonitor';
|
||
import { store } from '@/store';
|
||
import { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
||
import { fetchMyProfile, logout, setPrivacyAgreed } from '@/store/userSlice';
|
||
import { createWaterRecordAction } from '@/store/waterSlice';
|
||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||
import { initializeHealthPermissions } from '@/utils/health';
|
||
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||
import React, { useEffect } from 'react';
|
||
import { AppState, AppStateStatus } from 'react-native';
|
||
|
||
import { DialogProvider } from '@/components/ui/DialogProvider';
|
||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||
import { ToastProvider } from '@/contexts/ToastContext';
|
||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||
import { STORAGE_KEYS, setUnauthorizedHandler } from '@/services/api';
|
||
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
|
||
import { fetchChallenges } from '@/store/challengesSlice';
|
||
import AsyncStorage from '@/utils/kvStore';
|
||
import { logger } from '@/utils/logger';
|
||
import { Provider } from 'react-redux';
|
||
|
||
// 在开发环境中导入调试工具
|
||
let BackgroundTaskDebugger: any = null;
|
||
if (__DEV__) {
|
||
try {
|
||
const debuggerModule = require('@/services/backgroundTaskDebugger');
|
||
BackgroundTaskDebugger = debuggerModule.BackgroundTaskDebugger;
|
||
} catch (error) {
|
||
logger.warn('无法导入后台任务调试工具:', error);
|
||
}
|
||
}
|
||
|
||
|
||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||
const dispatch = useAppDispatch();
|
||
const router = useRouter();
|
||
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();
|
||
|
||
// 注册401未授权处理器(应用启动时执行一次)
|
||
React.useEffect(() => {
|
||
const handle401 = async () => {
|
||
try {
|
||
logger.info('[401处理] 开始处理登录过期');
|
||
|
||
// 清除Redux状态
|
||
await dispatch(logout());
|
||
|
||
// 跳转到登录页
|
||
router.push('/auth/login');
|
||
|
||
logger.info('[401处理] 登录过期处理完成');
|
||
} catch (error) {
|
||
logger.error('[401处理] 处理失败:', error);
|
||
}
|
||
};
|
||
|
||
setUnauthorizedHandler(handle401);
|
||
logger.info('[401处理器] 已注册到API服务');
|
||
}, [dispatch, router]);
|
||
|
||
React.useEffect(() => {
|
||
if (fastingHydrationRequestedRef.current) return;
|
||
if (activeFastingSchedule) {
|
||
fastingHydrationRequestedRef.current = true;
|
||
return;
|
||
}
|
||
|
||
fastingHydrationRequestedRef.current = true;
|
||
let cancelled = false;
|
||
|
||
const hydrate = async () => {
|
||
try {
|
||
const stored = await loadActiveFastingSchedule();
|
||
if (cancelled || !stored) return;
|
||
if (store.getState().fasting.activeSchedule) return;
|
||
dispatch(hydrateActiveSchedule(stored));
|
||
} catch (error) {
|
||
logger.warn('恢复断食计划失败:', error);
|
||
}
|
||
};
|
||
|
||
hydrate();
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [dispatch, activeFastingSchedule]);
|
||
|
||
useEffect(() => {
|
||
if (isLoggedIn) {
|
||
dispatch(fetchChallenges());
|
||
}
|
||
}, [isLoggedIn]);
|
||
|
||
// ==================== 基础服务初始化(不需要权限,总是执行)====================
|
||
React.useEffect(() => {
|
||
const initializeBasicServices = async () => {
|
||
try {
|
||
logger.info('🚀 开始初始化基础服务(不需要权限)...');
|
||
|
||
// 1. 加载用户数据(首屏展示需要)
|
||
await dispatch(fetchMyProfile());
|
||
logger.info('✅ 用户数据加载完成');
|
||
|
||
// 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化)
|
||
initializeHealthPermissions();
|
||
logger.info('✅ HealthKit 权限系统初始化完成');
|
||
|
||
// 3. 初始化快捷动作(用户可能立即使用)
|
||
await setupQuickActions();
|
||
logger.info('✅ 快捷动作初始化完成');
|
||
|
||
// 5. 初始化喝水记录 Bridge
|
||
initializeWaterRecordBridge();
|
||
logger.info('✅ 喝水记录 Bridge 初始化完成');
|
||
|
||
logger.info('🎉 基础服务初始化完成');
|
||
} catch (error) {
|
||
logger.error('❌ 基础服务初始化失败:', error);
|
||
}
|
||
};
|
||
|
||
initializeBasicServices();
|
||
}, [dispatch]);
|
||
|
||
// ==================== 应用状态监听 - 进入前台时清除角标 ====================
|
||
React.useEffect(() => {
|
||
const subscription = AppState.addEventListener('change', (nextAppState: AppStateStatus) => {
|
||
if (nextAppState === 'active') {
|
||
// 应用进入前台时清除角标
|
||
clearBadgeCount();
|
||
}
|
||
});
|
||
|
||
return () => {
|
||
subscription.remove();
|
||
};
|
||
}, []);
|
||
|
||
// ==================== 权限相关服务初始化(应用启动时执行)====================
|
||
React.useEffect(() => {
|
||
// 如果已经初始化过,则跳过(确保只初始化一次)
|
||
if (permissionInitializedRef.current) {
|
||
return;
|
||
}
|
||
|
||
permissionInitializedRef.current = true;
|
||
|
||
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
||
|
||
// ==================== 辅助函数 ====================
|
||
|
||
// 异步同步 Widget 数据(不阻塞主流程)
|
||
const syncWidgetDataInBackground = async () => {
|
||
try {
|
||
const widgetSync = await syncPendingWidgetChanges();
|
||
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
|
||
logger.info(`🔄 检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
|
||
|
||
// 异步处理每条记录
|
||
for (const record of widgetSync.pendingRecords) {
|
||
try {
|
||
await store.dispatch(createWaterRecordAction({
|
||
amount: record.amount,
|
||
recordedAt: record.recordedAt,
|
||
source: WaterRecordSource.Auto,
|
||
})).unwrap();
|
||
|
||
logger.info(`✅ 成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
|
||
} catch (error) {
|
||
logger.error('❌ 同步水记录失败:', error);
|
||
}
|
||
}
|
||
|
||
// 清除已同步的记录
|
||
await clearPendingWaterRecords();
|
||
logger.info('✅ 所有待同步的水记录已处理完成');
|
||
}
|
||
} catch (error) {
|
||
logger.error('❌ Widget 数据同步失败:', error);
|
||
}
|
||
};
|
||
|
||
// 批量注册所有通知提醒
|
||
const registerAllNotifications = async () => {
|
||
try {
|
||
logger.info('📢 开始批量注册通知提醒...');
|
||
|
||
// 并行注册所有通知,提高效率
|
||
await Promise.all([
|
||
// 营养提醒
|
||
NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() =>
|
||
logger.info('✅ 午餐提醒已注册')
|
||
),
|
||
NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() =>
|
||
logger.info('✅ 晚餐提醒已注册')
|
||
),
|
||
|
||
// 心情提醒
|
||
MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() =>
|
||
logger.info('✅ 心情提醒已注册')
|
||
),
|
||
|
||
// 喝水提醒
|
||
WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户').then(() =>
|
||
logger.info('✅ 喝水提醒已注册')
|
||
),
|
||
]);
|
||
|
||
// 检查断食通知(如果有活跃计划)
|
||
const fastingSchedule = store.getState().fasting.activeSchedule;
|
||
if (fastingSchedule) {
|
||
logger.info('✅ 检测到活跃的断食计划,将通过页面 hook 自动安排通知');
|
||
}
|
||
|
||
logger.info('🎉 所有通知提醒注册完成');
|
||
} catch (error) {
|
||
logger.error('❌ 通知提醒注册失败:', error);
|
||
}
|
||
};
|
||
|
||
// 初始化后台任务管理器
|
||
const initializeBackgroundTaskManager = async () => {
|
||
try {
|
||
logger.info('⚙️ 初始化后台任务管理器...');
|
||
|
||
await BackgroundTaskManager.getInstance().initialize();
|
||
logger.info('✅ 后台任务管理器初始化成功');
|
||
|
||
// 简单的任务调度检查
|
||
const taskManager = BackgroundTaskManager.getInstance();
|
||
const status = await taskManager.getStatus();
|
||
|
||
if (status === 'available') {
|
||
const pendingRequests = await taskManager.getPendingRequests();
|
||
if (pendingRequests.length === 0) {
|
||
await taskManager.scheduleNextTask();
|
||
logger.info('✅ 已调度新的后台任务');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error('❌ 后台任务管理器初始化失败:', error);
|
||
}
|
||
};
|
||
|
||
// 初始化健康监听服务(锻炼 + 睡眠)
|
||
const initializeHealthMonitoring = async () => {
|
||
try {
|
||
logger.info('💪 初始化健康监听服务...');
|
||
|
||
const [workoutResult, sleepResult, hrvResult] = await Promise.allSettled([
|
||
workoutMonitorService.initialize(),
|
||
sleepMonitorService.initialize(),
|
||
hrvMonitorService.initialize(),
|
||
]);
|
||
|
||
const workoutReady = workoutResult.status === 'fulfilled';
|
||
if (workoutReady) {
|
||
logger.info('✅ 锻炼监听服务初始化成功');
|
||
} else {
|
||
logger.error('❌ 锻炼监听服务初始化失败:', workoutResult.reason);
|
||
}
|
||
|
||
const sleepReady = sleepResult.status === 'fulfilled';
|
||
if (sleepReady) {
|
||
logger.info('✅ 睡眠监听服务初始化成功');
|
||
} else {
|
||
logger.error('❌ 睡眠监听服务初始化失败:', sleepResult.reason);
|
||
}
|
||
|
||
const hrvReady = hrvResult.status === 'fulfilled';
|
||
if (hrvReady) {
|
||
logger.info('✅ HRV 监听服务初始化成功');
|
||
} else {
|
||
logger.error('❌ HRV 监听服务初始化失败:', hrvResult.reason);
|
||
}
|
||
|
||
if (workoutReady && sleepReady && hrvReady) {
|
||
logger.info('🎉 健康监听服务初始化完成');
|
||
} else {
|
||
logger.warn('⚠️ 健康监听服务部分未能初始化成功,请检查上述错误日志');
|
||
}
|
||
|
||
return workoutReady && sleepReady && hrvReady;
|
||
} catch (error) {
|
||
logger.error('❌ 健康监听服务初始化失败:', error);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// 后台任务详细状态检查(空闲时执行)
|
||
const checkBackgroundTaskStatus = async () => {
|
||
try {
|
||
logger.info('🔍 检查后台任务详细状态...');
|
||
|
||
const taskManager = BackgroundTaskManager.getInstance();
|
||
const status = await taskManager.getStatus();
|
||
const statusText = await taskManager.checkStatus();
|
||
|
||
logger.info(`📊 后台任务状态: ${status} (${statusText})`);
|
||
|
||
// 检查上次执行时间
|
||
const lastCheckTime = await taskManager.getLastBackgroundCheckTime();
|
||
if (lastCheckTime) {
|
||
const timeSinceLastCheck = Date.now() - lastCheckTime;
|
||
const hoursSinceLastCheck = timeSinceLastCheck / (1000 * 60 * 60);
|
||
logger.info(`⏱️ 上次执行: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`);
|
||
|
||
if (hoursSinceLastCheck > 24) {
|
||
logger.warn('⚠️ 超过24小时未执行后台任务,请检查系统设置');
|
||
}
|
||
}
|
||
|
||
logger.info('✅ 后台任务状态检查完成');
|
||
} catch (error) {
|
||
logger.error('❌ 后台任务状态检查失败:', error);
|
||
}
|
||
};
|
||
|
||
// 权限服务初始化
|
||
const initializePermissionServices = async () => {
|
||
try {
|
||
logger.info('🔐 开始初始化需要权限的服务...');
|
||
|
||
// 1. 初始化通知服务(包含权限请求)
|
||
await notificationService.initialize();
|
||
logger.info('✅ 通知服务初始化完成');
|
||
|
||
// 2. 异步同步 Widget 数据(不阻塞主流程)
|
||
syncWidgetDataInBackground();
|
||
|
||
logger.info('🎉 权限相关服务初始化完成');
|
||
logger.info('💡 HealthKit 权限将在用户首次访问健康数据时请求');
|
||
} catch (error) {
|
||
logger.error('❌ 权限相关服务初始化失败:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// ==================== 后台服务初始化(延迟执行)====================
|
||
const initializeBackgroundServices = () => {
|
||
const { InteractionManager } = require('react-native');
|
||
|
||
InteractionManager.runAfterInteractions(() => {
|
||
setTimeout(async () => {
|
||
try {
|
||
logger.info('📅 开始初始化后台服务...');
|
||
|
||
// 1. 批量注册所有通知提醒
|
||
await registerAllNotifications();
|
||
|
||
// 2. 初始化后台任务管理器
|
||
await initializeBackgroundTaskManager();
|
||
|
||
// 3. 初始化健康监听服务
|
||
await initializeHealthMonitoring();
|
||
|
||
logger.info('🎉 后台服务初始化完成');
|
||
} catch (error) {
|
||
logger.error('❌ 后台服务初始化失败:', error);
|
||
}
|
||
}, 3000);
|
||
});
|
||
};
|
||
|
||
// ==================== 空闲服务初始化====================
|
||
const initializeIdleServices = () => {
|
||
setTimeout(async () => {
|
||
try {
|
||
logger.info('🔄 开始初始化空闲服务...');
|
||
|
||
// 1. 后台任务详细状态检查
|
||
await checkBackgroundTaskStatus();
|
||
|
||
// 2. 开发环境调试工具
|
||
if (__DEV__ && BackgroundTaskDebugger) {
|
||
BackgroundTaskDebugger.getInstance().initialize();
|
||
logger.info('✅ 后台任务调试工具已初始化(开发环境)');
|
||
}
|
||
|
||
logger.info('🎉 空闲服务初始化完成');
|
||
} catch (error) {
|
||
logger.error('❌ 空闲服务初始化失败:', error);
|
||
}
|
||
}, 8000);
|
||
};
|
||
|
||
const runInitializationSequence = async () => {
|
||
try {
|
||
await initializePermissionServices();
|
||
} catch {
|
||
logger.warn('⚠️ 权限相关服务初始化失败,将继续启动后台和空闲服务以便后续重试');
|
||
}
|
||
|
||
// 交互完成后执行后台服务
|
||
initializeBackgroundServices();
|
||
|
||
// 空闲时执行非关键服务
|
||
initializeIdleServices();
|
||
};
|
||
|
||
runInitializationSequence();
|
||
|
||
}, []); // 每次应用启动都执行,不依赖其他状态
|
||
|
||
React.useEffect(() => {
|
||
|
||
const getPrivacyAgreed = async () => {
|
||
const str = await AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed)
|
||
|
||
setShowPrivacyModal(str !== 'true');
|
||
}
|
||
getPrivacyAgreed();
|
||
}, []);
|
||
|
||
const handlePrivacyAgree = () => {
|
||
dispatch(setPrivacyAgreed());
|
||
setShowPrivacyModal(false);
|
||
};
|
||
|
||
const handlePrivacyDisagree = () => {
|
||
// RNExitApp.exitApp();
|
||
};
|
||
|
||
return (
|
||
<DialogProvider>
|
||
<MembershipModalProvider>
|
||
{children}
|
||
<PrivacyConsentModal
|
||
visible={showPrivacyModal}
|
||
onAgree={handlePrivacyAgree}
|
||
onDisagree={handlePrivacyDisagree}
|
||
/>
|
||
</MembershipModalProvider>
|
||
</DialogProvider>
|
||
);
|
||
}
|
||
|
||
export default function RootLayout() {
|
||
const [loaded] = useFonts({
|
||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||
});
|
||
|
||
if (!loaded) {
|
||
// Async font loading only occurs in development.
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||
<Provider store={store}>
|
||
<Bootstrapper>
|
||
<ToastProvider>
|
||
<ThemeProvider value={DefaultTheme}>
|
||
<Stack screenOptions={{ headerShown: false }}>
|
||
<Stack.Screen name="onboarding" />
|
||
<Stack.Screen name="(tabs)" />
|
||
<Stack.Screen name="challenge" options={{ headerShown: false }} />
|
||
<Stack.Screen name="training-plan" options={{ headerShown: false }} />
|
||
<Stack.Screen name="workout" options={{ headerShown: false }} />
|
||
<Stack.Screen name="profile/edit" />
|
||
<Stack.Screen name="fasting/[planId]" options={{ headerShown: false }} />
|
||
|
||
<Stack.Screen name="ai-posture-assessment" />
|
||
<Stack.Screen name="auth/login" options={{ headerShown: false }} />
|
||
<Stack.Screen name="legal/user-agreement" options={{ headerShown: true, title: '用户协议' }} />
|
||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||
<Stack.Screen name="water-detail" options={{ headerShown: false }} />
|
||
<Stack.Screen name="water-settings" options={{ headerShown: false }} />
|
||
<Stack.Screen name="workout/notification-settings" options={{ headerShown: false }} />
|
||
<Stack.Screen
|
||
name="health-data-permissions"
|
||
options={{ headerShown: false }}
|
||
/>
|
||
<Stack.Screen name="badges/index" options={{ headerShown: false }} />
|
||
<Stack.Screen name="+not-found" />
|
||
</Stack>
|
||
<StatusBar style="dark" />
|
||
</ThemeProvider>
|
||
</ToastProvider>
|
||
</Bootstrapper>
|
||
</Provider>
|
||
</GestureHandlerRootView>
|
||
);
|
||
}
|