558 lines
16 KiB
TypeScript
558 lines
16 KiB
TypeScript
import { getNotificationEnabled } from '@/utils/userPreferences';
|
||
import * as Notifications from 'expo-notifications';
|
||
import { router } from 'expo-router';
|
||
|
||
// 配置通知处理方式
|
||
Notifications.setNotificationHandler({
|
||
handleNotification: async () => ({
|
||
shouldShowAlert: true,
|
||
shouldPlaySound: true,
|
||
shouldSetBadge: true,
|
||
shouldShowBanner: true,
|
||
shouldShowList: true,
|
||
}),
|
||
});
|
||
|
||
export interface NotificationData {
|
||
title: string;
|
||
body: string;
|
||
data?: Record<string, unknown>;
|
||
sound?: boolean;
|
||
priority?: 'default' | 'normal' | 'high';
|
||
vibrate?: number[];
|
||
}
|
||
|
||
interface DateTrigger {
|
||
type: Notifications.SchedulableTriggerInputTypes.DATE;
|
||
date: number;
|
||
}
|
||
|
||
interface RepeatingTrigger {
|
||
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL;
|
||
seconds: number;
|
||
repeats?: boolean;
|
||
}
|
||
|
||
interface DailyTrigger {
|
||
type: Notifications.SchedulableTriggerInputTypes.DAILY;
|
||
hour: number;
|
||
minute: number;
|
||
}
|
||
|
||
interface WeeklyTrigger {
|
||
type: Notifications.SchedulableTriggerInputTypes.WEEKLY;
|
||
hour: number;
|
||
minute: number;
|
||
weekday: number;
|
||
}
|
||
|
||
interface MonthlyTrigger {
|
||
type: Notifications.SchedulableTriggerInputTypes.MONTHLY;
|
||
hour: number;
|
||
minute: number;
|
||
day: number;
|
||
}
|
||
|
||
type CalendarTrigger = DailyTrigger | WeeklyTrigger | MonthlyTrigger;
|
||
|
||
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();
|
||
|
||
// 检查已存在的通知
|
||
await this.getAllScheduledNotifications();
|
||
|
||
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;
|
||
|
||
console.log('处理通知点击:', data);
|
||
|
||
// 根据通知类型处理不同的逻辑
|
||
if (data?.type === 'workout_reminder') {
|
||
// 处理运动提醒
|
||
console.log('用户点击了运动提醒通知');
|
||
} else if (data?.type === 'goal_achievement') {
|
||
// 处理目标达成通知
|
||
console.log('用户点击了目标达成通知');
|
||
} else if (data?.type === 'mood_checkin') {
|
||
// 处理心情打卡提醒
|
||
console.log('用户点击了心情打卡提醒');
|
||
} else if (data?.type === 'goal_reminder') {
|
||
// 处理目标提醒通知
|
||
console.log('用户点击了目标提醒通知', data);
|
||
// 这里可以添加导航到目标页面的逻辑
|
||
} else if (data?.type === 'lunch_reminder') {
|
||
// 处理午餐提醒通知
|
||
console.log('用户点击了午餐提醒通知', data);
|
||
// 跳转到营养记录页面
|
||
if (data?.url) {
|
||
router.push(data.url as any);
|
||
}
|
||
} else if (data?.type === 'dinner_reminder') {
|
||
// 处理晚餐提醒通知
|
||
console.log('用户点击了晚餐提醒通知', data);
|
||
// 跳转到营养记录页面
|
||
if (data?.url) {
|
||
router.push(data.url as any);
|
||
}
|
||
} else if (data?.type === 'mood_reminder') {
|
||
// 处理心情提醒通知
|
||
console.log('用户点击了心情提醒通知', data);
|
||
// 跳转到心情页面
|
||
if (data?.url) {
|
||
router.push(data.url as any);
|
||
}
|
||
} else if (data?.type === 'water_reminder' || data?.type === 'regular_water_reminder') {
|
||
// 处理喝水提醒通知
|
||
console.log('用户点击了喝水提醒通知', data);
|
||
// 跳转到统计页面查看喝水进度
|
||
if (data?.url) {
|
||
router.push(data.url as any);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否允许推送通知(不包括系统权限检查,仅检查用户偏好)
|
||
*/
|
||
private async isNotificationAllowed(): Promise<boolean> {
|
||
try {
|
||
// 检查用户偏好设置中的推送开关
|
||
const userPreferenceEnabled = await getNotificationEnabled();
|
||
if (!userPreferenceEnabled) {
|
||
console.log('用户已在偏好设置中关闭推送通知');
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('检查推送权限失败:', error);
|
||
// 如果检查失败,默认允许(避免阻塞重要的健康提醒)
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 完整的权限检查(包括系统权限和用户偏好)
|
||
*/
|
||
private async hasFullNotificationPermission(): Promise<boolean> {
|
||
try {
|
||
// 检查系统权限
|
||
const permissionStatus = await this.getPermissionStatus();
|
||
if (permissionStatus !== 'granted') {
|
||
console.log('系统推送权限未授予:', permissionStatus);
|
||
return false;
|
||
}
|
||
|
||
// 检查用户偏好
|
||
const userPreferenceEnabled = await this.isNotificationAllowed();
|
||
if (!userPreferenceEnabled) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('完整权限检查失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送本地推送通知
|
||
*/
|
||
async scheduleLocalNotification(
|
||
notification: NotificationData,
|
||
trigger?: Notifications.NotificationTriggerInput
|
||
): Promise<string> {
|
||
try {
|
||
// 检查完整权限(系统权限 + 用户偏好)
|
||
const hasPermission = await this.hasFullNotificationPermission();
|
||
if (!hasPermission) {
|
||
console.log('推送通知被系统权限或用户偏好设置阻止,跳过发送');
|
||
return 'blocked_by_permission_or_preference';
|
||
}
|
||
|
||
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> {
|
||
console.log('📱 准备发送立即通知:', notification.title);
|
||
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: {
|
||
type: Notifications.SchedulableTriggerInputTypes.DATE,
|
||
date: date.getTime(),
|
||
} as DateTrigger,
|
||
});
|
||
|
||
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: {
|
||
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
|
||
seconds: totalSeconds,
|
||
repeats: true,
|
||
} as RepeatingTrigger,
|
||
});
|
||
|
||
console.log('重复通知已安排,ID:', notificationId);
|
||
return notificationId;
|
||
} catch (error) {
|
||
console.error('安排重复通知失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安排日历重复通知(支持每日、每周、每月)
|
||
*/
|
||
async scheduleCalendarRepeatingNotification(
|
||
notification: NotificationData,
|
||
options: {
|
||
type: Notifications.SchedulableTriggerInputTypes;
|
||
hour: number;
|
||
minute: number;
|
||
weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型
|
||
dayOfMonth?: number; // 1-31,仅用于monthly类型
|
||
}
|
||
): Promise<string> {
|
||
try {
|
||
let trigger: CalendarTrigger;
|
||
|
||
switch (options.type) {
|
||
case Notifications.SchedulableTriggerInputTypes.DAILY:
|
||
trigger = {
|
||
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||
hour: options.hour,
|
||
minute: options.minute,
|
||
};
|
||
break;
|
||
case Notifications.SchedulableTriggerInputTypes.WEEKLY:
|
||
if (options.weekdays && options.weekdays.length > 0) {
|
||
trigger = {
|
||
type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
|
||
hour: options.hour,
|
||
minute: options.minute,
|
||
weekday: options.weekdays[0], // Expo只支持单个weekday
|
||
};
|
||
} else {
|
||
trigger = {
|
||
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
||
hour: options.hour,
|
||
minute: options.minute,
|
||
};
|
||
}
|
||
break;
|
||
case Notifications.SchedulableTriggerInputTypes.MONTHLY:
|
||
trigger = {
|
||
type: Notifications.SchedulableTriggerInputTypes.MONTHLY,
|
||
hour: options.hour,
|
||
minute: options.minute,
|
||
day: options.dayOfMonth || 1,
|
||
};
|
||
break;
|
||
default:
|
||
throw new Error('不支持的重复类型');
|
||
}
|
||
|
||
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,
|
||
});
|
||
|
||
console.log(`${options.type}重复通知已安排,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',
|
||
LUNCH_REMINDER: 'lunch_reminder',
|
||
DINNER_REMINDER: 'dinner_reminder',
|
||
MOOD_REMINDER: 'mood_reminder',
|
||
WATER_REMINDER: 'water_reminder',
|
||
REGULAR_WATER_REMINDER: 'regular_water_reminder',
|
||
} 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);
|
||
}
|
||
};
|