feat(push): 新增iOS APNs推送通知功能

- 添加推送通知管理器和设备令牌管理
- 实现推送通知权限请求和令牌注册
- 新增推送通知设置页面
- 集成推送通知初始化到应用启动流程
- 添加推送通知API服务和本地存储管理
- 更新个人页面添加推送通知设置入口
This commit is contained in:
richarjiang
2025-10-14 19:25:35 +08:00
parent 435f5cc65c
commit 6cdd2fdf9c
9 changed files with 1263 additions and 2 deletions

View File

@@ -2,6 +2,7 @@ import { ROUTES } from '@/constants/Routes';
import { getNotificationEnabled } from '@/utils/userPreferences';
import * as Notifications from 'expo-notifications';
import { router } from 'expo-router';
import { pushNotificationManager } from './pushNotificationManager';
// 配置通知处理方式
Notifications.setNotificationHandler({
@@ -77,6 +78,24 @@ export class NotificationService {
try {
this.isIniting = true
// 初始化推送通知管理器(包含设备令牌管理)
const pushManagerInitialized = await pushNotificationManager.initialize({
onTokenReceived: (token) => {
console.log('设备令牌已接收:', token.substring(0, 20) + '...');
},
onTokenRefresh: (token) => {
console.log('设备令牌已刷新:', token.substring(0, 20) + '...');
},
onError: (error) => {
console.error('推送通知管理器错误:', error);
}
});
if (!pushManagerInitialized) {
console.warn('推送通知管理器初始化失败,但本地通知功能仍可用');
}
// 请求通知权限
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
@@ -91,7 +110,6 @@ export class NotificationService {
return;
}
// 设置通知监听器
this.setupNotificationListeners();

View File

@@ -0,0 +1,330 @@
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();

View File

@@ -0,0 +1,167 @@
import AsyncStorage from '@/utils/kvStore';
import { api } from './api';
// 设备令牌存储键
const DEVICE_TOKEN_KEY = '@device_token';
const TOKEN_REGISTERED_KEY = '@token_registered';
// 设备令牌注册请求参数
export interface DeviceTokenRequest {
deviceToken: string;
deviceType: 'IOS' | 'ANDROID';
appVersion: string;
osVersion: string;
deviceName: string;
}
// 设备令牌更新请求参数
export interface UpdateTokenRequest {
currentDeviceToken: string;
newDeviceToken: string;
appVersion: string;
osVersion: string;
}
// 设备令牌注销请求参数
export interface UnregisterTokenRequest {
deviceToken: string;
}
// API响应类型
export interface PushNotificationResponse {
success: boolean;
tokenId: string;
}
// API错误响应类型
export interface PushNotificationError {
code: number;
message: string;
data?: any;
}
/**
* 推送通知API服务类
*/
export class PushNotificationService {
private static instance: PushNotificationService;
private constructor() { }
public static getInstance(): PushNotificationService {
if (!PushNotificationService.instance) {
PushNotificationService.instance = new PushNotificationService();
}
return PushNotificationService.instance;
}
/**
* 注册设备令牌
*/
async registerDeviceToken(request: DeviceTokenRequest): Promise<PushNotificationResponse> {
try {
console.log('注册设备令牌:', request.deviceToken.substring(0, 20) + '...');
const response = await api.post<PushNotificationResponse>(
'/push-notifications/register-token',
request
);
// 保存设备令牌到本地存储
await AsyncStorage.setItem(DEVICE_TOKEN_KEY, request.deviceToken);
await AsyncStorage.setItem(TOKEN_REGISTERED_KEY, 'true');
console.log('设备令牌注册成功:', response.tokenId);
return response;
} catch (error) {
console.error('设备令牌注册失败:', error);
throw error;
}
}
/**
* 更新设备令牌
*/
async updateDeviceToken(request: UpdateTokenRequest): Promise<PushNotificationResponse> {
try {
console.log('更新设备令牌:', request.newDeviceToken.substring(0, 20) + '...');
const response = await api.put<PushNotificationResponse>(
'/push-notifications/update-token',
request
);
// 更新本地存储的设备令牌
await AsyncStorage.setItem(DEVICE_TOKEN_KEY, request.newDeviceToken);
console.log('设备令牌更新成功:', response.tokenId);
return response;
} catch (error) {
console.error('设备令牌更新失败:', error);
throw error;
}
}
/**
* 注销设备令牌
*/
async unregisterDeviceToken(deviceToken: string): Promise<void> {
try {
console.log('注销设备令牌:', deviceToken.substring(0, 20) + '...');
await api.delete<void>('/push-notifications/unregister-token', {
body: { deviceToken }
});
// 清除本地存储
await AsyncStorage.removeItem(DEVICE_TOKEN_KEY);
await AsyncStorage.removeItem(TOKEN_REGISTERED_KEY);
console.log('设备令牌注销成功');
} catch (error) {
console.error('设备令牌注销失败:', error);
throw error;
}
}
/**
* 获取本地存储的设备令牌
*/
async getStoredDeviceToken(): Promise<string | null> {
try {
return await AsyncStorage.getItem(DEVICE_TOKEN_KEY);
} catch (error) {
console.error('获取本地设备令牌失败:', error);
return null;
}
}
/**
* 检查设备令牌是否已注册
*/
async isTokenRegistered(): Promise<boolean> {
try {
const registered = await AsyncStorage.getItem(TOKEN_REGISTERED_KEY);
return registered === 'true';
} catch (error) {
console.error('检查令牌注册状态失败:', error);
return false;
}
}
/**
* 清除本地设备令牌数据
*/
async clearLocalTokenData(): Promise<void> {
try {
await AsyncStorage.removeItem(DEVICE_TOKEN_KEY);
await AsyncStorage.removeItem(TOKEN_REGISTERED_KEY);
console.log('本地设备令牌数据已清除');
} catch (error) {
console.error('清除本地令牌数据失败:', error);
}
}
}
// 导出单例实例
export const pushNotificationService = PushNotificationService.getInstance();