T4B_Chat/utilities/fcmUtils.ts

358 lines
12 KiB
TypeScript

/**
* 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<string | null>;
private callbacks: NotificationCallbacks;
constructor(
getWebViewCookies: () => Promise<string | null>,
callbacks: NotificationCallbacks
) {
this.getWebViewCookies = getWebViewCookies;
this.callbacks = callbacks;
}
/**
* Initialize FCM notifications
*/
async initializeNotifications(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<string | null>,
callbacks: NotificationCallbacks
): FCMHandler {
return new FCMHandler(getWebViewCookies, callbacks);
}