/** * FCM (Firebase Cloud Messaging) Utilities * Handles FCM token management, messaging, and backend communication */ import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import { Platform } from 'react-native'; import { requestNotificationPermission, checkNotificationPermission, needsNotificationPermission, showPermissionExplanation, showSettingsDialog, PermissionResult } from './permissionUtils'; import { RESULTS } from 'react-native-permissions'; import { showLocalNotification } from './notificationUtils'; import { ALLOWED_DOMAIN, FCM_REGISTER_ENDPOINT, NAVIGATION_DELAYS } from './constants'; /** * Interface for FCM token data */ export interface FCMTokenData { token: string; deviceName: string; deviceType: string; appVersion: string; osVersion: string; sessionCookies: string | null; } /** * Interface for notification callback functions */ export interface NotificationCallbacks { onForegroundNotification: (data: any) => void; onBackgroundNotification: (data: any) => void; onKilledAppNotification: (data: any) => void; } /** * FCM Handler Class */ export class FCMHandler { private getWebViewCookies: () => Promise; private callbacks: NotificationCallbacks; constructor( getWebViewCookies: () => Promise, callbacks: NotificationCallbacks ) { this.getWebViewCookies = getWebViewCookies; this.callbacks = callbacks; } /** * Initialize FCM notifications */ async initializeNotifications(): Promise { try { console.log('Initializing notifications...'); // Request permission first await this.requestUserPermission(); // Then get FCM token await this.getFcmToken(); console.log('Notification initialization completed'); } catch (error) { console.error('Error initializing notifications:', error); } } /** * Request user permission for notifications */ async requestUserPermission(): Promise { try { console.log('Checking notification permission requirements...'); // Check if permission is needed based on platform and version const needsPermission = needsNotificationPermission(); console.log('Permission needed:', needsPermission); if (!needsPermission) { console.log('No explicit permission required for this Android version'); return; } // Check current permission status first const currentStatus = await checkNotificationPermission(); console.log('Current permission status:', currentStatus); if (currentStatus.granted) { console.log('Notification permission already granted'); return; } // Show explanation dialog for Android users if (Platform.OS === 'android') { const userWantsPermission = await showPermissionExplanation(); if (!userWantsPermission) { console.log('User declined permission request'); return; } } // Request permission const result: PermissionResult = await requestNotificationPermission(); console.log('📋 Permission request result:', JSON.stringify(result, null, 2)); if (result.granted) { console.log('✅ Notification permission granted successfully'); } else { console.log('❌ Notification permission denied'); console.log('📋 Denial details:', { status: result.status, platform: result.platform, androidVersion: result.androidVersion }); // Check for different denial scenarios if (Platform.OS === 'android') { if (result.status === RESULTS.DENIED) { console.log('🚫 Permission denied by user'); showSettingsDialog(); } else if (result.status === RESULTS.BLOCKED) { console.log('🚫 Permission blocked by user'); showSettingsDialog(); } else if (result.status === RESULTS.UNAVAILABLE) { console.log('⚠️ Permission not available on this device'); } else { console.log('❓ Unknown permission status:', result.status); } } } } catch (error) { console.error('Error requesting permission:', error); } } /** * Get FCM token */ async getFcmToken(): Promise { try { console.log('🔑 Attempting to get FCM token...'); // Check if we have permission before trying to get token const permissionStatus = await checkNotificationPermission(); console.log('🔍 Permission status for token:', JSON.stringify(permissionStatus, null, 2)); if (!permissionStatus.granted && needsNotificationPermission()) { console.log('❌ Cannot get FCM token: notification permission not granted'); return; } console.log('✅ Permission check passed, requesting FCM token...'); const token = await messaging().getToken(); console.log('🎫 FCM Token received:', token ? 'YES' : 'NO'); if (token) { console.log('📤 FCM Token:', token); // Send FCM token to Odoo backend await this.sendTokenToBackend(token); } else { console.log('⚠️ No FCM token available'); } } catch (err) { console.error('❌ Error getting FCM token:', err); } } /** * Send FCM token to backend * @param token - FCM token to send */ async sendTokenToBackend(token: string): Promise { try { // Import react-native-device-info const DeviceInfo = require('react-native-device-info'); // Get device information const deviceName = await DeviceInfo.getDeviceName(); const deviceType = Platform.OS; const appVersion = await DeviceInfo.getVersion(); const osVersion = await DeviceInfo.getSystemVersion(); // Get session cookies from WebView const sessionCookies = await this.getWebViewCookies(); console.log('📤 Sending FCM token to backend:', { token: token ? token.substring(0, 20) + '...' : 'NO', deviceName, deviceType, appVersion, osVersion, hasSessionCookies: !!sessionCookies }); if (!sessionCookies) { console.warn('⚠️ No session cookies found - user may not be logged in'); return; } const response = await fetch(`${ALLOWED_DOMAIN}${FCM_REGISTER_ENDPOINT}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cookie': sessionCookies, }, body: JSON.stringify({ token: token, device_name: deviceName, device_type: deviceType, app_version: appVersion, os_version: osVersion, }), }); if (response.ok) { const result = await response.json(); console.log('✅ FCM token registered successfully:', result); } else { console.error('❌ Failed to register FCM token:', response.status, response.statusText); } } catch (err) { console.error('❌ Error sending token to backend:', err); } } /** * Handle FCM messages when app is in foreground * @param remoteMessage - FCM message received * @param appState - Current app state */ handleForegroundMessage( remoteMessage: FirebaseMessagingTypes.RemoteMessage, appState: string ): void { console.log('📨 FCM Message received:', JSON.stringify(remoteMessage, null, 2)); console.log('📱 Current app state:', appState); // Always show notification in foreground - FCM doesn't show notifications automatically in foreground console.log('📤 Showing foreground notification'); if (remoteMessage.notification) { console.log('📤 Showing notification with payload'); showLocalNotification({ title: remoteMessage.notification.title || 'New Message', message: remoteMessage.notification.body || 'You have a new message', }); // For foreground: Update WebView URL directly based on notification data if (remoteMessage.data) { console.log('🔗 Processing notification data for WebView URL update (foreground):', remoteMessage.data); setTimeout(() => { this.callbacks.onForegroundNotification(remoteMessage.data); }, NAVIGATION_DELAYS.FOREGROUND); } } else if (remoteMessage.data) { console.log('📤 Showing notification with data only'); // Extract notification info from data payload const title = String(remoteMessage.data.title || 'Odoo Chat'); const body = String(remoteMessage.data.body || 'New message received'); showLocalNotification({ title: title, message: body, }); // For foreground: Update WebView URL directly based on notification data console.log('🔗 Processing data payload for WebView URL update (foreground):', remoteMessage.data); setTimeout(() => { this.callbacks.onForegroundNotification(remoteMessage.data); }, NAVIGATION_DELAYS.FOREGROUND); } else { console.log('⚠️ No notification content available'); } } /** * Handle FCM messages when app is opened from background * @param remoteMessage - FCM message received */ handleBackgroundMessage(remoteMessage: FirebaseMessagingTypes.RemoteMessage): void { if (remoteMessage?.data) { console.log('🔗 App opened from background with notification (using WebView URL update):', remoteMessage.data); // For background state: Update WebView URL directly setTimeout(() => { this.callbacks.onBackgroundNotification(remoteMessage.data); }, NAVIGATION_DELAYS.BACKGROUND); } } /** * Handle FCM messages when app is opened from quit state * @param remoteMessage - FCM message received */ handleKilledAppMessage(remoteMessage: FirebaseMessagingTypes.RemoteMessage): void { if (remoteMessage?.data) { console.log('🔗 App opened from quit state with notification (using deep linking):', remoteMessage.data); // For quit state: Use proper deep linking approach setTimeout(() => { this.callbacks.onKilledAppNotification(remoteMessage.data); }, NAVIGATION_DELAYS.KILLED_APP); } } /** * Setup FCM message listeners * @param appState - Current app state * @returns Function to unsubscribe from listeners */ setupMessageListeners(appState: string): () => void { // Handle FCM messages when app is in foreground const unsubscribe = messaging().onMessage(async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { this.handleForegroundMessage(remoteMessage, appState); }); // Handle background/quit state notifications with deep linking messaging() .getInitialNotification() .then((remoteMessage: FirebaseMessagingTypes.RemoteMessage | null) => { if (remoteMessage) { this.handleKilledAppMessage(remoteMessage); } }); messaging().onNotificationOpenedApp((remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { this.handleBackgroundMessage(remoteMessage); }); // Listen for token refresh const unsubscribeTokenRefresh = messaging().onTokenRefresh(async (token) => { console.log('🔄 FCM Token refreshed:', token); await this.sendTokenToBackend(token); }); return () => { unsubscribe(); unsubscribeTokenRefresh(); }; } } /** * Create FCM handler instance */ export function createFCMHandler( getWebViewCookies: () => Promise, callbacks: NotificationCallbacks ): FCMHandler { return new FCMHandler(getWebViewCookies, callbacks); }