feat(notification): migrate from expo-notifications to JPush push service
Replace expo-notifications with JPush for push notifications across the app. This includes adding JPush native module dependencies, configuring the JPush plugin in app.json, implementing jpushService wrapper, and updating notification services to use JPush APIs. Also adds device registration on token refresh for both mobile and web platforms. BREAKING CHANGE: expo-notifications removed in favor of JPush for push notifications
This commit is contained in:
318
src/services/notification/jpushService.ts
Normal file
318
src/services/notification/jpushService.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { Platform, NativeModules } from 'react-native';
|
||||
import { pushService } from './pushService';
|
||||
|
||||
type JPushNotificationMessage = {
|
||||
messageID: string;
|
||||
title: string;
|
||||
content: string;
|
||||
extras: Record<string, string>;
|
||||
notificationType?: string;
|
||||
notificationEventType?: 'notificationArrived' | 'notificationOpened';
|
||||
};
|
||||
|
||||
type JPushCallback = (message: JPushNotificationMessage) => void;
|
||||
|
||||
let JPush: typeof import('jpush-react-native').default | null = null;
|
||||
let jpushAppKey = '';
|
||||
let jpushChannel = 'developer-default';
|
||||
|
||||
try {
|
||||
const nativeModule = NativeModules.JPushModule;
|
||||
if (nativeModule) {
|
||||
JPush = require('jpush-react-native').default;
|
||||
const Constants = require('expo-constants').default;
|
||||
const extra = Constants?.expoConfig?.extra || Constants?.extra || {};
|
||||
jpushAppKey = extra.jpushAppKey || '';
|
||||
jpushChannel = extra.jpushChannel || 'withyou-default';
|
||||
} else {
|
||||
console.warn('[JPush] native module not linked, skipping');
|
||||
}
|
||||
} catch {
|
||||
console.warn('[JPush] jpush-react-native not available');
|
||||
}
|
||||
|
||||
function getAuthStore() {
|
||||
return require('@/stores').useAuthStore;
|
||||
}
|
||||
|
||||
class JPushService {
|
||||
private registrationID: string | null = null;
|
||||
private isInitialized = false;
|
||||
private initPromise: Promise<boolean> | null = null;
|
||||
private notificationCallback: JPushCallback | null = null;
|
||||
private connectResolved = false;
|
||||
private listenersRegistered = false;
|
||||
|
||||
async initialize(): Promise<boolean> {
|
||||
if (this.isInitialized) return true;
|
||||
if (Platform.OS === 'web') return false;
|
||||
if (!JPush) return false;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = this._doInit();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
private _registerListeners(): void {
|
||||
if (this.listenersRegistered) return;
|
||||
this.listenersRegistered = true;
|
||||
|
||||
JPush!.addConnectEventListener((result: { connectEnable: boolean }) => {
|
||||
console.log('[JPush] connect event:', result.connectEnable);
|
||||
if (result.connectEnable && !this.connectResolved) {
|
||||
this.connectResolved = true;
|
||||
this._onConnected();
|
||||
}
|
||||
});
|
||||
|
||||
JPush!.addNotificationListener((result: any) => {
|
||||
console.log('[JPush] notification:', result);
|
||||
if (this.notificationCallback) {
|
||||
this.notificationCallback({
|
||||
messageID: result.messageID || '',
|
||||
title: result.title || '',
|
||||
content: result.content || '',
|
||||
extras: result.extras || {},
|
||||
notificationType: result.extras?.notification_type,
|
||||
notificationEventType: result.notificationEventType,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
JPush!.addCustomMessageListener((result: any) => {
|
||||
console.log('[JPush] custom message:', result);
|
||||
});
|
||||
|
||||
JPush!.addLocalNotificationListener((result: any) => {
|
||||
console.log('[JPush] local notification:', result);
|
||||
});
|
||||
|
||||
JPush!.addTagAliasListener((result: any) => {
|
||||
console.log('[JPush] tag/alias result:', result);
|
||||
});
|
||||
|
||||
JPush!.addCommandEventListener((result: any) => {
|
||||
console.log('[JPush] command result:', result);
|
||||
});
|
||||
}
|
||||
|
||||
private async _doInit(): Promise<boolean> {
|
||||
try {
|
||||
JPush!.setLoggerEnable(true);
|
||||
|
||||
// Register listeners only once (idempotent guard)
|
||||
this._registerListeners();
|
||||
|
||||
// 初始化 SDK
|
||||
JPush!.init({
|
||||
appKey: jpushAppKey,
|
||||
channel: jpushChannel,
|
||||
production: !__DEV__,
|
||||
});
|
||||
|
||||
console.log('[JPush] init called, waiting for registration...');
|
||||
|
||||
// 轮询获取 RegistrationID,间隔逐渐增大
|
||||
const regID = await this._pollRegistrationID();
|
||||
if (regID) {
|
||||
this.registrationID = regID;
|
||||
this.isInitialized = true;
|
||||
console.log('[JPush] initialized with registrationID:', regID);
|
||||
await this.registerDeviceToServer();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 即使没拿到 registrationID,也标记为已初始化
|
||||
// connect 事件回来后会自动获取
|
||||
this.isInitialized = true;
|
||||
console.warn('[JPush] init done but no registrationID yet, will retry on connect');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[JPush] initialization failed:', error);
|
||||
this.initPromise = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _onConnected(): void {
|
||||
if (this.registrationID) return; // Already have registrationID, skip
|
||||
console.log('[JPush] connected, fetching registrationID...');
|
||||
this._fetchRegistrationID();
|
||||
}
|
||||
|
||||
private _fetchRegistrationID(): void {
|
||||
if (!JPush) return;
|
||||
JPush.getRegistrationID((result: any) => {
|
||||
console.log('[JPush] getRegistrationID result:', JSON.stringify(result));
|
||||
const regID = result?.registerID || result?.registrationID;
|
||||
if (regID) {
|
||||
this.registrationID = regID;
|
||||
this.isInitialized = true;
|
||||
console.log('[JPush] got registrationID:', regID);
|
||||
this.registerDeviceToServer();
|
||||
} else {
|
||||
console.warn('[JPush] getRegistrationID returned empty');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _pollRegistrationID(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
// 轮询策略: 2s, 4s, 6s, 8s, 10s (总计 30s)
|
||||
const delays = [2000, 4000, 6000, 8000, 10000];
|
||||
let attempt = 0;
|
||||
|
||||
const tryFetch = () => {
|
||||
if (!JPush) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
JPush.getRegistrationID((result: any) => {
|
||||
const regID = result?.registerID || result?.registrationID;
|
||||
console.log(`[JPush] poll attempt ${attempt + 1}, regID:`, regID || '(empty)');
|
||||
|
||||
if (regID) {
|
||||
resolve(regID);
|
||||
return;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
if (attempt < delays.length) {
|
||||
setTimeout(tryFetch, delays[attempt] - (attempt > 0 ? delays[attempt - 1] : 0));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(tryFetch, delays[0]);
|
||||
});
|
||||
}
|
||||
|
||||
async registerDeviceToServer(): Promise<void> {
|
||||
if (!this.registrationID) return;
|
||||
|
||||
const useAuthStore = getAuthStore();
|
||||
const { isAuthenticated } = useAuthStore.getState();
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
try {
|
||||
const deviceType = Platform.OS === 'ios' ? 'ios' : 'android';
|
||||
const deviceID = await this.getDeviceID();
|
||||
|
||||
await pushService.registerDevice({
|
||||
device_id: deviceID,
|
||||
device_type: deviceType as 'ios' | 'android',
|
||||
push_token: this.registrationID,
|
||||
device_name: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} Device`,
|
||||
});
|
||||
|
||||
console.log('[JPush] device registered to server');
|
||||
} catch (error) {
|
||||
console.error('[JPush] failed to register device to server:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async getDeviceID(): Promise<string> {
|
||||
try {
|
||||
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
||||
const DEVICE_ID_KEY = 'withyou_device_id';
|
||||
let deviceId = await AsyncStorage.getItem(DEVICE_ID_KEY);
|
||||
if (deviceId) return deviceId;
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
deviceId = `${Platform.OS}_${timestamp}_${random}`;
|
||||
await AsyncStorage.setItem(DEVICE_ID_KEY, deviceId);
|
||||
return deviceId;
|
||||
} catch {
|
||||
return `${Platform.OS}_${Date.now()}`;
|
||||
}
|
||||
}
|
||||
|
||||
onNotification(callback: JPushCallback): void {
|
||||
this.notificationCallback = callback;
|
||||
}
|
||||
|
||||
setTags(tags: string[]): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.addTags({ sequence: Date.now(), tags });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setTags failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setAlias(alias: string): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.setAlias({ sequence: Date.now(), alias });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setAlias failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async setAliasForUser(userID: string): Promise<void> {
|
||||
this.setAlias(userID);
|
||||
}
|
||||
|
||||
deleteAlias(): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.deleteAlias({ sequence: Date.now() });
|
||||
} catch (error) {
|
||||
console.error('[JPush] deleteAlias failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getRegistrationIDValue(): string | null {
|
||||
return this.registrationID;
|
||||
}
|
||||
|
||||
isJPushAvailable(): boolean {
|
||||
return this.isInitialized && this.registrationID !== null;
|
||||
}
|
||||
|
||||
addLocalNotification(params: {
|
||||
messageID: string;
|
||||
title: string;
|
||||
content: string;
|
||||
extras: Record<string, string>;
|
||||
}): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.addLocalNotification(params);
|
||||
} catch (error) {
|
||||
console.error('[JPush] addLocalNotification failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllNotifications(): void {
|
||||
if (!JPush || !this.isInitialized) return;
|
||||
try {
|
||||
JPush.clearAllNotifications();
|
||||
} catch (error) {
|
||||
console.error('[JPush] clearAllNotifications failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setBadge(badge: number): void {
|
||||
if (!JPush || !this.isInitialized || Platform.OS !== 'ios') return;
|
||||
try {
|
||||
JPush.setBadge({ badge, appBadge: badge });
|
||||
} catch (error) {
|
||||
console.error('[JPush] setBadge failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.isInitialized = false;
|
||||
this.registrationID = null;
|
||||
this.notificationCallback = null;
|
||||
this.initPromise = null;
|
||||
this.connectResolved = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const jpushService = new JPushService();
|
||||
export default jpushService;
|
||||
Reference in New Issue
Block a user