feat: 集成推送通知功能及相关组件
- 在项目中引入expo-notifications库,支持本地推送通知功能 - 实现通知权限管理,用户可选择开启或关闭通知 - 新增通知发送、定时通知和重复通知功能 - 更新个人页面,集成通知开关和权限请求逻辑 - 编写推送通知功能实现文档,详细描述功能和使用方法 - 优化心情日历页面,确保数据实时刷新
This commit is contained in:
375
README-notifications.md
Normal file
375
README-notifications.md
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
# 推送通知功能使用指南
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
推送通知功能已经集成到项目中,无需额外安装依赖。
|
||||||
|
|
||||||
|
### 2. 基本使用
|
||||||
|
|
||||||
|
在组件中使用推送通知功能:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { sendNotification, sendWorkoutReminder } = useNotifications();
|
||||||
|
|
||||||
|
const handleSendNotification = async () => {
|
||||||
|
await sendNotification({
|
||||||
|
title: '测试通知',
|
||||||
|
body: '这是一个测试通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'high'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendWorkoutReminder = async () => {
|
||||||
|
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
// 你的组件内容
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用辅助函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { WorkoutNotificationHelpers, GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
|
||||||
|
// 发送运动开始提醒
|
||||||
|
await WorkoutNotificationHelpers.sendWorkoutStartReminder('张三');
|
||||||
|
|
||||||
|
// 发送目标达成通知
|
||||||
|
await GoalNotificationHelpers.sendGoalAchievementNotification('张三', '每周运动3次');
|
||||||
|
|
||||||
|
// 安排每日运动提醒
|
||||||
|
await WorkoutNotificationHelpers.scheduleDailyWorkoutReminder('张三', 9, 0); // 每天上午9点
|
||||||
|
```
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### ✅ 已实现功能
|
||||||
|
|
||||||
|
- [x] 立即发送通知
|
||||||
|
- [x] 定时发送通知
|
||||||
|
- [x] 重复发送通知
|
||||||
|
- [x] 通知权限管理
|
||||||
|
- [x] 通知点击处理
|
||||||
|
- [x] 通知取消功能
|
||||||
|
- [x] 多种通知类型支持
|
||||||
|
- [x] 通知模板系统
|
||||||
|
- [x] 批量通知发送
|
||||||
|
- [x] 通知状态查询
|
||||||
|
|
||||||
|
### 📱 支持的通知类型
|
||||||
|
|
||||||
|
1. **运动相关**
|
||||||
|
- 运动开始提醒
|
||||||
|
- 运动完成通知
|
||||||
|
- 每日运动提醒
|
||||||
|
|
||||||
|
2. **目标相关**
|
||||||
|
- 目标达成通知
|
||||||
|
- 目标进度更新
|
||||||
|
- 目标截止提醒
|
||||||
|
|
||||||
|
3. **心情相关**
|
||||||
|
- 心情打卡提醒
|
||||||
|
- 每日心情提醒
|
||||||
|
|
||||||
|
4. **营养相关**
|
||||||
|
- 营养记录提醒
|
||||||
|
- 三餐提醒
|
||||||
|
|
||||||
|
5. **通用通知**
|
||||||
|
- 欢迎通知
|
||||||
|
- 成就通知
|
||||||
|
- 系统维护通知
|
||||||
|
|
||||||
|
## 详细使用示例
|
||||||
|
|
||||||
|
### 1. 在运动页面中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { WorkoutNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
|
||||||
|
// 运动开始时
|
||||||
|
const handleWorkoutStart = async () => {
|
||||||
|
await WorkoutNotificationHelpers.sendWorkoutStartReminder(userName);
|
||||||
|
// 开始运动逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 运动完成时
|
||||||
|
const handleWorkoutComplete = async (duration: number) => {
|
||||||
|
await WorkoutNotificationHelpers.sendWorkoutCompleteNotification(userName, duration);
|
||||||
|
// 完成运动逻辑
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在目标页面中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { GoalNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
|
||||||
|
// 目标达成时
|
||||||
|
const handleGoalAchieved = async (goalName: string) => {
|
||||||
|
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, goalName);
|
||||||
|
// 目标达成逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 目标进度更新时
|
||||||
|
const handleGoalProgress = async (goalName: string, progress: number) => {
|
||||||
|
if (progress >= 50 && progress < 100) {
|
||||||
|
await GoalNotificationHelpers.sendGoalProgressNotification(userName, goalName, progress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在心情页面中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MoodNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
|
||||||
|
// 安排每日心情打卡提醒
|
||||||
|
const setupMoodReminder = async () => {
|
||||||
|
await MoodNotificationHelpers.scheduleDailyMoodReminder(userName, 20, 0); // 每天晚上8点
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 在营养页面中使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
|
||||||
|
// 安排营养记录提醒
|
||||||
|
const setupNutritionReminders = async () => {
|
||||||
|
await NutritionNotificationHelpers.scheduleNutritionReminders(userName);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通知模板使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NotificationTemplates } from '@/utils/notificationHelpers';
|
||||||
|
import { notificationService } from '@/services/notifications';
|
||||||
|
|
||||||
|
// 使用模板发送通知
|
||||||
|
const sendWorkoutNotification = async (userName: string) => {
|
||||||
|
const notification = NotificationTemplates.workout.start(userName);
|
||||||
|
await notificationService.sendImmediateNotification(notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendGoalNotification = async (userName: string, goalName: string) => {
|
||||||
|
const notification = NotificationTemplates.goal.achievement(userName, goalName);
|
||||||
|
await notificationService.sendImmediateNotification(notification);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 权限管理
|
||||||
|
|
||||||
|
### 检查权限状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const { permissionStatus, requestPermission } = useNotifications();
|
||||||
|
|
||||||
|
const handleRequestPermission = async () => {
|
||||||
|
const status = await requestPermission();
|
||||||
|
if (status === 'granted') {
|
||||||
|
console.log('通知权限已授予');
|
||||||
|
} else {
|
||||||
|
console.log('通知权限被拒绝');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>权限状态: {permissionStatus}</Text>
|
||||||
|
<Button title="请求权限" onPress={handleRequestPermission} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 处理权限被拒绝
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleNotificationToggle = async (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
const status = await requestPermission();
|
||||||
|
if (status === 'granted') {
|
||||||
|
setNotificationEnabled(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert(
|
||||||
|
'权限被拒绝',
|
||||||
|
'请在系统设置中开启通知权限以获得最佳体验',
|
||||||
|
[
|
||||||
|
{ text: '取消', style: 'cancel' },
|
||||||
|
{ text: '去设置', onPress: () => Linking.openSettings() }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '请求通知权限失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通知管理
|
||||||
|
|
||||||
|
### 取消特定通知
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { notificationService } from '@/services/notifications';
|
||||||
|
|
||||||
|
// 取消特定通知
|
||||||
|
await notificationService.cancelNotification(notificationId);
|
||||||
|
|
||||||
|
// 取消所有通知
|
||||||
|
await notificationService.cancelAllNotifications();
|
||||||
|
|
||||||
|
// 取消特定类型的通知
|
||||||
|
import { GeneralNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
|
await GeneralNotificationHelpers.cancelNotificationsByType('workout_reminder');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查询已安排的通知
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { notificationService } from '@/services/notifications';
|
||||||
|
|
||||||
|
// 获取所有已安排的通知
|
||||||
|
const notifications = await notificationService.getAllScheduledNotifications();
|
||||||
|
console.log('已安排的通知数量:', notifications.length);
|
||||||
|
|
||||||
|
notifications.forEach(notification => {
|
||||||
|
console.log('通知ID:', notification.identifier);
|
||||||
|
console.log('通知标题:', notification.content.title);
|
||||||
|
console.log('通知内容:', notification.content.body);
|
||||||
|
console.log('通知类型:', notification.content.data?.type);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试功能
|
||||||
|
|
||||||
|
### 使用测试组件
|
||||||
|
|
||||||
|
项目中包含了一个完整的测试组件 `NotificationTest.tsx`,可以用来测试所有通知功能:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NotificationTest } from '@/components/NotificationTest';
|
||||||
|
|
||||||
|
// 在开发环境中使用
|
||||||
|
function TestScreen() {
|
||||||
|
return <NotificationTest />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动测试
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 测试立即通知
|
||||||
|
await sendNotification({
|
||||||
|
title: '测试通知',
|
||||||
|
body: '这是一个测试通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'high'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试定时通知(5秒后)
|
||||||
|
const futureDate = new Date(Date.now() + 5000);
|
||||||
|
await scheduleNotification(
|
||||||
|
{
|
||||||
|
title: '定时测试',
|
||||||
|
body: '这是一个5秒后的定时通知',
|
||||||
|
sound: true
|
||||||
|
},
|
||||||
|
futureDate
|
||||||
|
);
|
||||||
|
|
||||||
|
// 测试重复通知(每分钟)
|
||||||
|
await scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '重复测试',
|
||||||
|
body: '这是一个每分钟重复的通知',
|
||||||
|
sound: true
|
||||||
|
},
|
||||||
|
{ minutes: 1 }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 通知内容设计
|
||||||
|
|
||||||
|
- **标题**: 简洁明了,不超过50个字符
|
||||||
|
- **内容**: 具体有用,不超过200个字符
|
||||||
|
- **时机**: 选择合适的发送时间
|
||||||
|
- **频率**: 避免过于频繁的通知
|
||||||
|
|
||||||
|
### 2. 用户体验
|
||||||
|
|
||||||
|
- 提供通知开关选项
|
||||||
|
- 允许用户自定义通知时间
|
||||||
|
- 支持通知类型的选择
|
||||||
|
- 提供通知历史记录
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
|
||||||
|
- 始终处理权限请求失败
|
||||||
|
- 提供用户友好的错误提示
|
||||||
|
- 实现降级处理方案
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
|
||||||
|
- 避免同时发送大量通知
|
||||||
|
- 及时清理不需要的通知
|
||||||
|
- 合理使用重复通知
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **通知不显示**
|
||||||
|
- 检查权限是否已授予
|
||||||
|
- 确认应用是否在前台
|
||||||
|
- 验证通知配置是否正确
|
||||||
|
|
||||||
|
2. **定时通知不触发**
|
||||||
|
- 检查设备是否重启
|
||||||
|
- 确认应用是否被系统杀死
|
||||||
|
- 验证时间设置是否正确
|
||||||
|
|
||||||
|
3. **权限被拒绝**
|
||||||
|
- 引导用户到系统设置
|
||||||
|
- 提供权限说明
|
||||||
|
- 实现降级处理方案
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 启用详细日志
|
||||||
|
console.log('通知权限状态:', await notificationService.getPermissionStatus());
|
||||||
|
console.log('已安排通知:', await notificationService.getAllScheduledNotifications());
|
||||||
|
|
||||||
|
// 测试通知发送
|
||||||
|
await notificationService.sendImmediateNotification({
|
||||||
|
title: '调试通知',
|
||||||
|
body: '这是一个调试通知',
|
||||||
|
sound: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
推送通知功能已经完整集成到项目中,提供了丰富的功能和良好的用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
|
||||||
|
|
||||||
|
如有任何问题或建议,请参考 `docs/notification-implementation.md` 文档或联系开发团队。
|
||||||
18
app.json
18
app.json
@@ -16,7 +16,8 @@
|
|||||||
"ITSAppUsesNonExemptEncryption": false,
|
"ITSAppUsesNonExemptEncryption": false,
|
||||||
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。"
|
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
|
||||||
|
"UIBackgroundModes": ["remote-notification"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
@@ -25,7 +26,12 @@
|
|||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"package": "com.anonymous.digitalpilates"
|
"package": "com.anonymous.digitalpilates",
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.RECEIVE_BOOT_COMPLETED",
|
||||||
|
"android.permission.VIBRATE",
|
||||||
|
"android.permission.WAKE_LOCK"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
@@ -50,6 +56,14 @@
|
|||||||
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。",
|
"healthSharePermission": "应用需要访问您的健康数据(步数与能量消耗)以展示运动统计。",
|
||||||
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"expo-notifications",
|
||||||
|
{
|
||||||
|
"icon": "./assets/images/Sealife.jpeg",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"sounds": ["./assets/sounds/notification.wav"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
||||||
import { clearErrors as clearTaskErrors, completeTask, fetchTasks, loadMoreTasks, skipTask } from '@/store/tasksSlice';
|
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
||||||
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
@@ -122,40 +122,7 @@ export default function GoalsScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 任务点击处理
|
|
||||||
const handleTaskPress = (task: TaskListItem) => {
|
|
||||||
console.log('Task pressed:', task.title);
|
|
||||||
// 这里可以导航到任务详情页面
|
|
||||||
};
|
|
||||||
|
|
||||||
// 完成任务处理
|
|
||||||
const handleCompleteTask = async (task: TaskListItem) => {
|
|
||||||
try {
|
|
||||||
await dispatch(completeTask({
|
|
||||||
taskId: task.id,
|
|
||||||
completionData: {
|
|
||||||
count: 1,
|
|
||||||
notes: '通过任务卡片完成'
|
|
||||||
}
|
|
||||||
})).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '完成任务失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳过任务处理
|
|
||||||
const handleSkipTask = async (task: TaskListItem) => {
|
|
||||||
try {
|
|
||||||
await dispatch(skipTask({
|
|
||||||
taskId: task.id,
|
|
||||||
skipData: {
|
|
||||||
reason: '用户主动跳过'
|
|
||||||
}
|
|
||||||
})).unwrap();
|
|
||||||
} catch (error) {
|
|
||||||
Alert.alert('错误', '跳过任务失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 导航到目标管理页面
|
// 导航到目标管理页面
|
||||||
const handleNavigateToGoals = () => {
|
const handleNavigateToGoals = () => {
|
||||||
@@ -190,9 +157,6 @@ export default function GoalsScreen() {
|
|||||||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||||||
<TaskCard
|
<TaskCard
|
||||||
task={item}
|
task={item}
|
||||||
onPress={handleTaskPress}
|
|
||||||
onComplete={handleCompleteTask}
|
|
||||||
onSkip={handleSkipTask}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { getTabBarBottomPadding } from '@/constants/TabBar';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, Image, Linking, SafeAreaView, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
|
const DEFAULT_AVATAR_URL = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/avatarGirl01.jpeg';
|
||||||
@@ -21,7 +22,16 @@ export default function PersonalScreen() {
|
|||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const [notificationEnabled, setNotificationEnabled] = useState(true);
|
|
||||||
|
// 推送通知相关
|
||||||
|
const {
|
||||||
|
isInitialized,
|
||||||
|
permissionStatus,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification,
|
||||||
|
} = useNotifications();
|
||||||
|
|
||||||
|
const [notificationEnabled, setNotificationEnabled] = useState(false);
|
||||||
|
|
||||||
// 计算底部间距
|
// 计算底部间距
|
||||||
const bottomPadding = useMemo(() => {
|
const bottomPadding = useMemo(() => {
|
||||||
@@ -64,6 +74,41 @@ export default function PersonalScreen() {
|
|||||||
// 显示名称
|
// 显示名称
|
||||||
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
const displayName = (userProfile.name?.trim()) ? userProfile.name : DEFAULT_MEMBER_NAME;
|
||||||
|
|
||||||
|
// 监听通知权限状态变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (permissionStatus === 'granted') {
|
||||||
|
setNotificationEnabled(true);
|
||||||
|
} else {
|
||||||
|
setNotificationEnabled(false);
|
||||||
|
}
|
||||||
|
}, [permissionStatus]);
|
||||||
|
|
||||||
|
// 处理通知开关变化
|
||||||
|
const handleNotificationToggle = async (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
const status = await requestPermission();
|
||||||
|
if (status === 'granted') {
|
||||||
|
setNotificationEnabled(true);
|
||||||
|
// 发送测试通知
|
||||||
|
await sendNotification({
|
||||||
|
title: '通知已开启',
|
||||||
|
body: '您将收到运动提醒和重要通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Alert.alert('权限被拒绝', '请在系统设置中开启通知权限');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '请求通知权限失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setNotificationEnabled(false);
|
||||||
|
Alert.alert('通知已关闭', '您将不会收到推送通知');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 用户信息头部
|
// 用户信息头部
|
||||||
const UserHeader = () => (
|
const UserHeader = () => (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
@@ -137,14 +182,8 @@ export default function PersonalScreen() {
|
|||||||
</View>
|
</View>
|
||||||
{item.type === 'switch' ? (
|
{item.type === 'switch' ? (
|
||||||
<Switch
|
<Switch
|
||||||
value={isLoggedIn ? notificationEnabled : false}
|
value={item.switchValue || false}
|
||||||
onValueChange={(value) => {
|
onValueChange={item.onSwitchChange || (() => {})}
|
||||||
if (!isLoggedIn) {
|
|
||||||
pushIfAuthedElseLogin('/profile/notification-settings');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setNotificationEnabled(value);
|
|
||||||
}}
|
|
||||||
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
|
||||||
thumbColor="#FFFFFF"
|
thumbColor="#FFFFFF"
|
||||||
style={styles.switch}
|
style={styles.switch}
|
||||||
@@ -177,6 +216,8 @@ export default function PersonalScreen() {
|
|||||||
icon: 'notifications-outline' as const,
|
icon: 'notifications-outline' as const,
|
||||||
title: '消息推送',
|
title: '消息推送',
|
||||||
type: 'switch' as const,
|
type: 'switch' as const,
|
||||||
|
switchValue: notificationEnabled,
|
||||||
|
onSwitchChange: handleNotificationToggle,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useMoodData } from '@/hooks/useMoodData';
|
import { useMoodData } from '@/hooks/useMoodData';
|
||||||
import { getMoodOptions } from '@/services/moodCheckins';
|
import { getMoodOptions } from '@/services/moodCheckins';
|
||||||
|
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
@@ -51,6 +53,7 @@ export default function MoodCalendarScreen() {
|
|||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const params = useLocalSearchParams();
|
const params = useLocalSearchParams();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
|
const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
|
||||||
|
|
||||||
const { selectedDate } = params;
|
const { selectedDate } = params;
|
||||||
@@ -58,8 +61,16 @@ export default function MoodCalendarScreen() {
|
|||||||
|
|
||||||
const [currentMonth, setCurrentMonth] = useState(initialDate);
|
const [currentMonth, setCurrentMonth] = useState(initialDate);
|
||||||
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
const [selectedDay, setSelectedDay] = useState<number | null>(null);
|
||||||
const [selectedDateMood, setSelectedDateMood] = useState<any>(null);
|
|
||||||
const [moodRecords, setMoodRecords] = useState<Record<string, any[]>>({});
|
// 使用 Redux store 中的数据
|
||||||
|
const moodRecords = useAppSelector(state => state.mood.moodRecords);
|
||||||
|
|
||||||
|
// 获取选中日期的数据
|
||||||
|
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null;
|
||||||
|
const selectedDateMood = useAppSelector(state => {
|
||||||
|
if (!selectedDateString) return null;
|
||||||
|
return selectLatestMoodRecordByDate(selectedDateString)(state);
|
||||||
|
});
|
||||||
|
|
||||||
const moodOptions = getMoodOptions();
|
const moodOptions = getMoodOptions();
|
||||||
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||||
@@ -68,6 +79,31 @@ export default function MoodCalendarScreen() {
|
|||||||
// 生成当前月份的日历数据
|
// 生成当前月份的日历数据
|
||||||
const { calendar, today, month, year } = generateCalendarData(currentMonth);
|
const { calendar, today, month, year } = generateCalendarData(currentMonth);
|
||||||
|
|
||||||
|
// 加载整个月份的心情数据
|
||||||
|
const loadMonthMoodData = async (targetMonth: Date) => {
|
||||||
|
try {
|
||||||
|
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
||||||
|
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
|
||||||
|
|
||||||
|
// 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中
|
||||||
|
// 不需要额外的处理
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载月份心情数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载选中日期的心情记录
|
||||||
|
const loadDailyMoodCheckins = async (dateString: string) => {
|
||||||
|
try {
|
||||||
|
await fetchMoodRecords(dateString);
|
||||||
|
// 不需要手动设置 selectedDateMood,因为它现在从 Redux store 中获取
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载心情记录失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化选中日期
|
// 初始化选中日期
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
@@ -86,44 +122,22 @@ export default function MoodCalendarScreen() {
|
|||||||
loadMonthMoodData(currentMonth);
|
loadMonthMoodData(currentMonth);
|
||||||
}, [selectedDate]);
|
}, [selectedDate]);
|
||||||
|
|
||||||
// 加载整个月份的心情数据
|
// 监听页面焦点变化,当从编辑页面返回时刷新数据
|
||||||
const loadMonthMoodData = async (targetMonth: Date) => {
|
useFocusEffect(
|
||||||
try {
|
useCallback(() => {
|
||||||
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
// 当页面获得焦点时,刷新当前月份的数据和选中日期的数据
|
||||||
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
const refreshData = async () => {
|
||||||
|
if (selectedDay) {
|
||||||
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
|
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
|
||||||
|
await fetchMoodRecords(selectedDateString);
|
||||||
// 将历史记录按日期分组
|
|
||||||
const monthData: Record<string, any[]> = {};
|
|
||||||
historyData.forEach(checkin => {
|
|
||||||
const date = checkin.checkinDate;
|
|
||||||
if (!monthData[date]) {
|
|
||||||
monthData[date] = [];
|
|
||||||
}
|
}
|
||||||
monthData[date].push(checkin);
|
const startDate = dayjs(currentMonth).startOf('month').format('YYYY-MM-DD');
|
||||||
});
|
const endDate = dayjs(currentMonth).endOf('month').format('YYYY-MM-DD');
|
||||||
|
await fetchMoodHistoryRecords({ startDate, endDate });
|
||||||
setMoodRecords(monthData);
|
};
|
||||||
} catch (error) {
|
refreshData();
|
||||||
console.error('加载月份心情数据失败:', error);
|
}, [currentMonth, selectedDay, fetchMoodRecords, fetchMoodHistoryRecords])
|
||||||
}
|
);
|
||||||
};
|
|
||||||
|
|
||||||
// 加载选中日期的心情记录
|
|
||||||
const loadDailyMoodCheckins = async (dateString: string) => {
|
|
||||||
try {
|
|
||||||
const checkins = await fetchMoodRecords(dateString);
|
|
||||||
if (checkins.length > 0) {
|
|
||||||
setSelectedDateMood(checkins[0]); // 取最新的记录
|
|
||||||
} else {
|
|
||||||
setSelectedDateMood(null);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载心情记录失败:', error);
|
|
||||||
setSelectedDateMood(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 月份切换函数
|
// 月份切换函数
|
||||||
const goToPreviousMonth = () => {
|
const goToPreviousMonth = () => {
|
||||||
@@ -166,7 +180,7 @@ export default function MoodCalendarScreen() {
|
|||||||
const renderMoodIcon = (day: number | null, isSelected: boolean) => {
|
const renderMoodIcon = (day: number | null, isSelected: boolean) => {
|
||||||
if (!day) return null;
|
if (!day) return null;
|
||||||
|
|
||||||
// 检查该日期是否有心情记录
|
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
|
||||||
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
|
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
|
||||||
const dayRecords = moodRecords[dayDateString] || [];
|
const dayRecords = moodRecords[dayDateString] || [];
|
||||||
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
|
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
|
||||||
|
|||||||
302
components/NotificationTest.tsx
Normal file
302
components/NotificationTest.tsx
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
StyleSheet,
|
||||||
|
Alert,
|
||||||
|
ScrollView,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useNotifications } from '../hooks/useNotifications';
|
||||||
|
import { ThemedText } from './ThemedText';
|
||||||
|
import { ThemedView } from './ThemedView';
|
||||||
|
|
||||||
|
export const NotificationTest: React.FC = () => {
|
||||||
|
const {
|
||||||
|
isInitialized,
|
||||||
|
permissionStatus,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification,
|
||||||
|
scheduleNotification,
|
||||||
|
scheduleRepeatingNotification,
|
||||||
|
cancelAllNotifications,
|
||||||
|
getAllScheduledNotifications,
|
||||||
|
sendWorkoutReminder,
|
||||||
|
sendGoalAchievement,
|
||||||
|
sendMoodCheckinReminder,
|
||||||
|
} = useNotifications();
|
||||||
|
|
||||||
|
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const handleRequestPermission = async () => {
|
||||||
|
try {
|
||||||
|
const status = await requestPermission();
|
||||||
|
Alert.alert('权限状态', `通知权限: ${status}`);
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '请求权限失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendImmediateNotification = async () => {
|
||||||
|
try {
|
||||||
|
await sendNotification({
|
||||||
|
title: '测试通知',
|
||||||
|
body: '这是一个立即发送的测试通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
Alert.alert('成功', '通知已发送');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '发送通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScheduleNotification = async () => {
|
||||||
|
try {
|
||||||
|
const futureDate = new Date(Date.now() + 5000); // 5秒后
|
||||||
|
await scheduleNotification(
|
||||||
|
{
|
||||||
|
title: '定时通知',
|
||||||
|
body: '这是一个5秒后发送的定时通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
},
|
||||||
|
futureDate
|
||||||
|
);
|
||||||
|
Alert.alert('成功', '定时通知已安排');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '安排定时通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScheduleRepeatingNotification = async () => {
|
||||||
|
try {
|
||||||
|
await scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '重复通知',
|
||||||
|
body: '这是一个每分钟重复的通知',
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
},
|
||||||
|
{ minutes: 1 }
|
||||||
|
);
|
||||||
|
Alert.alert('成功', '重复通知已安排');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '安排重复通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendWorkoutReminder = async () => {
|
||||||
|
try {
|
||||||
|
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
|
||||||
|
Alert.alert('成功', '运动提醒已发送');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '发送运动提醒失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendGoalAchievement = async () => {
|
||||||
|
try {
|
||||||
|
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
||||||
|
Alert.alert('成功', '目标达成通知已发送');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '发送目标达成通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMoodCheckinReminder = async () => {
|
||||||
|
try {
|
||||||
|
await sendMoodCheckinReminder('心情打卡', '记得记录今天的心情状态哦');
|
||||||
|
Alert.alert('成功', '心情打卡提醒已发送');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '发送心情打卡提醒失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAllNotifications = async () => {
|
||||||
|
try {
|
||||||
|
await cancelAllNotifications();
|
||||||
|
Alert.alert('成功', '所有通知已取消');
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '取消通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetScheduledNotifications = async () => {
|
||||||
|
try {
|
||||||
|
const notifications = await getAllScheduledNotifications();
|
||||||
|
setScheduledNotifications(notifications);
|
||||||
|
Alert.alert('成功', `找到 ${notifications.length} 个已安排的通知`);
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '获取已安排通知失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemedView style={styles.container}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false}>
|
||||||
|
<ThemedText style={styles.title}>推送通知测试</ThemedText>
|
||||||
|
|
||||||
|
<View style={styles.statusContainer}>
|
||||||
|
<ThemedText style={styles.statusText}>
|
||||||
|
初始化状态: {isInitialized ? '已初始化' : '未初始化'}
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={styles.statusText}>
|
||||||
|
权限状态: {permissionStatus || '未知'}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.buttonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleRequestPermission}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>请求通知权限</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleSendImmediateNotification}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>发送立即通知</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleScheduleNotification}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>安排定时通知(5秒后)</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleScheduleRepeatingNotification}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>安排重复通知(每分钟)</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleSendWorkoutReminder}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>发送运动提醒</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleSendGoalAchievement}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>发送目标达成通知</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleSendMoodCheckinReminder}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>发送心情打卡提醒</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.button}
|
||||||
|
onPress={handleGetScheduledNotifications}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.dangerButton]}
|
||||||
|
onPress={handleCancelAllNotifications}
|
||||||
|
>
|
||||||
|
<ThemedText style={styles.buttonText}>取消所有通知</ThemedText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{scheduledNotifications.length > 0 && (
|
||||||
|
<View style={styles.notificationsContainer}>
|
||||||
|
<ThemedText style={styles.sectionTitle}>已安排的通知:</ThemedText>
|
||||||
|
{scheduledNotifications.map((notification, index) => (
|
||||||
|
<View key={index} style={styles.notificationItem}>
|
||||||
|
<ThemedText style={styles.notificationTitle}>
|
||||||
|
{notification.content.title}
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={styles.notificationBody}>
|
||||||
|
{notification.content.body}
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={styles.notificationId}>
|
||||||
|
ID: {notification.identifier}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</ThemedView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
statusContainer: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
gap: 10,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: '#007AFF',
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
dangerButton: {
|
||||||
|
backgroundColor: '#FF3B30',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
notificationsContainer: {
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
notificationItem: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
padding: 15,
|
||||||
|
borderRadius: 10,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
notificationTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
notificationBody: {
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
notificationId: {
|
||||||
|
fontSize: 12,
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,40 +1,25 @@
|
|||||||
|
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { useAppDispatch } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { completeTask, skipTask } from '@/store/tasksSlice';
|
||||||
import { TaskListItem } from '@/types/goals';
|
import { TaskListItem } from '@/types/goals';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { Alert, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: TaskListItem;
|
task: TaskListItem;
|
||||||
onPress?: (task: TaskListItem) => void;
|
|
||||||
onComplete?: (task: TaskListItem) => void;
|
|
||||||
onSkip?: (task: TaskListItem) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TaskCard: React.FC<TaskCardProps> = ({
|
export const TaskCard: React.FC<TaskCardProps> = ({
|
||||||
task,
|
task,
|
||||||
onPress,
|
|
||||||
onComplete,
|
|
||||||
onSkip,
|
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useColorScheme() ?? 'light';
|
const theme = useColorScheme() ?? 'light';
|
||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const { showConfirm } = useGlobalDialog();
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return '#10B981';
|
|
||||||
case 'in_progress':
|
|
||||||
return '#F59E0B';
|
|
||||||
case 'overdue':
|
|
||||||
return '#EF4444';
|
|
||||||
case 'skipped':
|
|
||||||
return '#6B7280';
|
|
||||||
default:
|
|
||||||
return '#3B82F6';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -77,8 +62,6 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const month = date.toLocaleDateString('zh-CN', { month: 'short' });
|
const month = date.toLocaleDateString('zh-CN', { month: 'short' });
|
||||||
@@ -86,25 +69,105 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
return `${day} ${month}`;
|
return `${day} ${month}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCompleteTask = async () => {
|
||||||
|
// 如果任务已经完成,不执行任何操作
|
||||||
|
if (task.status === 'completed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用完成任务 API
|
||||||
|
await dispatch(completeTask({
|
||||||
|
taskId: task.id,
|
||||||
|
completionData: {
|
||||||
|
count: 1,
|
||||||
|
notes: '通过任务卡片完成'
|
||||||
|
}
|
||||||
|
})).unwrap();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '完成任务失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkipTask = async () => {
|
||||||
|
// 如果任务已经完成或已跳过,不执行任何操作
|
||||||
|
if (task.status === 'completed' || task.status === 'skipped') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示确认弹窗
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '确认跳过任务',
|
||||||
|
message: `确定要跳过任务"${task.title}"吗?跳过后将无法恢复。`,
|
||||||
|
confirmText: '跳过',
|
||||||
|
cancelText: '取消',
|
||||||
|
destructive: true,
|
||||||
|
icon: 'warning',
|
||||||
|
iconColor: '#F59E0B',
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// 调用跳过任务 API
|
||||||
|
await dispatch(skipTask({
|
||||||
|
taskId: task.id,
|
||||||
|
skipData: {
|
||||||
|
reason: '用户主动跳过'
|
||||||
|
}
|
||||||
|
})).unwrap();
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('错误', '跳过任务失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderActionIcons = () => {
|
||||||
|
if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.actionIconsContainer}>
|
||||||
|
{/* 完成任务图标 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.iconContainer}
|
||||||
|
onPress={handleCompleteTask}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/task/iconTaskHeader.png')}
|
||||||
|
style={styles.taskIcon}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 跳过任务图标 - 仅对进行中的任务显示 */}
|
||||||
|
{task.status === 'pending' && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.skipIconContainer}
|
||||||
|
onPress={handleSkipTask}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="skip-next" size={20} color="#6B7280" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.container, { backgroundColor: colorTokens.background }]}
|
style={[styles.container, { backgroundColor: colorTokens.background }]}
|
||||||
onPress={() => onPress?.(task)}
|
onPress={() => {}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
{/* 头部区域 */}
|
{/* 头部区域 */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/task/iconTaskHeader.png')}
|
|
||||||
style={styles.taskIcon}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
|
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
|
||||||
{task.title}
|
{task.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
{renderActionIcons()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -184,6 +247,18 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
lineHeight: 22,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
actionIconsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -191,18 +266,21 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#7A5AF8',
|
backgroundColor: '#7A5AF8',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flexShrink: 0,
|
},
|
||||||
|
skipIconContainer: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
},
|
},
|
||||||
taskIcon: {
|
taskIcon: {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
},
|
},
|
||||||
title: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '600',
|
|
||||||
lineHeight: 22,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
tagsContainer: {
|
tagsContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<HealthDataCard
|
<HealthDataCard
|
||||||
title="血氧饱和度"
|
title="血氧饱和度"
|
||||||
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'}
|
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? (oxygenSaturation * 100).toFixed(1) : '--'}
|
||||||
unit="%"
|
unit="%"
|
||||||
icon={oxygenIcon}
|
icon={oxygenIcon}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
155
docs/mood-calendar-refresh-fix.md
Normal file
155
docs/mood-calendar-refresh-fix.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# 心情日历数据实时刷新修复方案
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
用户编辑或新建心情后,当天的心情数据没有实时更新,必须退出页面或切换日期再切换回来才会生效。
|
||||||
|
|
||||||
|
## 问题原因
|
||||||
|
|
||||||
|
1. **本地状态与 Redux 状态不同步**:心情日历页面使用本地状态 `moodRecords` 和 `selectedDateMood` 来管理数据,而不是直接使用 Redux store 中的数据。
|
||||||
|
|
||||||
|
2. **缺少页面焦点监听**:当用户从编辑页面返回时,没有监听页面焦点变化来刷新数据。
|
||||||
|
|
||||||
|
3. **数据流不一致**:编辑页面通过 Redux 更新数据,但日历页面没有监听这些变化。
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### 1. 迁移到 Redux 状态管理
|
||||||
|
|
||||||
|
**修改文件**: `app/mood/calendar.tsx`
|
||||||
|
|
||||||
|
#### 移除本地状态
|
||||||
|
```typescript
|
||||||
|
// 移除这些本地状态
|
||||||
|
const [selectedDateMood, setSelectedDateMood] = useState<any>(null);
|
||||||
|
const [moodRecords, setMoodRecords] = useState<Record<string, any[]>>({});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用 Redux 状态
|
||||||
|
```typescript
|
||||||
|
// 使用 Redux store 中的数据
|
||||||
|
const moodRecords = useAppSelector(state => state.mood.moodRecords);
|
||||||
|
|
||||||
|
// 获取选中日期的数据
|
||||||
|
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null;
|
||||||
|
const selectedDateMood = selectedDateString ? useAppSelector(selectLatestMoodRecordByDate(selectedDateString)) : null;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 添加页面焦点监听
|
||||||
|
|
||||||
|
#### 导入 useFocusEffect
|
||||||
|
```typescript
|
||||||
|
import { router, useLocalSearchParams, useFocusEffect } from 'expo-router';
|
||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 添加焦点监听器
|
||||||
|
```typescript
|
||||||
|
// 监听页面焦点变化,当从编辑页面返回时刷新数据
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
// 当页面获得焦点时,刷新当前月份的数据和选中日期的数据
|
||||||
|
const refreshData = async () => {
|
||||||
|
if (selectedDay) {
|
||||||
|
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
|
||||||
|
await loadDailyMoodCheckins(selectedDateString);
|
||||||
|
}
|
||||||
|
await loadMonthMoodData(currentMonth);
|
||||||
|
};
|
||||||
|
refreshData();
|
||||||
|
}, [currentMonth, selectedDay, loadDailyMoodCheckins, loadMonthMoodData])
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 简化数据加载逻辑
|
||||||
|
|
||||||
|
#### 移除手动状态更新
|
||||||
|
```typescript
|
||||||
|
// 加载选中日期的心情记录
|
||||||
|
const loadDailyMoodCheckins = async (dateString: string) => {
|
||||||
|
try {
|
||||||
|
await fetchMoodRecords(dateString);
|
||||||
|
// 不需要手动设置 selectedDateMood,因为它现在从 Redux store 中获取
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载心情记录失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 简化月份数据加载
|
||||||
|
```typescript
|
||||||
|
// 加载整个月份的心情数据
|
||||||
|
const loadMonthMoodData = async (targetMonth: Date) => {
|
||||||
|
try {
|
||||||
|
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
||||||
|
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
|
||||||
|
|
||||||
|
// 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中
|
||||||
|
// 不需要额外的处理
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载月份心情数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 更新心情图标渲染
|
||||||
|
|
||||||
|
#### 使用 Redux 数据渲染图标
|
||||||
|
```typescript
|
||||||
|
const renderMoodIcon = (day: number | null, isSelected: boolean) => {
|
||||||
|
if (!day) return null;
|
||||||
|
|
||||||
|
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
|
||||||
|
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
|
||||||
|
const dayRecords = moodRecords[dayDateString] || [];
|
||||||
|
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
|
||||||
|
|
||||||
|
if (moodRecord) {
|
||||||
|
const mood = moodOptions.find(m => m.type === moodRecord.moodType);
|
||||||
|
return (
|
||||||
|
<View style={[styles.moodIconContainer, { backgroundColor: mood?.color }]}>
|
||||||
|
<View style={styles.moodIcon}>
|
||||||
|
<Text style={styles.moodEmoji}>{mood?.emoji || '😊'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.defaultMoodIcon}>
|
||||||
|
<Text style={styles.defaultMoodEmoji}>😊</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 修复效果
|
||||||
|
|
||||||
|
### 修复前
|
||||||
|
- 用户编辑心情后,日历页面数据不更新
|
||||||
|
- 需要退出页面或切换日期才能看到变化
|
||||||
|
- 用户体验不佳
|
||||||
|
|
||||||
|
### 修复后
|
||||||
|
- 用户编辑心情后,日历页面数据立即更新
|
||||||
|
- 页面焦点变化时自动刷新数据
|
||||||
|
- 数据状态统一管理,避免不一致
|
||||||
|
|
||||||
|
## 技术要点
|
||||||
|
|
||||||
|
1. **统一状态管理**:所有心情数据都通过 Redux 管理,确保数据一致性。
|
||||||
|
|
||||||
|
2. **响应式更新**:使用 `useAppSelector` 监听 Redux 状态变化,自动更新 UI。
|
||||||
|
|
||||||
|
3. **页面焦点监听**:使用 `useFocusEffect` 监听页面焦点变化,确保从其他页面返回时数据是最新的。
|
||||||
|
|
||||||
|
4. **异步数据加载**:保持异步数据加载的性能优势,同时确保数据同步。
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `app/mood/calendar.tsx` - 心情日历页面
|
||||||
|
- `app/mood/edit.tsx` - 心情编辑页面
|
||||||
|
- `store/moodSlice.ts` - 心情 Redux slice
|
||||||
|
- `hooks/useMoodData.ts` - 心情数据管理 hook
|
||||||
304
docs/notification-implementation.md
Normal file
304
docs/notification-implementation.md
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
# 推送通知功能实现文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本项目已成功集成本地推送通知功能,使用 Expo 官方的 `expo-notifications` 库。该功能支持立即通知、定时通知、重复通知等多种类型,并提供了完整的权限管理和通知处理机制。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **expo-notifications**: Expo 官方推送通知库
|
||||||
|
- **React Native**: 跨平台移动应用框架
|
||||||
|
- **TypeScript**: 类型安全的 JavaScript 超集
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
services/
|
||||||
|
├── notifications.ts # 推送通知服务核心逻辑
|
||||||
|
hooks/
|
||||||
|
├── useNotifications.ts # 推送通知自定义 Hook
|
||||||
|
components/
|
||||||
|
├── NotificationTest.tsx # 通知功能测试组件
|
||||||
|
app/(tabs)/
|
||||||
|
├── personal.tsx # 个人页面(集成通知开关)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 通知服务 (services/notifications.ts)
|
||||||
|
|
||||||
|
#### 主要特性
|
||||||
|
- **单例模式**: 确保全局只有一个通知服务实例
|
||||||
|
- **权限管理**: 自动请求和管理通知权限
|
||||||
|
- **多种通知类型**: 支持立即、定时、重复通知
|
||||||
|
- **通知监听**: 处理通知接收和点击事件
|
||||||
|
- **便捷方法**: 提供常用通知类型的快捷发送方法
|
||||||
|
|
||||||
|
#### 核心方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 初始化通知服务
|
||||||
|
await notificationService.initialize();
|
||||||
|
|
||||||
|
// 发送立即通知
|
||||||
|
await notificationService.sendImmediateNotification({
|
||||||
|
title: '标题',
|
||||||
|
body: '内容',
|
||||||
|
sound: true,
|
||||||
|
priority: 'high'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 安排定时通知
|
||||||
|
await notificationService.scheduleNotificationAtDate(
|
||||||
|
notification,
|
||||||
|
new Date(Date.now() + 5000) // 5秒后
|
||||||
|
);
|
||||||
|
|
||||||
|
// 安排重复通知
|
||||||
|
await notificationService.scheduleRepeatingNotification(
|
||||||
|
notification,
|
||||||
|
{ minutes: 1 } // 每分钟重复
|
||||||
|
);
|
||||||
|
|
||||||
|
// 取消通知
|
||||||
|
await notificationService.cancelNotification(notificationId);
|
||||||
|
await notificationService.cancelAllNotifications();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 自定义 Hook (hooks/useNotifications.ts)
|
||||||
|
|
||||||
|
#### 主要特性
|
||||||
|
- **状态管理**: 管理通知权限和初始化状态
|
||||||
|
- **自动初始化**: 组件挂载时自动初始化通知服务
|
||||||
|
- **便捷接口**: 提供简化的通知操作方法
|
||||||
|
- **类型安全**: 完整的 TypeScript 类型定义
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const {
|
||||||
|
isInitialized,
|
||||||
|
permissionStatus,
|
||||||
|
sendNotification,
|
||||||
|
scheduleNotification,
|
||||||
|
sendWorkoutReminder,
|
||||||
|
sendGoalAchievement,
|
||||||
|
} = useNotifications();
|
||||||
|
|
||||||
|
// 发送运动提醒
|
||||||
|
await sendWorkoutReminder('运动提醒', '该开始今天的普拉提训练了!');
|
||||||
|
|
||||||
|
// 发送目标达成通知
|
||||||
|
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试组件 (components/NotificationTest.tsx)
|
||||||
|
|
||||||
|
#### 功能特性
|
||||||
|
- **完整测试**: 测试所有通知功能
|
||||||
|
- **状态显示**: 显示初始化状态和权限状态
|
||||||
|
- **交互测试**: 提供各种通知类型的测试按钮
|
||||||
|
- **通知列表**: 显示已安排的通知列表
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### app.json 配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"expo-notifications",
|
||||||
|
{
|
||||||
|
"icon": "./assets/images/Sealife.jpeg",
|
||||||
|
"color": "#ffffff",
|
||||||
|
"sounds": ["./assets/sounds/notification.wav"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"ios": {
|
||||||
|
"infoPlist": {
|
||||||
|
"UIBackgroundModes": ["remote-notification"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"permissions": [
|
||||||
|
"android.permission.RECEIVE_BOOT_COMPLETED",
|
||||||
|
"android.permission.VIBRATE",
|
||||||
|
"android.permission.WAKE_LOCK"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 1. 运动提醒
|
||||||
|
```typescript
|
||||||
|
// 每天定时发送运动提醒
|
||||||
|
await scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '运动提醒',
|
||||||
|
body: '该开始今天的普拉提训练了!',
|
||||||
|
data: { type: 'workout_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high'
|
||||||
|
},
|
||||||
|
{ days: 1 }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 目标达成通知
|
||||||
|
```typescript
|
||||||
|
// 用户达成目标时立即发送通知
|
||||||
|
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 心情打卡提醒
|
||||||
|
```typescript
|
||||||
|
// 每天晚上提醒用户记录心情
|
||||||
|
const eveningTime = new Date();
|
||||||
|
eveningTime.setHours(20, 0, 0, 0);
|
||||||
|
|
||||||
|
await scheduleNotification(
|
||||||
|
{
|
||||||
|
title: '心情打卡',
|
||||||
|
body: '记得记录今天的心情状态哦',
|
||||||
|
data: { type: 'mood_checkin' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal'
|
||||||
|
},
|
||||||
|
eveningTime
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 营养提醒
|
||||||
|
```typescript
|
||||||
|
// 定时提醒用户记录饮食
|
||||||
|
await scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '营养记录',
|
||||||
|
body: '记得记录今天的饮食情况',
|
||||||
|
data: { type: 'nutrition_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal'
|
||||||
|
},
|
||||||
|
{ hours: 4 } // 每4小时提醒一次
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 权限处理
|
||||||
|
|
||||||
|
### iOS 权限
|
||||||
|
- 自动请求通知权限
|
||||||
|
- 支持后台通知模式
|
||||||
|
- 处理权限被拒绝的情况
|
||||||
|
|
||||||
|
### Android 权限
|
||||||
|
- 自动请求必要权限
|
||||||
|
- 支持开机启动和唤醒锁
|
||||||
|
- 处理权限被拒绝的情况
|
||||||
|
|
||||||
|
## 通知处理
|
||||||
|
|
||||||
|
### 通知接收处理
|
||||||
|
```typescript
|
||||||
|
Notifications.addNotificationReceivedListener((notification) => {
|
||||||
|
console.log('收到通知:', notification);
|
||||||
|
// 可以在这里处理通知接收逻辑
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通知点击处理
|
||||||
|
```typescript
|
||||||
|
Notifications.addNotificationResponseReceivedListener((response) => {
|
||||||
|
const { notification } = response;
|
||||||
|
const data = notification.request.content.data;
|
||||||
|
|
||||||
|
// 根据通知类型处理不同的逻辑
|
||||||
|
if (data?.type === 'workout_reminder') {
|
||||||
|
// 跳转到运动页面
|
||||||
|
} else if (data?.type === 'goal_achievement') {
|
||||||
|
// 跳转到目标页面
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 通知内容
|
||||||
|
- 标题简洁明了,不超过50个字符
|
||||||
|
- 内容具体有用,不超过200个字符
|
||||||
|
- 使用适当的优先级和声音
|
||||||
|
|
||||||
|
### 2. 定时策略
|
||||||
|
- 避免过于频繁的通知
|
||||||
|
- 考虑用户的使用习惯
|
||||||
|
- 提供通知频率设置选项
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
- 始终处理权限请求失败的情况
|
||||||
|
- 提供用户友好的错误提示
|
||||||
|
- 记录通知发送失败的原因
|
||||||
|
|
||||||
|
### 4. 性能优化
|
||||||
|
- 避免同时发送大量通知
|
||||||
|
- 及时清理不需要的通知
|
||||||
|
- 合理使用重复通知
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
### 1. 功能测试
|
||||||
|
- 测试所有通知类型
|
||||||
|
- 验证权限请求流程
|
||||||
|
- 检查通知点击处理
|
||||||
|
|
||||||
|
### 2. 兼容性测试
|
||||||
|
- 测试不同 iOS 版本
|
||||||
|
- 测试不同 Android 版本
|
||||||
|
- 验证后台通知功能
|
||||||
|
|
||||||
|
### 3. 用户体验测试
|
||||||
|
- 测试通知时机是否合适
|
||||||
|
- 验证通知内容是否清晰
|
||||||
|
- 检查通知频率是否合理
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **通知不显示**
|
||||||
|
- 检查权限是否已授予
|
||||||
|
- 确认应用是否在前台
|
||||||
|
- 验证通知配置是否正确
|
||||||
|
|
||||||
|
2. **定时通知不触发**
|
||||||
|
- 检查设备是否重启
|
||||||
|
- 确认应用是否被系统杀死
|
||||||
|
- 验证时间设置是否正确
|
||||||
|
|
||||||
|
3. **权限被拒绝**
|
||||||
|
- 引导用户到系统设置
|
||||||
|
- 提供权限说明
|
||||||
|
- 实现降级处理方案
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 启用详细日志
|
||||||
|
console.log('通知权限状态:', await notificationService.getPermissionStatus());
|
||||||
|
console.log('已安排通知:', await notificationService.getAllScheduledNotifications());
|
||||||
|
|
||||||
|
// 测试通知发送
|
||||||
|
await notificationService.sendImmediateNotification({
|
||||||
|
title: '测试通知',
|
||||||
|
body: '这是一个测试通知',
|
||||||
|
sound: true
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本推送通知功能实现完整、功能丰富,支持多种通知类型和场景。通过合理的架构设计和错误处理,确保了功能的稳定性和用户体验。开发者可以根据具体需求灵活使用各种通知功能,为用户提供个性化的提醒服务。
|
||||||
157
hooks/useNotifications.ts
Normal file
157
hooks/useNotifications.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { notificationService, NotificationData, NotificationTypes } from '../services/notifications';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
|
||||||
|
export interface UseNotificationsReturn {
|
||||||
|
isInitialized: boolean;
|
||||||
|
permissionStatus: Notifications.PermissionStatus | null;
|
||||||
|
initialize: () => Promise<void>;
|
||||||
|
requestPermission: () => Promise<Notifications.PermissionStatus>;
|
||||||
|
sendNotification: (notification: NotificationData) => Promise<string>;
|
||||||
|
scheduleNotification: (notification: NotificationData, date: Date) => Promise<string>;
|
||||||
|
scheduleRepeatingNotification: (
|
||||||
|
notification: NotificationData,
|
||||||
|
interval: {
|
||||||
|
seconds?: number;
|
||||||
|
minutes?: number;
|
||||||
|
hours?: number;
|
||||||
|
days?: number;
|
||||||
|
weeks?: number;
|
||||||
|
months?: number;
|
||||||
|
years?: number;
|
||||||
|
}
|
||||||
|
) => Promise<string>;
|
||||||
|
cancelNotification: (notificationId: string) => Promise<void>;
|
||||||
|
cancelAllNotifications: () => Promise<void>;
|
||||||
|
getAllScheduledNotifications: () => Promise<Notifications.NotificationRequest[]>;
|
||||||
|
sendWorkoutReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
|
sendGoalAchievement: (title: string, body: string) => Promise<string>;
|
||||||
|
sendMoodCheckinReminder: (title: string, body: string, date?: Date) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNotifications = (): UseNotificationsReturn => {
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [permissionStatus, setPermissionStatus] = useState<Notifications.PermissionStatus | null>(null);
|
||||||
|
|
||||||
|
const initialize = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await notificationService.initialize();
|
||||||
|
const status = await notificationService.getPermissionStatus();
|
||||||
|
setPermissionStatus(status);
|
||||||
|
setIsInitialized(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化通知服务失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const requestPermission = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const status = await notificationService.requestPermission();
|
||||||
|
setPermissionStatus(status);
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求通知权限失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendNotification = useCallback(async (notification: NotificationData) => {
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scheduleNotification = useCallback(async (notification: NotificationData, date: Date) => {
|
||||||
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scheduleRepeatingNotification = useCallback(async (
|
||||||
|
notification: NotificationData,
|
||||||
|
interval: {
|
||||||
|
seconds?: number;
|
||||||
|
minutes?: number;
|
||||||
|
hours?: number;
|
||||||
|
days?: number;
|
||||||
|
weeks?: number;
|
||||||
|
months?: number;
|
||||||
|
years?: number;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return notificationService.scheduleRepeatingNotification(notification, interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelNotification = useCallback(async (notificationId: string) => {
|
||||||
|
return notificationService.cancelNotification(notificationId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancelAllNotifications = useCallback(async () => {
|
||||||
|
return notificationService.cancelAllNotifications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getAllScheduledNotifications = useCallback(async () => {
|
||||||
|
return notificationService.getAllScheduledNotifications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendWorkoutReminder = useCallback(async (title: string, body: string, date?: Date) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.WORKOUT_REMINDER },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
|
} else {
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendGoalAchievement = useCallback(async (title: string, body: string) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
};
|
||||||
|
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sendMoodCheckinReminder = useCallback(async (title: string, body: string, date?: Date) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.MOOD_CHECKIN },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
|
} else {
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 组件挂载时自动初始化
|
||||||
|
useEffect(() => {
|
||||||
|
initialize();
|
||||||
|
}, [initialize]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInitialized,
|
||||||
|
permissionStatus,
|
||||||
|
initialize,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification,
|
||||||
|
scheduleNotification,
|
||||||
|
scheduleRepeatingNotification,
|
||||||
|
cancelNotification,
|
||||||
|
cancelAllNotifications,
|
||||||
|
getAllScheduledNotifications,
|
||||||
|
sendWorkoutReminder,
|
||||||
|
sendGoalAchievement,
|
||||||
|
sendMoodCheckinReminder,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- boost (1.84.0)
|
- boost (1.84.0)
|
||||||
- DoubleConversion (1.1.6)
|
- DoubleConversion (1.1.6)
|
||||||
|
- EXApplication (6.1.5):
|
||||||
|
- ExpoModulesCore
|
||||||
- EXConstants (17.1.7):
|
- EXConstants (17.1.7):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXImageLoader (5.1.0):
|
- EXImageLoader (5.1.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- EXNotifications (0.31.4):
|
||||||
|
- ExpoModulesCore
|
||||||
- Expo (53.0.20):
|
- Expo (53.0.20):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
@@ -1950,8 +1954,10 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||||
|
- EXApplication (from `../node_modules/expo-application/ios`)
|
||||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||||
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
||||||
|
- EXNotifications (from `../node_modules/expo-notifications/ios`)
|
||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||||
@@ -2077,10 +2083,14 @@ EXTERNAL SOURCES:
|
|||||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||||
DoubleConversion:
|
DoubleConversion:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||||
|
EXApplication:
|
||||||
|
:path: "../node_modules/expo-application/ios"
|
||||||
EXConstants:
|
EXConstants:
|
||||||
:path: "../node_modules/expo-constants/ios"
|
:path: "../node_modules/expo-constants/ios"
|
||||||
EXImageLoader:
|
EXImageLoader:
|
||||||
:path: "../node_modules/expo-image-loader/ios"
|
:path: "../node_modules/expo-image-loader/ios"
|
||||||
|
EXNotifications:
|
||||||
|
:path: "../node_modules/expo-notifications/ios"
|
||||||
Expo:
|
Expo:
|
||||||
:path: "../node_modules/expo"
|
:path: "../node_modules/expo"
|
||||||
ExpoAppleAuthentication:
|
ExpoAppleAuthentication:
|
||||||
@@ -2285,8 +2295,10 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||||
|
EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718
|
||||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||||
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee
|
||||||
|
EXNotifications: be5e949edf1d60b70e77178b81aa505298fadd07
|
||||||
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
||||||
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
||||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||||
|
|||||||
@@ -264,8 +264,10 @@
|
|||||||
);
|
);
|
||||||
inputPaths = (
|
inputPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-resources.sh",
|
"${PODS_ROOT}/Target Support Files/Pods-digitalpilates/Pods-digitalpilates-resources.sh",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
@@ -284,8 +286,10 @@
|
|||||||
);
|
);
|
||||||
name = "[CP] Copy Pods Resources";
|
name = "[CP] Copy Pods Resources";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
|
|||||||
@@ -6,20 +6,20 @@
|
|||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>CA92.1</string>
|
<string>C617.1</string>
|
||||||
|
<string>0A2A.1</string>
|
||||||
|
<string>3B52.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>0A2A.1</string>
|
<string>CA92.1</string>
|
||||||
<string>3B52.1</string>
|
|
||||||
<string>C617.1</string>
|
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
<key>com.apple.developer.applesignin</key>
|
<key>com.apple.developer.applesignin</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Default</string>
|
<string>Default</string>
|
||||||
@@ -12,5 +14,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>health-records</string>
|
<string>health-records</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.developer.healthkit.background-delivery</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
144
package-lock.json
generated
144
package-lock.json
generated
@@ -29,6 +29,7 @@
|
|||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "^14.1.5",
|
"expo-linear-gradient": "^14.1.5",
|
||||||
"expo-linking": "~7.1.7",
|
"expo-linking": "~7.1.7",
|
||||||
|
"expo-notifications": "~0.31.4",
|
||||||
"expo-router": "~5.1.4",
|
"expo-router": "~5.1.4",
|
||||||
"expo-splash-screen": "~0.30.10",
|
"expo-splash-screen": "~0.30.10",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
@@ -2352,6 +2353,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ide/backoff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -4850,6 +4857,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@@ -4870,7 +4890,6 @@
|
|||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"possible-typed-array-names": "^1.0.0"
|
"possible-typed-array-names": "^1.0.0"
|
||||||
@@ -5076,6 +5095,12 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badgin": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -5272,7 +5297,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.0",
|
"call-bind-apply-helpers": "^1.0.0",
|
||||||
@@ -5291,7 +5315,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -5305,7 +5328,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -6042,7 +6064,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0",
|
"es-define-property": "^1.0.0",
|
||||||
@@ -6069,7 +6090,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.0.1",
|
"define-data-property": "^1.0.1",
|
||||||
@@ -6210,7 +6230,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -6366,7 +6385,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -6376,7 +6394,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -6414,7 +6431,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@@ -6990,6 +7006,15 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-application": {
|
||||||
|
"version": "6.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-6.1.5.tgz",
|
||||||
|
"integrity": "sha512-ToImFmzw8luY043pWFJhh2ZMm4IwxXoHXxNoGdlhD4Ym6+CCmkAvCglg0FK8dMLzAb+/XabmOE7Rbm8KZb6NZg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "11.1.7",
|
"version": "11.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz",
|
||||||
@@ -7162,6 +7187,26 @@
|
|||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-notifications": {
|
||||||
|
"version": "0.31.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.31.4.tgz",
|
||||||
|
"integrity": "sha512-NnGKIFGpgZU66qfiFUyjEBYsS77VahURpSSeWEOLt+P1zOaUFlgx2XqS+dxH3/Bn1Vm7TMj04qKsK5KvzR/8Lw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/image-utils": "^0.7.6",
|
||||||
|
"@ide/backoff": "^1.0.0",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"badgin": "^1.1.5",
|
||||||
|
"expo-application": "~6.1.5",
|
||||||
|
"expo-constants": "~17.1.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-5.1.4.tgz",
|
||||||
@@ -7562,7 +7607,6 @@
|
|||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-callable": "^1.2.7"
|
"is-callable": "^1.2.7"
|
||||||
@@ -7690,7 +7734,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
@@ -7724,7 +7767,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@@ -7865,7 +7907,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7913,7 +7954,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-define-property": "^1.0.0"
|
"es-define-property": "^1.0.0"
|
||||||
@@ -7942,7 +7982,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7955,7 +7994,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -8294,6 +8332,22 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -8398,7 +8452,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -8520,7 +8573,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
|
||||||
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
|
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.3",
|
"call-bound": "^1.0.3",
|
||||||
@@ -8561,6 +8613,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
@@ -8613,7 +8681,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -8696,7 +8763,6 @@
|
|||||||
"version": "1.1.15",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
|
||||||
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"which-typed-array": "^1.1.16"
|
"which-typed-array": "^1.1.16"
|
||||||
@@ -9622,7 +9688,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -10331,11 +10396,26 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -10345,7 +10425,6 @@
|
|||||||
"version": "4.1.7",
|
"version": "4.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
|
||||||
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
"integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.8",
|
||||||
@@ -10835,7 +10914,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -12201,7 +12279,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
|
||||||
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
@@ -12470,7 +12547,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.4",
|
"define-data-property": "^1.1.4",
|
||||||
@@ -13803,6 +13879,19 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
@@ -14000,7 +14089,6 @@
|
|||||||
"version": "1.1.19",
|
"version": "1.1.19",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||||
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"available-typed-arrays": "^1.0.7",
|
"available-typed-arrays": "^1.0.7",
|
||||||
|
|||||||
@@ -59,7 +59,8 @@
|
|||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"react-native-wheel-picker-expo": "^0.5.4",
|
"react-native-wheel-picker-expo": "^0.5.4",
|
||||||
"react-redux": "^9.2.0"
|
"react-redux": "^9.2.0",
|
||||||
|
"expo-notifications": "~0.31.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { Platform } from 'react-native';
|
|
||||||
import {
|
|
||||||
HKQuantityTypeIdentifier,
|
|
||||||
HKQuantitySample,
|
|
||||||
getMostRecentQuantitySample,
|
|
||||||
isAvailable,
|
|
||||||
authorize,
|
|
||||||
} from 'react-native-health';
|
|
||||||
|
|
||||||
interface HealthData {
|
|
||||||
oxygenSaturation: number | null;
|
|
||||||
heartRate: number | null;
|
|
||||||
lastUpdated: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HealthDataService {
|
|
||||||
private static instance: HealthDataService;
|
|
||||||
private isAuthorized = false;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
public static getInstance(): HealthDataService {
|
|
||||||
if (!HealthDataService.instance) {
|
|
||||||
HealthDataService.instance = new HealthDataService();
|
|
||||||
}
|
|
||||||
return HealthDataService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestAuthorization(): Promise<boolean> {
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const available = await isAvailable();
|
|
||||||
if (!available) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const permissions = [
|
|
||||||
{
|
|
||||||
type: HKQuantityTypeIdentifier.OxygenSaturation,
|
|
||||||
access: 'read' as const
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: HKQuantityTypeIdentifier.HeartRate,
|
|
||||||
access: 'read' as const
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const authorized = await authorize(permissions);
|
|
||||||
this.isAuthorized = authorized;
|
|
||||||
return authorized;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Health data authorization error:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOxygenSaturation(): Promise<number | null> {
|
|
||||||
if (!this.isAuthorized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sample: HKQuantitySample | null = await getMostRecentQuantitySample(
|
|
||||||
HKQuantityTypeIdentifier.OxygenSaturation
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sample) {
|
|
||||||
return Number(sample.value.toFixed(1));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading oxygen saturation:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHeartRate(): Promise<number | null> {
|
|
||||||
if (!this.isAuthorized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sample: HKQuantitySample | null = await getMostRecentQuantitySample(
|
|
||||||
HKQuantityTypeIdentifier.HeartRate
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sample) {
|
|
||||||
return Math.round(sample.value);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading heart rate:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHealthData(): Promise<HealthData> {
|
|
||||||
const [oxygenSaturation, heartRate] = await Promise.all([
|
|
||||||
this.getOxygenSaturation(),
|
|
||||||
this.getHeartRate()
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
oxygenSaturation,
|
|
||||||
heartRate,
|
|
||||||
lastUpdated: new Date()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HealthDataService.getInstance();
|
|
||||||
352
services/notifications.ts
Normal file
352
services/notifications.ts
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
// 配置通知处理方式
|
||||||
|
Notifications.setNotificationHandler({
|
||||||
|
handleNotification: async () => ({
|
||||||
|
shouldShowAlert: true,
|
||||||
|
shouldPlaySound: true,
|
||||||
|
shouldSetBadge: true,
|
||||||
|
shouldShowBanner: true,
|
||||||
|
shouldShowList: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface NotificationData {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
sound?: boolean;
|
||||||
|
priority?: 'default' | 'normal' | 'high';
|
||||||
|
vibrate?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationService {
|
||||||
|
private static instance: NotificationService;
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): NotificationService {
|
||||||
|
if (!NotificationService.instance) {
|
||||||
|
NotificationService.instance = new NotificationService();
|
||||||
|
}
|
||||||
|
return NotificationService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化推送通知服务
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 请求通知权限
|
||||||
|
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||||
|
let finalStatus = existingStatus;
|
||||||
|
|
||||||
|
if (existingStatus !== 'granted') {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
finalStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalStatus !== 'granted') {
|
||||||
|
console.warn('推送通知权限未授予');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取推送令牌(用于远程推送,本地推送不需要)
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
const token = await Notifications.getExpoPushTokenAsync({
|
||||||
|
projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
|
||||||
|
});
|
||||||
|
console.log('推送令牌:', token.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置通知监听器
|
||||||
|
this.setupNotificationListeners();
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('推送通知服务初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('推送通知服务初始化失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置通知监听器
|
||||||
|
*/
|
||||||
|
private setupNotificationListeners(): void {
|
||||||
|
// 监听通知接收
|
||||||
|
Notifications.addNotificationReceivedListener((notification) => {
|
||||||
|
console.log('收到通知:', notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听通知点击
|
||||||
|
Notifications.addNotificationResponseReceivedListener((response) => {
|
||||||
|
console.log('用户点击了通知:', response);
|
||||||
|
// 这里可以处理通知点击后的逻辑
|
||||||
|
this.handleNotificationResponse(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理通知响应
|
||||||
|
*/
|
||||||
|
private handleNotificationResponse(response: Notifications.NotificationResponse): void {
|
||||||
|
const { notification } = response;
|
||||||
|
const data = notification.request.content.data;
|
||||||
|
|
||||||
|
// 根据通知类型处理不同的逻辑
|
||||||
|
if (data?.type === 'workout_reminder') {
|
||||||
|
// 处理运动提醒
|
||||||
|
console.log('用户点击了运动提醒通知');
|
||||||
|
} else if (data?.type === 'goal_achievement') {
|
||||||
|
// 处理目标达成通知
|
||||||
|
console.log('用户点击了目标达成通知');
|
||||||
|
} else if (data?.type === 'mood_checkin') {
|
||||||
|
// 处理心情打卡提醒
|
||||||
|
console.log('用户点击了心情打卡提醒');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送本地推送通知
|
||||||
|
*/
|
||||||
|
async scheduleLocalNotification(
|
||||||
|
notification: NotificationData,
|
||||||
|
trigger?: Notifications.NotificationTriggerInput
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
data: notification.data || {},
|
||||||
|
sound: notification.sound ? 'default' : undefined,
|
||||||
|
priority: notification.priority || 'default',
|
||||||
|
vibrate: notification.vibrate,
|
||||||
|
},
|
||||||
|
trigger: trigger || null, // null表示立即发送
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('本地通知已安排,ID:', notificationId);
|
||||||
|
return notificationId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('安排本地通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送立即通知
|
||||||
|
*/
|
||||||
|
async sendImmediateNotification(notification: NotificationData): Promise<string> {
|
||||||
|
return this.scheduleLocalNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排定时通知
|
||||||
|
*/
|
||||||
|
async scheduleNotificationAtDate(
|
||||||
|
notification: NotificationData,
|
||||||
|
date: Date
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
data: notification.data || {},
|
||||||
|
sound: notification.sound ? 'default' : undefined,
|
||||||
|
priority: notification.priority || 'default',
|
||||||
|
vibrate: notification.vibrate,
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
date: date.getTime(),
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('定时通知已安排,ID:', notificationId);
|
||||||
|
return notificationId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('安排定时通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排重复通知(仅支持秒级别)
|
||||||
|
*/
|
||||||
|
async scheduleRepeatingNotification(
|
||||||
|
notification: NotificationData,
|
||||||
|
interval: {
|
||||||
|
seconds?: number;
|
||||||
|
minutes?: number;
|
||||||
|
hours?: number;
|
||||||
|
days?: number;
|
||||||
|
weeks?: number;
|
||||||
|
months?: number;
|
||||||
|
years?: number;
|
||||||
|
}
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
// 计算总秒数
|
||||||
|
const totalSeconds =
|
||||||
|
(interval.seconds || 0) +
|
||||||
|
(interval.minutes || 0) * 60 +
|
||||||
|
(interval.hours || 0) * 3600 +
|
||||||
|
(interval.days || 0) * 86400 +
|
||||||
|
(interval.weeks || 0) * 604800 +
|
||||||
|
(interval.months || 0) * 2592000 +
|
||||||
|
(interval.years || 0) * 31536000;
|
||||||
|
|
||||||
|
if (totalSeconds <= 0) {
|
||||||
|
throw new Error('重复间隔必须大于0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
data: notification.data || {},
|
||||||
|
sound: notification.sound ? 'default' : undefined,
|
||||||
|
priority: notification.priority || 'default',
|
||||||
|
vibrate: notification.vibrate,
|
||||||
|
},
|
||||||
|
trigger: {
|
||||||
|
seconds: totalSeconds,
|
||||||
|
repeats: true,
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('重复通知已安排,ID:', notificationId);
|
||||||
|
return notificationId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('安排重复通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消特定通知
|
||||||
|
*/
|
||||||
|
async cancelNotification(notificationId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
||||||
|
console.log('通知已取消:', notificationId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有通知
|
||||||
|
*/
|
||||||
|
async cancelAllNotifications(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||||
|
console.log('所有通知已取消');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消所有通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有已安排的通知
|
||||||
|
*/
|
||||||
|
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
|
||||||
|
try {
|
||||||
|
const notifications = await Notifications.getAllScheduledNotificationsAsync();
|
||||||
|
return notifications;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取已安排通知失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取通知权限状态
|
||||||
|
*/
|
||||||
|
async getPermissionStatus(): Promise<Notifications.PermissionStatus> {
|
||||||
|
try {
|
||||||
|
const { status } = await Notifications.getPermissionsAsync();
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取通知权限状态失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求通知权限
|
||||||
|
*/
|
||||||
|
async requestPermission(): Promise<Notifications.PermissionStatus> {
|
||||||
|
try {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求通知权限失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const notificationService = NotificationService.getInstance();
|
||||||
|
|
||||||
|
// 预定义的推送通知类型
|
||||||
|
export const NotificationTypes = {
|
||||||
|
WORKOUT_REMINDER: 'workout_reminder',
|
||||||
|
GOAL_ACHIEVEMENT: 'goal_achievement',
|
||||||
|
MOOD_CHECKIN: 'mood_checkin',
|
||||||
|
NUTRITION_REMINDER: 'nutrition_reminder',
|
||||||
|
PROGRESS_UPDATE: 'progress_update',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 便捷方法
|
||||||
|
export const sendWorkoutReminder = (title: string, body: string, date?: Date) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.WORKOUT_REMINDER },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
|
} else {
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendGoalAchievement = (title: string, body: string) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
};
|
||||||
|
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
|
||||||
|
const notification: NotificationData = {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data: { type: NotificationTypes.MOOD_CHECKIN },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
return notificationService.scheduleNotificationAtDate(notification, date);
|
||||||
|
} else {
|
||||||
|
return notificationService.sendImmediateNotification(notification);
|
||||||
|
}
|
||||||
|
};
|
||||||
337
utils/notificationHelpers.ts
Normal file
337
utils/notificationHelpers.ts
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
import { notificationService, NotificationData } from '../services/notifications';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运动相关的通知辅助函数
|
||||||
|
*/
|
||||||
|
export class WorkoutNotificationHelpers {
|
||||||
|
/**
|
||||||
|
* 发送运动开始提醒
|
||||||
|
*/
|
||||||
|
static async sendWorkoutStartReminder(userName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '运动时间到',
|
||||||
|
body: `${userName},该开始今天的普拉提训练了!`,
|
||||||
|
data: { type: 'workout_start' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送运动完成通知
|
||||||
|
*/
|
||||||
|
static async sendWorkoutCompleteNotification(userName: string, duration: number) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '运动完成',
|
||||||
|
body: `${userName},恭喜您完成了${duration}分钟的训练!`,
|
||||||
|
data: { type: 'workout_complete' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排每日运动提醒
|
||||||
|
*/
|
||||||
|
static async scheduleDailyWorkoutReminder(userName: string, hour: number = 9, minute: number = 0) {
|
||||||
|
const reminderTime = new Date();
|
||||||
|
reminderTime.setHours(hour, minute, 0, 0);
|
||||||
|
|
||||||
|
// 如果今天的时间已经过了,设置为明天
|
||||||
|
if (reminderTime.getTime() <= Date.now()) {
|
||||||
|
reminderTime.setDate(reminderTime.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationService.scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '每日运动提醒',
|
||||||
|
body: `${userName},该开始今天的普拉提训练了!`,
|
||||||
|
data: { type: 'daily_workout_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
},
|
||||||
|
{ days: 1 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标相关的通知辅助函数
|
||||||
|
*/
|
||||||
|
export class GoalNotificationHelpers {
|
||||||
|
/**
|
||||||
|
* 发送目标达成通知
|
||||||
|
*/
|
||||||
|
static async sendGoalAchievementNotification(userName: string, goalName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '目标达成',
|
||||||
|
body: `${userName},恭喜您达成了目标:${goalName}!`,
|
||||||
|
data: { type: 'goal_achievement', goalName },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送目标进度更新通知
|
||||||
|
*/
|
||||||
|
static async sendGoalProgressNotification(userName: string, goalName: string, progress: number) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '目标进度',
|
||||||
|
body: `${userName},您的目标"${goalName}"已完成${progress}%!`,
|
||||||
|
data: { type: 'goal_progress', goalName, progress },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排目标提醒
|
||||||
|
*/
|
||||||
|
static async scheduleGoalReminder(userName: string, goalName: string, deadline: Date) {
|
||||||
|
// 在截止日期前一天发送提醒
|
||||||
|
const reminderDate = new Date(deadline);
|
||||||
|
reminderDate.setDate(reminderDate.getDate() - 1);
|
||||||
|
|
||||||
|
return notificationService.scheduleNotificationAtDate(
|
||||||
|
{
|
||||||
|
title: '目标截止提醒',
|
||||||
|
body: `${userName},您的目标"${goalName}"明天就要截止了,加油!`,
|
||||||
|
data: { type: 'goal_deadline_reminder', goalName },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
},
|
||||||
|
reminderDate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心情相关的通知辅助函数
|
||||||
|
*/
|
||||||
|
export class MoodNotificationHelpers {
|
||||||
|
/**
|
||||||
|
* 发送心情打卡提醒
|
||||||
|
*/
|
||||||
|
static async sendMoodCheckinReminder(userName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '心情打卡',
|
||||||
|
body: `${userName},记得记录今天的心情状态哦`,
|
||||||
|
data: { type: 'mood_checkin_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排每日心情打卡提醒
|
||||||
|
*/
|
||||||
|
static async scheduleDailyMoodReminder(userName: string, hour: number = 20, minute: number = 0) {
|
||||||
|
const reminderTime = new Date();
|
||||||
|
reminderTime.setHours(hour, minute, 0, 0);
|
||||||
|
|
||||||
|
// 如果今天的时间已经过了,设置为明天
|
||||||
|
if (reminderTime.getTime() <= Date.now()) {
|
||||||
|
reminderTime.setDate(reminderTime.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationService.scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: '每日心情打卡',
|
||||||
|
body: `${userName},记得记录今天的心情状态哦`,
|
||||||
|
data: { type: 'daily_mood_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
},
|
||||||
|
{ days: 1 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 营养相关的通知辅助函数
|
||||||
|
*/
|
||||||
|
export class NutritionNotificationHelpers {
|
||||||
|
/**
|
||||||
|
* 发送营养记录提醒
|
||||||
|
*/
|
||||||
|
static async sendNutritionRecordReminder(userName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '营养记录',
|
||||||
|
body: `${userName},记得记录今天的饮食情况`,
|
||||||
|
data: { type: 'nutrition_record_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安排营养记录提醒
|
||||||
|
*/
|
||||||
|
static async scheduleNutritionReminders(userName: string) {
|
||||||
|
// 安排三餐提醒
|
||||||
|
const mealTimes = [
|
||||||
|
{ hour: 8, minute: 0, meal: '早餐' },
|
||||||
|
{ hour: 12, minute: 0, meal: '午餐' },
|
||||||
|
{ hour: 18, minute: 0, meal: '晚餐' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const notifications = [];
|
||||||
|
|
||||||
|
for (const mealTime of mealTimes) {
|
||||||
|
const reminderTime = new Date();
|
||||||
|
reminderTime.setHours(mealTime.hour, mealTime.minute, 0, 0);
|
||||||
|
|
||||||
|
// 如果今天的时间已经过了,设置为明天
|
||||||
|
if (reminderTime.getTime() <= Date.now()) {
|
||||||
|
reminderTime.setDate(reminderTime.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationId = await notificationService.scheduleRepeatingNotification(
|
||||||
|
{
|
||||||
|
title: `${mealTime.meal}提醒`,
|
||||||
|
body: `${userName},记得记录您的${mealTime.meal}情况`,
|
||||||
|
data: { type: 'meal_reminder', meal: mealTime.meal },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
},
|
||||||
|
{ days: 1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
notifications.push(notificationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用通知辅助函数
|
||||||
|
*/
|
||||||
|
export class GeneralNotificationHelpers {
|
||||||
|
/**
|
||||||
|
* 发送欢迎通知
|
||||||
|
*/
|
||||||
|
static async sendWelcomeNotification(userName: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '欢迎使用',
|
||||||
|
body: `${userName},欢迎来到普拉提世界!开始您的健康之旅吧`,
|
||||||
|
data: { type: 'welcome' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送成就通知
|
||||||
|
*/
|
||||||
|
static async sendAchievementNotification(userName: string, achievement: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '新成就',
|
||||||
|
body: `${userName},恭喜您获得了新成就:${achievement}!`,
|
||||||
|
data: { type: 'achievement', achievement },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送系统维护通知
|
||||||
|
*/
|
||||||
|
static async sendMaintenanceNotification(message: string) {
|
||||||
|
return notificationService.sendImmediateNotification({
|
||||||
|
title: '系统通知',
|
||||||
|
body: message,
|
||||||
|
data: { type: 'maintenance' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消特定类型的通知
|
||||||
|
*/
|
||||||
|
static async cancelNotificationsByType(type: string) {
|
||||||
|
const notifications = await notificationService.getAllScheduledNotifications();
|
||||||
|
|
||||||
|
for (const notification of notifications) {
|
||||||
|
if (notification.content.data?.type === type) {
|
||||||
|
await notificationService.cancelNotification(notification.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量发送通知
|
||||||
|
*/
|
||||||
|
static async sendBatchNotifications(notifications: NotificationData[]) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const notification of notifications) {
|
||||||
|
try {
|
||||||
|
const id = await notificationService.sendImmediateNotification(notification);
|
||||||
|
results.push({ success: true, id, notification });
|
||||||
|
} catch (error) {
|
||||||
|
results.push({ success: false, error, notification });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知模板
|
||||||
|
*/
|
||||||
|
export const NotificationTemplates = {
|
||||||
|
workout: {
|
||||||
|
start: (userName: string) => ({
|
||||||
|
title: '运动时间到',
|
||||||
|
body: `${userName},该开始今天的普拉提训练了!`,
|
||||||
|
data: { type: 'workout_start' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high' as const,
|
||||||
|
}),
|
||||||
|
complete: (userName: string, duration: number) => ({
|
||||||
|
title: '运动完成',
|
||||||
|
body: `${userName},恭喜您完成了${duration}分钟的训练!`,
|
||||||
|
data: { type: 'workout_complete' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal' as const,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
goal: {
|
||||||
|
achievement: (userName: string, goalName: string) => ({
|
||||||
|
title: '目标达成',
|
||||||
|
body: `${userName},恭喜您达成了目标:${goalName}!`,
|
||||||
|
data: { type: 'goal_achievement', goalName },
|
||||||
|
sound: true,
|
||||||
|
priority: 'high' as const,
|
||||||
|
}),
|
||||||
|
progress: (userName: string, goalName: string, progress: number) => ({
|
||||||
|
title: '目标进度',
|
||||||
|
body: `${userName},您的目标"${goalName}"已完成${progress}%!`,
|
||||||
|
data: { type: 'goal_progress', goalName, progress },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal' as const,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mood: {
|
||||||
|
reminder: (userName: string) => ({
|
||||||
|
title: '心情打卡',
|
||||||
|
body: `${userName},记得记录今天的心情状态哦`,
|
||||||
|
data: { type: 'mood_checkin_reminder' },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal' as const,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
nutrition: {
|
||||||
|
reminder: (userName: string, meal: string) => ({
|
||||||
|
title: `${meal}提醒`,
|
||||||
|
body: `${userName},记得记录您的${meal}情况`,
|
||||||
|
data: { type: 'meal_reminder', meal },
|
||||||
|
sound: true,
|
||||||
|
priority: 'normal' as const,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user