feat: 添加用户推送通知偏好设置功能,支持开启/关闭推送通知
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
|||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { useNotifications } from '@/hooks/useNotifications';
|
||||||
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
import { DEFAULT_MEMBER_NAME, fetchActivityHistory, fetchMyProfile } from '@/store/userSlice';
|
||||||
|
import { getNotificationEnabled, setNotificationEnabled as saveNotificationEnabled } from '@/utils/userPreferences';
|
||||||
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';
|
||||||
@@ -42,10 +43,22 @@ export default function PersonalScreen() {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
dispatch(fetchMyProfile());
|
dispatch(fetchMyProfile());
|
||||||
dispatch(fetchActivityHistory())
|
dispatch(fetchActivityHistory());
|
||||||
|
// 加载用户推送偏好设置
|
||||||
|
loadNotificationPreference();
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 加载用户推送偏好设置
|
||||||
|
const loadNotificationPreference = async () => {
|
||||||
|
try {
|
||||||
|
const enabled = await getNotificationEnabled();
|
||||||
|
setNotificationEnabled(enabled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载推送偏好设置失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 数据格式化函数
|
// 数据格式化函数
|
||||||
const formatHeight = () => {
|
const formatHeight = () => {
|
||||||
if (userProfile.height == null) return '--';
|
if (userProfile.height == null) return '--';
|
||||||
@@ -68,22 +81,22 @@ 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(() => {
|
useEffect(() => {
|
||||||
if (permissionStatus === 'granted') {
|
loadNotificationPreference();
|
||||||
setNotificationEnabled(true);
|
}, []);
|
||||||
} else {
|
|
||||||
setNotificationEnabled(false);
|
|
||||||
}
|
|
||||||
}, [permissionStatus]);
|
|
||||||
|
|
||||||
// 处理通知开关变化
|
// 处理通知开关变化
|
||||||
const handleNotificationToggle = async (value: boolean) => {
|
const handleNotificationToggle = async (value: boolean) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
try {
|
try {
|
||||||
|
// 先检查系统权限
|
||||||
const status = await requestPermission();
|
const status = await requestPermission();
|
||||||
if (status === 'granted') {
|
if (status === 'granted') {
|
||||||
|
// 系统权限获取成功,保存用户偏好设置
|
||||||
|
await saveNotificationEnabled(true);
|
||||||
setNotificationEnabled(true);
|
setNotificationEnabled(true);
|
||||||
|
|
||||||
// 发送测试通知
|
// 发送测试通知
|
||||||
await sendNotification({
|
await sendNotification({
|
||||||
title: '通知已开启',
|
title: '通知已开启',
|
||||||
@@ -92,14 +105,29 @@ export default function PersonalScreen() {
|
|||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Alert.alert('权限被拒绝', '请在系统设置中开启通知权限');
|
// 系统权限被拒绝,不更新用户偏好设置
|
||||||
|
Alert.alert(
|
||||||
|
'权限被拒绝',
|
||||||
|
'请在系统设置中开启通知权限,然后再尝试开启推送功能',
|
||||||
|
[
|
||||||
|
{ text: '取消', style: 'cancel' },
|
||||||
|
{ text: '去设置', onPress: () => Linking.openSettings() }
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('开启推送通知失败:', error);
|
||||||
Alert.alert('错误', '请求通知权限失败');
|
Alert.alert('错误', '请求通知权限失败');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
|
// 关闭推送,保存用户偏好设置
|
||||||
|
await saveNotificationEnabled(false);
|
||||||
setNotificationEnabled(false);
|
setNotificationEnabled(false);
|
||||||
Alert.alert('通知已关闭', '您将不会收到推送通知');
|
} catch (error) {
|
||||||
|
console.error('关闭推送通知失败:', error);
|
||||||
|
Alert.alert('错误', '保存设置失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,15 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
|
|
||||||
// 触发柱体动画
|
// 触发柱体动画
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chartData && chartData.length > 0) {
|
// 检查是否有实际数据(不只是空数组)
|
||||||
|
const hasData = chartData && chartData.length > 0 && chartData.some(data => data.steps > 0);
|
||||||
|
|
||||||
|
if (hasData) {
|
||||||
// 重置所有动画值
|
// 重置所有动画值
|
||||||
animatedValues.forEach(animValue => animValue.setValue(0));
|
animatedValues.forEach(animValue => animValue.setValue(0));
|
||||||
|
|
||||||
|
// 使用 setTimeout 确保在下一个事件循环中执行动画,保证组件已完全渲染
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
// 同时启动所有柱体的弹性动画,有步数的柱体才执行动画
|
// 同时启动所有柱体的弹性动画,有步数的柱体才执行动画
|
||||||
chartData.forEach((data, index) => {
|
chartData.forEach((data, index) => {
|
||||||
if (data.steps > 0) {
|
if (data.steps > 0) {
|
||||||
@@ -69,6 +74,9 @@ const StepsCard: React.FC<StepsCardProps> = ({
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}, 50); // 添加小延迟确保渲染完成
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}, [chartData, animatedValues]);
|
}, [chartData, animatedValues]);
|
||||||
|
|
||||||
|
|||||||
137
docs/notification-user-preference-implementation.md
Normal file
137
docs/notification-user-preference-implementation.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# 推送通知用户偏好设置控制实现文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本次实现为项目的推送通知系统添加了用户偏好设置控制功能。用户现在可以通过个人页面的开关来控制是否接收推送通知,该设置会在底层推送服务中生效。
|
||||||
|
|
||||||
|
## 实现内容
|
||||||
|
|
||||||
|
### 1. 用户偏好设置扩展 (`utils/userPreferences.ts`)
|
||||||
|
|
||||||
|
新增了以下功能:
|
||||||
|
|
||||||
|
- **接口扩展**:在 `UserPreferences` 接口中添加了 `notificationEnabled: boolean` 字段
|
||||||
|
- **存储键**:添加了 `NOTIFICATION_ENABLED` 存储键
|
||||||
|
- **默认值**:推送通知默认开启 (`true`)
|
||||||
|
- **新增方法**:
|
||||||
|
- `setNotificationEnabled(enabled: boolean)`: 设置推送开关状态
|
||||||
|
- `getNotificationEnabled()`: 获取推送开关状态
|
||||||
|
- 更新了 `resetUserPreferences()` 以包含推送设置
|
||||||
|
|
||||||
|
### 2. 推送服务核心控制 (`services/notifications.ts`)
|
||||||
|
|
||||||
|
在 `NotificationService` 类中添加了用户偏好检查机制:
|
||||||
|
|
||||||
|
- **新增方法**:`isNotificationAllowed()` - 检查用户是否允许推送通知
|
||||||
|
- **双重检查**:
|
||||||
|
1. 检查用户偏好设置中的推送开关
|
||||||
|
2. 检查系统推送权限状态
|
||||||
|
- **统一拦截**:在 `scheduleLocalNotification()` 方法中统一检查,确保所有推送都受控制
|
||||||
|
|
||||||
|
### 3. 个人页面开关同步 (`app/(tabs)/personal.tsx`)
|
||||||
|
|
||||||
|
更新了个人页面的推送开关逻辑:
|
||||||
|
|
||||||
|
- **状态同步**:开关状态与用户偏好设置保持同步
|
||||||
|
- **页面加载**:页面聚焦时自动加载用户推送偏好设置
|
||||||
|
- **开关处理**:
|
||||||
|
- 开启时:先检查系统权限,成功后保存用户偏好
|
||||||
|
- 关闭时:直接保存用户偏好设置
|
||||||
|
- **错误处理**:提供友好的错误提示和引导
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户操作推送开关
|
||||||
|
↓
|
||||||
|
检查系统推送权限 (仅开启时)
|
||||||
|
↓
|
||||||
|
保存用户偏好设置
|
||||||
|
↓
|
||||||
|
底层推送服务检查
|
||||||
|
↓
|
||||||
|
发送/阻止推送通知
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 1. 用户操作
|
||||||
|
|
||||||
|
用户可以在个人页面的"通知"部分找到"消息推送"开关:
|
||||||
|
- 开启:需要系统权限,成功后会发送测试通知
|
||||||
|
- 关闭:立即生效,不会收到任何推送通知
|
||||||
|
|
||||||
|
### 2. 开发者使用
|
||||||
|
|
||||||
|
所有现有的推送通知方法都会自动受到用户偏好设置控制:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 这些方法都会自动检查用户偏好设置
|
||||||
|
await notificationService.sendImmediateNotification(notification);
|
||||||
|
await WorkoutNotificationHelpers.sendWorkoutStartReminder(userName);
|
||||||
|
await GoalNotificationHelpers.sendGoalAchievementNotification(userName, goalName);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试功能
|
||||||
|
|
||||||
|
提供了完整的测试工具 (`utils/notificationTest.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { NotificationTestHelpers } from '@/utils/notificationTest';
|
||||||
|
|
||||||
|
// 测试用户偏好设置控制
|
||||||
|
await NotificationTestHelpers.testUserPreferenceControl();
|
||||||
|
|
||||||
|
// 测试权限状态
|
||||||
|
await NotificationTestHelpers.testPermissionStatus();
|
||||||
|
|
||||||
|
// 测试不同类型的通知
|
||||||
|
await NotificationTestHelpers.testDifferentNotificationTypes();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术特点
|
||||||
|
|
||||||
|
### 1. 双重保护机制
|
||||||
|
- **用户偏好层**:用户主动控制是否接收推送
|
||||||
|
- **系统权限层**:系统级别的推送权限控制
|
||||||
|
|
||||||
|
### 2. 统一拦截点
|
||||||
|
- 所有推送通知都通过 `NotificationService.scheduleLocalNotification()` 方法
|
||||||
|
- 在此方法中统一检查用户偏好和系统权限
|
||||||
|
- 确保没有推送能绕过用户设置
|
||||||
|
|
||||||
|
### 3. 状态同步
|
||||||
|
- 个人页面开关与用户偏好设置实时同步
|
||||||
|
- 页面聚焦时自动刷新状态
|
||||||
|
- 避免状态不一致问题
|
||||||
|
|
||||||
|
### 4. 向后兼容
|
||||||
|
- 不影响现有推送通知代码
|
||||||
|
- 所有现有方法自动获得用户偏好控制功能
|
||||||
|
- 默认开启推送,保持原有用户体验
|
||||||
|
|
||||||
|
## 日志和调试
|
||||||
|
|
||||||
|
系统会在控制台输出相关日志:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户已在偏好设置中关闭推送通知
|
||||||
|
推送通知被用户偏好设置或系统权限阻止,跳过发送
|
||||||
|
本地通知已安排,ID: notification-id-123
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **权限优先级**:即使用户开启了偏好设置,如果系统权限被拒绝,推送仍然无法发送
|
||||||
|
2. **测试通知**:用户开启推送时会发送一条测试通知,确认功能正常
|
||||||
|
3. **错误处理**:所有操作都有完善的错误处理和用户提示
|
||||||
|
4. **性能影响**:每次发送推送前会进行异步检查,但影响微乎其微
|
||||||
|
|
||||||
|
## 未来扩展
|
||||||
|
|
||||||
|
可以考虑添加更细粒度的推送控制:
|
||||||
|
- 按通知类型分别控制(运动提醒、目标通知等)
|
||||||
|
- 时间段控制(勿扰时间)
|
||||||
|
- 频率控制(每日最大推送数量)
|
||||||
|
|
||||||
|
这些功能可以基于当前的架构轻松扩展。
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getNotificationEnabled } from '@/utils/userPreferences';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
|
|
||||||
@@ -180,6 +181,32 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否允许推送通知
|
||||||
|
*/
|
||||||
|
private async isNotificationAllowed(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 检查用户偏好设置中的推送开关
|
||||||
|
const userPreferenceEnabled = await getNotificationEnabled();
|
||||||
|
if (!userPreferenceEnabled) {
|
||||||
|
console.log('用户已在偏好设置中关闭推送通知');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查系统权限
|
||||||
|
const permissionStatus = await this.getPermissionStatus();
|
||||||
|
if (permissionStatus !== 'granted') {
|
||||||
|
console.log('系统推送权限未授予');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查推送权限失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送本地推送通知
|
* 发送本地推送通知
|
||||||
*/
|
*/
|
||||||
@@ -188,6 +215,13 @@ export class NotificationService {
|
|||||||
trigger?: Notifications.NotificationTriggerInput
|
trigger?: Notifications.NotificationTriggerInput
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
// 检查用户是否允许推送通知
|
||||||
|
const isAllowed = await this.isNotificationAllowed();
|
||||||
|
if (!isAllowed) {
|
||||||
|
console.log('推送通知被用户偏好设置或系统权限阻止,跳过发送');
|
||||||
|
return 'blocked_by_user_preference';
|
||||||
|
}
|
||||||
|
|
||||||
const notificationId = await Notifications.scheduleNotificationAsync({
|
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||||
content: {
|
content: {
|
||||||
title: notification.title,
|
title: notification.title,
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
// 用户偏好设置的存储键
|
// 用户偏好设置的存储键
|
||||||
const PREFERENCES_KEYS = {
|
const PREFERENCES_KEYS = {
|
||||||
QUICK_WATER_AMOUNT: 'user_preference_quick_water_amount',
|
QUICK_WATER_AMOUNT: 'user_preference_quick_water_amount',
|
||||||
|
NOTIFICATION_ENABLED: 'user_preference_notification_enabled',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// 用户偏好设置接口
|
// 用户偏好设置接口
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
quickWaterAmount: number;
|
quickWaterAmount: number;
|
||||||
|
notificationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认的用户偏好设置
|
// 默认的用户偏好设置
|
||||||
const DEFAULT_PREFERENCES: UserPreferences = {
|
const DEFAULT_PREFERENCES: UserPreferences = {
|
||||||
quickWaterAmount: 150, // 默认快速添加饮水量为 250ml
|
quickWaterAmount: 150, // 默认快速添加饮水量为 150ml
|
||||||
|
notificationEnabled: true, // 默认开启消息推送
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,9 +24,11 @@ const DEFAULT_PREFERENCES: UserPreferences = {
|
|||||||
export const getUserPreferences = async (): Promise<UserPreferences> => {
|
export const getUserPreferences = async (): Promise<UserPreferences> => {
|
||||||
try {
|
try {
|
||||||
const quickWaterAmount = await AsyncStorage.getItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT);
|
const quickWaterAmount = await AsyncStorage.getItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT);
|
||||||
|
const notificationEnabled = await AsyncStorage.getItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount,
|
quickWaterAmount: quickWaterAmount ? parseInt(quickWaterAmount, 10) : DEFAULT_PREFERENCES.quickWaterAmount,
|
||||||
|
notificationEnabled: notificationEnabled !== null ? notificationEnabled === 'true' : DEFAULT_PREFERENCES.notificationEnabled,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户偏好设置失败:', error);
|
console.error('获取用户偏好设置失败:', error);
|
||||||
@@ -59,12 +64,39 @@ export const getQuickWaterAmount = async (): Promise<number> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息推送开关
|
||||||
|
* @param enabled 是否开启消息推送
|
||||||
|
*/
|
||||||
|
export const setNotificationEnabled = async (enabled: boolean): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED, enabled.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置消息推送开关失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息推送开关状态
|
||||||
|
*/
|
||||||
|
export const getNotificationEnabled = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const enabled = await AsyncStorage.getItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
||||||
|
return enabled !== null ? enabled === 'true' : DEFAULT_PREFERENCES.notificationEnabled;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取消息推送开关状态失败:', error);
|
||||||
|
return DEFAULT_PREFERENCES.notificationEnabled;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置所有用户偏好设置为默认值
|
* 重置所有用户偏好设置为默认值
|
||||||
*/
|
*/
|
||||||
export const resetUserPreferences = async (): Promise<void> => {
|
export const resetUserPreferences = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.removeItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT);
|
await AsyncStorage.removeItem(PREFERENCES_KEYS.QUICK_WATER_AMOUNT);
|
||||||
|
await AsyncStorage.removeItem(PREFERENCES_KEYS.NOTIFICATION_ENABLED);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重置用户偏好设置失败:', error);
|
console.error('重置用户偏好设置失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user