/** * FCM Service - Platform-aware push notification handling * * Native (Android/iOS): Uses @react-native-firebase/messaging * Web: Uses Firebase JS SDK with service workers (limited support) */ import { Platform } from 'react-native'; import { doc, FieldValue, isWeb, } from '../firebase'; // Only import native messaging on native platforms let messaging: any = null; if (Platform.OS !== 'web') { messaging = require('@react-native-firebase/messaging').default; } export class FCMService { private static tokenRefreshUnsubscribe: (() => void) | null = null; /** * Check if FCM is supported on current platform */ static isSupported(): boolean { // FCM is fully supported on Android // Limited support on web (requires service worker setup) // iOS uses APNs through FCM return Platform.OS === 'android' || Platform.OS === 'ios'; } /** * Request notification permissions */ static async requestPermission(): Promise { // Web handling if (Platform.OS === 'web') { if (typeof window === 'undefined' || !('Notification' in window)) { console.log('Notifications not supported in this browser'); return false; } try { const permission = await Notification.requestPermission(); return permission === 'granted'; } catch (error) { console.error('Error requesting notification permission:', error); return false; } } // Native handling (Android/iOS) if (!messaging) { return false; } try { const authStatus = await messaging().requestPermission(); const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL; if (!enabled) { console.warn('User notification permission denied'); } return enabled; } catch (error) { console.error('Error requesting notification permission:', error); return false; } } /** * Get the FCM token */ static async getToken(): Promise { // Web handling if (Platform.OS === 'web') { // Web FCM requires service worker setup // For now, return null and log a message console.log('Web push notifications require additional setup (service worker, VAPID key)'); return null; } // Native handling if (!messaging) { return null; } try { const token = await messaging().getToken(); console.log('FCM Token retrieved:', token); return token; } catch (error) { console.error('Error getting FCM token:', error); return null; } } /** * Save FCM token to Firestore user document during user creation */ static async saveTokenToFirestoreOnCreate(uid: string, token: string): Promise<{ success: boolean; error?: string }> { try { const userDocRef = doc('users', uid); await userDocRef.update({ fcmToken: token, fcmTokenUpdatedAt: FieldValue.serverTimestamp(), }); console.log('FCM token saved to Firestore during user creation for user:', uid); return { success: true }; } catch (error) { console.error('Error saving FCM token to Firestore during creation:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to save FCM token', }; } } /** * Update FCM token in Firestore user document */ static async updateTokenInFirestore(uid: string, token: string): Promise<{ success: boolean; error?: string }> { try { const userDocRef = doc('users', uid); const userDoc = await userDocRef.get(); if (!userDoc.exists) { console.log('User document does not exist yet, skipping token update for:', uid); return { success: true }; } await userDocRef.update({ fcmToken: token, fcmTokenUpdatedAt: FieldValue.serverTimestamp(), }); console.log('FCM token updated in Firestore for user:', uid); return { success: true }; } catch (error: any) { console.error('Error updating FCM token in Firestore:', error); if (error?.code === 'firestore/not-found') { console.log('User document not found, skipping token update'); return { success: true }; } return { success: false, error: error instanceof Error ? error.message : 'Failed to update FCM token', }; } } /** * Remove FCM token from Firestore (on logout) */ static async removeTokenFromFirestore(uid: string): Promise<{ success: boolean; error?: string }> { try { const userDocRef = doc('users', uid); await userDocRef.update({ fcmToken: FieldValue.delete(), fcmTokenUpdatedAt: FieldValue.serverTimestamp(), }); console.log('FCM token removed from Firestore for user:', uid); return { success: true }; } catch (error) { console.error('Error removing FCM token from Firestore:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to remove FCM token', }; } } /** * Initialize FCM token for a new user (during user creation) */ static async initializeTokenForNewUser(uid: string): Promise<{ success: boolean; error?: string; token?: string }> { // Skip on web if (Platform.OS === 'web') { console.log('Skipping FCM token initialization on web'); return { success: true }; } try { const hasPermission = await this.requestPermission(); if (!hasPermission) { console.warn('Notification permission not granted, continuing anyway...'); } const token = await this.getToken(); if (!token) { return { success: false, error: 'Failed to retrieve FCM token' }; } const saveResult = await this.saveTokenToFirestoreOnCreate(uid, token); if (!saveResult.success) { return saveResult; } this.setupTokenRefreshListener(uid); return { success: true, token }; } catch (error) { console.error('Error initializing FCM token for new user:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to initialize FCM token', }; } } /** * Update FCM token for an existing user */ static async updateTokenForExistingUser(uid: string): Promise<{ success: boolean; error?: string }> { // Skip on web if (Platform.OS === 'web') { return { success: true }; } try { const hasPermission = await this.requestPermission(); if (!hasPermission) { console.warn('Notification permission not granted, continuing anyway...'); } const token = await this.getToken(); if (!token) { return { success: false, error: 'Failed to retrieve FCM token' }; } const updateResult = await this.updateTokenInFirestore(uid, token); if (!updateResult.success) { return updateResult; } this.setupTokenRefreshListener(uid); return { success: true }; } catch (error) { console.error('Error updating FCM token:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to update FCM token', }; } } /** * Set up a listener for FCM token refresh (native only) */ static setupTokenRefreshListener(uid: string): void { if (Platform.OS === 'web' || !messaging) { return; } if (this.tokenRefreshUnsubscribe) { this.tokenRefreshUnsubscribe(); this.tokenRefreshUnsubscribe = null; } this.tokenRefreshUnsubscribe = messaging().onTokenRefresh(async (token: string) => { console.log('FCM token refreshed:', token); await this.updateTokenInFirestore(uid, token); }); } /** * Clean up token refresh listener */ static cleanup(): void { if (this.tokenRefreshUnsubscribe) { this.tokenRefreshUnsubscribe(); this.tokenRefreshUnsubscribe = null; } } /** * Set up foreground message handler (native only) */ static setupForegroundMessageHandler(): () => void { if (Platform.OS === 'web' || !messaging) { return () => { }; } return messaging().onMessage(async (remoteMessage: any) => { console.log('Foreground FCM message received:', remoteMessage); if (remoteMessage.notification) { console.log('Notification:', remoteMessage.notification); } }); } /** * Set up background message handler (native only) */ static setupBackgroundMessageHandler(): void { if (Platform.OS === 'web' || !messaging) { return; } messaging().setBackgroundMessageHandler(async (remoteMessage: any) => { console.log('Background FCM message received:', remoteMessage); }); } /** * Get the initial notification when app is opened from a notification */ static async getInitialNotification(): Promise { if (Platform.OS === 'web' || !messaging) { return null; } try { const remoteMessage = await messaging().getInitialNotification(); if (remoteMessage) { console.log('App opened from notification:', remoteMessage); } return remoteMessage; } catch (error) { console.error('Error getting initial notification:', error); return null; } } /** * Set up notification opened handler */ static setupNotificationOpenedHandler(callback: (remoteMessage: any) => void): () => void { if (Platform.OS === 'web' || !messaging) { return () => { }; } return messaging().onNotificationOpenedApp((remoteMessage: any) => { console.log('Notification opened app:', remoteMessage); callback(remoteMessage); }); } }