Files
digital-pilates/services/pushNotificationManager.ts
richarjiang 6cdd2fdf9c feat(push): 新增iOS APNs推送通知功能
- 添加推送通知管理器和设备令牌管理
- 实现推送通知权限请求和令牌注册
- 新增推送通知设置页面
- 集成推送通知初始化到应用启动流程
- 添加推送通知API服务和本地存储管理
- 更新个人页面添加推送通知设置入口
2025-10-14 19:25:35 +08:00

330 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { logger } from '@/utils/logger';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import { DeviceTokenRequest, pushNotificationService } from './pushNotifications';
// 设备令牌管理状态
export enum TokenStatus {
UNKNOWN = 'unknown',
GRANTED = 'granted',
DENIED = 'denied',
REGISTERED = 'registered',
FAILED = 'failed',
}
// 推送通知管理器配置
export interface PushNotificationConfig {
onTokenReceived?: (token: string) => void;
onTokenRefresh?: (token: string) => void;
onError?: (error: Error) => void;
}
/**
* 推送通知管理器类
* 负责iOS推送通知权限请求和设备令牌管理
*/
export class PushNotificationManager {
private static instance: PushNotificationManager;
private isInitialized = false;
private isInitializing = false;
private config: PushNotificationConfig = {};
private currentToken: string | null = null;
private constructor() { }
public static getInstance(): PushNotificationManager {
if (!PushNotificationManager.instance) {
PushNotificationManager.instance = new PushNotificationManager();
}
return PushNotificationManager.instance;
}
/**
* 初始化推送通知管理器
*/
async initialize(config: PushNotificationConfig = {}): Promise<boolean> {
if (this.isInitialized || this.isInitializing) {
return this.isInitialized;
}
try {
this.isInitializing = true;
this.config = config;
console.log('初始化推送通知管理器...');
// 检查设备是否支持推送通知
// 在Expo Go环境中某些推送功能可能受限
if (Platform.OS === 'web') {
console.warn('Web平台不支持推送通知功能');
this.config.onError?.(new Error('Web平台不支持推送通知功能'));
return false;
}
// 配置推送通知处理方式
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
// 请求推送通知权限
const hasPermission = await this.requestPermissions();
if (!hasPermission) {
console.warn('推送通知权限未授予');
return false;
}
// 获取设备令牌
const token = await this.getDeviceToken();
if (!token) {
console.error('获取设备令牌失败');
return false;
}
logger.info('获取到设备令牌:', token);
// 检查是否需要注册令牌
await this.checkAndRegisterToken(token);
// 设置令牌刷新监听器
this.setupTokenRefreshListener();
this.isInitialized = true;
console.log('推送通知管理器初始化成功');
return true;
} catch (error) {
console.error('推送通知管理器初始化失败:', error);
this.config.onError?.(error as Error);
return false;
} finally {
this.isInitializing = false;
}
}
/**
* 请求推送通知权限
*/
async requestPermissions(): Promise<boolean> {
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('推送通知权限未授予:', finalStatus);
return false;
}
console.log('推送通知权限已授予');
return true;
} catch (error) {
console.error('请求推送通知权限失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 获取设备令牌
*/
async getDeviceToken(): Promise<string | null> {
try {
if (Platform.OS === 'ios') {
// iOS使用APNs令牌
const token = await Notifications.getDevicePushTokenAsync();
this.currentToken = token.data;
console.log('获取到iOS设备令牌:', token.data.substring(0, 20) + '...');
return token.data;
} else {
// Android使用FCM令牌虽然项目只支持iOS但保留代码完整性
const token = await Notifications.getDevicePushTokenAsync();
this.currentToken = token.data;
console.log('获取到Android设备令牌:', token.data.substring(0, 20) + '...');
return token.data;
}
} catch (error) {
console.error('获取设备令牌失败:', error);
this.config.onError?.(error as Error);
return null;
}
}
/**
* 检查并注册设备令牌
*/
private async checkAndRegisterToken(token: string): Promise<void> {
try {
const storedToken = await pushNotificationService.getStoredDeviceToken();
const isRegistered = await pushNotificationService.isTokenRegistered();
// 如果令牌已改变或未注册,则重新注册
if (!isRegistered || storedToken !== token) {
await this.registerDeviceToken(token);
} else {
console.log('设备令牌已注册,无需重复注册');
}
} catch (error) {
console.error('检查和注册设备令牌失败:', error);
this.config.onError?.(error as Error);
}
}
/**
* 注册设备令牌到后端
*/
async registerDeviceToken(token?: string): Promise<boolean> {
try {
const deviceToken = token || this.currentToken || await this.getDeviceToken();
if (!deviceToken) {
throw new Error('设备令牌为空');
}
// 构建设备信息
const deviceInfo: DeviceTokenRequest = {
deviceToken,
deviceType: Platform.OS === 'ios' ? 'IOS' : 'ANDROID',
appVersion: Constants.expoConfig?.version || '1.0.0',
osVersion: `${Platform.OS} ${Platform.Version}`,
deviceName: Platform.OS === 'ios' ? 'iOS Device' : 'Android Device',
};
logger.info('设备信息:', deviceInfo);
// 注册到后端
const response = await pushNotificationService.registerDeviceToken(deviceInfo);
console.log('设备令牌注册成功:', response.tokenId);
this.config.onTokenReceived?.(deviceToken);
return true;
} catch (error) {
console.error('注册设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 更新设备令牌
*/
async updateDeviceToken(newToken: string): Promise<boolean> {
try {
const oldToken = await pushNotificationService.getStoredDeviceToken();
if (!oldToken) {
console.warn('未找到旧令牌,将执行新注册');
return this.registerDeviceToken(newToken);
}
const updateRequest = {
currentDeviceToken: oldToken,
newDeviceToken: newToken,
appVersion: Constants.expoConfig?.version || '1.0.0',
osVersion: `${Platform.OS} ${Platform.Version}`,
};
const response = await pushNotificationService.updateDeviceToken(updateRequest);
this.currentToken = newToken;
console.log('设备令牌更新成功:', response.tokenId);
this.config.onTokenRefresh?.(newToken);
return true;
} catch (error) {
console.error('更新设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 注销设备令牌
*/
async unregisterDeviceToken(): Promise<boolean> {
try {
const token = this.currentToken || await pushNotificationService.getStoredDeviceToken();
if (!token) {
console.warn('未找到设备令牌,无需注销');
return true;
}
await pushNotificationService.unregisterDeviceToken(token);
this.currentToken = null;
console.log('设备令牌注销成功');
return true;
} catch (error) {
console.error('注销设备令牌失败:', error);
this.config.onError?.(error as Error);
return false;
}
}
/**
* 设置令牌刷新监听器
*/
private setupTokenRefreshListener(): void {
// 监听令牌变化iOS上通常不会频繁变化
Notifications.addNotificationResponseReceivedListener((response) => {
console.log('收到推送通知响应:', response);
});
}
/**
* 获取当前设备令牌
*/
getCurrentToken(): string | null {
return this.currentToken;
}
/**
* 获取令牌状态
*/
async getTokenStatus(): Promise<TokenStatus> {
try {
const hasPermission = await this.requestPermissions();
if (!hasPermission) {
return TokenStatus.DENIED;
}
const isRegistered = await pushNotificationService.isTokenRegistered();
if (isRegistered) {
return TokenStatus.REGISTERED;
}
const token = await this.getDeviceToken();
return token ? TokenStatus.GRANTED : TokenStatus.FAILED;
} catch (error) {
console.error('获取令牌状态失败:', error);
return TokenStatus.FAILED;
}
}
/**
* 清除所有本地数据
*/
async clearAllData(): Promise<void> {
try {
await pushNotificationService.clearLocalTokenData();
this.currentToken = null;
this.isInitialized = false;
console.log('推送通知管理器数据已清除');
} catch (error) {
console.error('清除推送通知管理器数据失败:', error);
}
}
}
// 导出单例实例
export const pushNotificationManager = PushNotificationManager.getInstance();