import messaging, { FirebaseMessagingTypes, } from '@react-native-firebase/messaging'; import notifee, { AndroidImportance, AndroidVisibility, AuthorizationStatus, EventType, } from '@notifee/react-native'; import { Platform } from 'react-native'; const CHANNEL_ID = 'convex_crm_default'; const CHANNEL_NAME = 'Convex CRM Notifications'; const CHANNEL_DESCRIPTION = 'General push notifications for Convex CRM'; export interface NotificationData { [key: string]: string; } export type MessageHandler = ( message: FirebaseMessagingTypes.RemoteMessage, ) => void; export type NotificationOpenHandler = (data: NotificationData) => void; export async function requestPermission(): Promise { try { const settings = await notifee.requestPermission(); const granted = settings.authorizationStatus === AuthorizationStatus.AUTHORIZED || settings.authorizationStatus === AuthorizationStatus.PROVISIONAL; console.log('[NotificationService] Permission granted:', granted); return granted; } catch (error) { console.error('[NotificationService] requestPermission error:', error); return false; } } export async function createDefaultChannel(): Promise { if (Platform.OS !== 'android') { return; } try { await notifee.createChannel({ id: CHANNEL_ID, name: CHANNEL_NAME, description: CHANNEL_DESCRIPTION, importance: AndroidImportance.HIGH, visibility: AndroidVisibility.PUBLIC, sound: 'default', vibration: true, vibrationPattern: [300, 500], lights: true, lightColor: '#4F46E5', // indigo accent }); console.log('[NotificationService] Default Android channel created:', CHANNEL_ID); } catch (error) { console.error('[NotificationService] createDefaultChannel error:', error); } } export async function getFCMToken(): Promise { try { // Ensure FCM is registered to receive messages if (!messaging().isDeviceRegisteredForRemoteMessages) { await messaging().registerDeviceForRemoteMessages(); } const token = await messaging().getToken(); console.log('[NotificationService] FCM Token:', token); return token; } catch (error) { console.error('[NotificationService] getFCMToken error:', error); return null; } } /** * Subscribe to token-refresh events. * Call this once; the returned function unsubscribes when invoked. */ export function onTokenRefresh( callback: (token: string) => void, ): () => void { const unsubscribe = messaging().onTokenRefresh(newToken => { console.log('[NotificationService] FCM token refreshed:', newToken); callback(newToken); }); return unsubscribe; } export async function displayNotification( title: string, body: string, data: NotificationData = {}, ): Promise { try { await notifee.displayNotification({ title, body, data, android: { channelId: CHANNEL_ID, importance: AndroidImportance.HIGH, smallIcon: 'ic_notification', // must exist in android/app/src/main/res/drawable pressAction: { id: 'default' }, color: '#4F46E5', }, ios: { sound: 'default', foregroundPresentationOptions: { alert: true, badge: true, sound: true, }, }, }); } catch (error) { console.error('[NotificationService] displayNotification error:', error); } } export function onForegroundMessage( onOpen?: NotificationOpenHandler, ): () => void { // FCM foreground listener const unsubscribeFCM = messaging().onMessage( async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { console.log('[NotificationService] Foreground FCM message:', remoteMessage); const title = remoteMessage.notification?.title ?? 'Convex CRM'; const body = remoteMessage.notification?.body ?? 'You have a new notification'; const data = (remoteMessage.data ?? {}) as NotificationData; await displayNotification(title, body, data); }, ); // Notifee foreground event listener (handles taps on Notifee notifications) const unsubscribeNotifee = notifee.onForegroundEvent(({ type, detail }) => { if (type === EventType.PRESS && detail.notification?.data) { console.log( '[NotificationService] Notifee notification pressed (foreground):', detail.notification.data, ); onOpen?.(detail.notification.data as NotificationData); } }); return () => { unsubscribeFCM(); unsubscribeNotifee(); }; } export function registerBackgroundHandler(): void { messaging().setBackgroundMessageHandler( async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => { console.log( '[NotificationService] Background FCM message:', remoteMessage, ); // FCM data-only messages in the background need manual display if (remoteMessage.data && !remoteMessage.notification) { const title = (remoteMessage.data.title as string) ?? 'Convex CRM'; const body = (remoteMessage.data.body as string) ?? 'You have a new notification'; await displayNotification(title, body, remoteMessage.data as NotificationData); } }, ); // Notifee background event (handles taps on notifications from background) notifee.onBackgroundEvent(async ({ type, detail }) => { if (type === EventType.PRESS) { console.log( '[NotificationService] Notifee notification pressed (background):', detail.notification?.data, ); } if (type === EventType.DISMISSED) { console.log( '[NotificationService] Notification dismissed (background)', ); } }); } /** * Subscribe to FCM notification taps when the app is in the background * (but not fully quit). Returns an unsubscribe function. */ export function onNotificationOpenedApp( callback: NotificationOpenHandler, ): () => void { const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => { console.log( '[NotificationService] App opened from background notification:', remoteMessage, ); callback((remoteMessage.data ?? {}) as NotificationData); }); return unsubscribe; } /** * Check whether the app was launched by tapping a notification * while it was fully quit. Call once on app startup. */ export async function getInitialNotification( callback: NotificationOpenHandler, ): Promise { try { const remoteMessage = await messaging().getInitialNotification(); if (remoteMessage) { console.log( '[NotificationService] App launched from quit-state notification:', remoteMessage, ); callback((remoteMessage.data ?? {}) as NotificationData); } } catch (error) { console.error('[NotificationService] getInitialNotification error:', error); } } /** * Set the app icon badge count (iOS only; no-op on Android). */ export async function setBadgeCount(count: number): Promise { try { await notifee.setBadgeCount(count); } catch (error) { console.error('[NotificationService] setBadgeCount error:', error); } } /** * Clear the app icon badge (iOS only; no-op on Android). */ export async function clearBadge(): Promise { try { await notifee.setBadgeCount(0); } catch (error) { console.error('[NotificationService] clearBadge error:', error); } } export interface InitializeOptions { onNotificationOpen?: NotificationOpenHandler; onTokenRefreshed?: (token: string) => void; } export async function initialize( options: InitializeOptions = {}, ): Promise<() => void> { const { onNotificationOpen, onTokenRefreshed } = options; // 1. Permission await requestPermission(); // 2. Android channel await createDefaultChannel(); // 3. FCM token await getFCMToken(); // 4. Token refresh const unsubscribeTokenRefresh = onTokenRefresh(token => { onTokenRefreshed?.(token); }); // 5. Foreground messages const unsubscribeForeground = onForegroundMessage(onNotificationOpen); // 6. Background tap const unsubscribeBackgroundOpen = onNotificationOpenedApp(data => { onNotificationOpen?.(data); }); // 7. Quit-state launch await getInitialNotification(data => { onNotificationOpen?.(data); }); // Return aggregate cleanup return () => { unsubscribeTokenRefresh(); unsubscribeForeground(); unsubscribeBackgroundOpen(); }; } export const NotificationService = { requestPermission, createDefaultChannel, getFCMToken, onTokenRefresh, displayNotification, onForegroundMessage, registerBackgroundHandler, onNotificationOpenedApp, getInitialNotification, setBadgeCount, clearBadge, initialize, };