- 在多个页面添加 isLoggedIn 检查,防止未登录时进行不必要的数据获取 - 使用 React.memo 和 useMemo 优化个人页面徽章渲染性能 - 为 badges API 添加节流机制,避免频繁请求 - 优化图片缓存策略和字符串处理 - 移除调试日志并改进推送通知的认证检查
364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import { getAuthToken } from '@/services/api';
|
||
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;
|
||
}
|
||
|
||
// 检查是否需要注册令牌
|
||
await this.checkAndRegisterToken(token);
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查并注册设备令牌
|
||
* 每次应用启动都会上报token,更新用户ID绑定关系
|
||
*/
|
||
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 {
|
||
// 令牌已注册且未变化
|
||
// 只有在用户已登录的情况下才更新用户ID绑定关系
|
||
const authToken = await getAuthToken();
|
||
if (authToken) {
|
||
await this.updateTokenUserId(token);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('检查和注册设备令牌失败:', error);
|
||
this.config.onError?.(error as Error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新设备令牌的用户ID绑定关系
|
||
* 用户ID从服务端通过header中的token解析获取
|
||
*/
|
||
private async updateTokenUserId(token: string): Promise<boolean> {
|
||
try {
|
||
logger.info('更新设备令牌用户ID绑定:', token.substring(0, 20) + '...');
|
||
|
||
// 调用服务端API更新token用户ID绑定关系
|
||
const response = await pushNotificationService.updateTokenUserId(token);
|
||
|
||
console.log('设备令牌用户ID绑定更新成功:', response.tokenId);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('更新设备令牌用户ID绑定失败:', error);
|
||
// 如果更新用户ID绑定失败,尝试重新注册token
|
||
console.log('尝试重新注册设备令牌...');
|
||
return this.registerDeviceToken(token);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 用户登录成功后调用此方法更新token用户ID绑定
|
||
*/
|
||
async onUserLogin(): Promise<boolean> {
|
||
try {
|
||
const token = this.currentToken || await this.getDeviceToken();
|
||
|
||
if (!token) {
|
||
console.warn('未找到设备令牌,无法更新用户ID绑定');
|
||
return false;
|
||
}
|
||
|
||
return await this.updateTokenUserId(token);
|
||
} catch (error) {
|
||
console.error('用户登录后更新token用户ID绑定失败:', error);
|
||
this.config.onError?.(error as Error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注册设备令牌到后端
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取当前设备令牌
|
||
*/
|
||
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(); |