import { Platform, NativeModules } from 'react-native'; import { pushService } from './pushService'; type JPushNotificationMessage = { messageID: string; title: string; content: string; extras: Record; 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 | null = null; private notificationCallback: JPushCallback | null = null; private connectResolved = false; private listenersRegistered = false; async initialize(): Promise { 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 { 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 { 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 { 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 { 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 { 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; }): 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;