358 lines
12 KiB
TypeScript
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);
|
|
}
|