263 lines
7.4 KiB
TypeScript
263 lines
7.4 KiB
TypeScript
import messaging, {
|
||
FirebaseMessagingTypes,
|
||
} from '@react-native-firebase/messaging';
|
||
import notifee, {
|
||
AndroidImportance,
|
||
AuthorizationStatus,
|
||
EventType,
|
||
} from '@notifee/react-native';
|
||
import { Platform } from 'react-native';
|
||
import { navigateToLeadDetails } from '@utils';
|
||
|
||
const CHANNEL_ID = 'convex_crm_default';
|
||
const CHANNEL_NAME = 'Convex CRM Notifications';
|
||
|
||
export interface NotificationData {
|
||
[key: string]: string;
|
||
}
|
||
|
||
export type MessageHandler = (
|
||
message: FirebaseMessagingTypes.RemoteMessage,
|
||
) => void;
|
||
|
||
export type NotificationOpenHandler = (data: NotificationData) => void;
|
||
|
||
export async function requestPermission(): Promise<boolean> {
|
||
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<void> {
|
||
if (Platform.OS !== 'android') {
|
||
return;
|
||
}
|
||
try {
|
||
await notifee.createChannel({
|
||
id: CHANNEL_ID,
|
||
name: CHANNEL_NAME,
|
||
importance: AndroidImportance.HIGH,
|
||
});
|
||
} catch (error) {
|
||
console.error('[NotificationService] createDefaultChannel error:', error);
|
||
}
|
||
}
|
||
|
||
export async function getFCMToken(): Promise<string | null> {
|
||
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<void> {
|
||
try {
|
||
await notifee.displayNotification({
|
||
title,
|
||
body,
|
||
data,
|
||
android: {
|
||
channelId: CHANNEL_ID,
|
||
pressAction: { id: 'default' },
|
||
smallIcon: 'ic_notification',
|
||
color: '#07BAD2', // Brand teal – sampled from app logo
|
||
},
|
||
});
|
||
} catch (error) {
|
||
console.error('[NotificationService] displayNotification error:', error);
|
||
}
|
||
}
|
||
|
||
export function onForegroundMessage(onOpen?: NotificationOpenHandler): () => void {
|
||
// Show a Notifee notification for every incoming FCM message
|
||
const unsubscribeFCM = messaging().onMessage(async remoteMessage => {
|
||
const title = remoteMessage.notification?.title ?? 'Convex CRM';
|
||
const body = remoteMessage.notification?.body ?? 'You have a new notification';
|
||
await displayNotification(title, body, (remoteMessage.data ?? {}) as NotificationData);
|
||
});
|
||
|
||
// Handle tap on a Notifee notification while app is in foreground
|
||
const unsubscribeNotifee = notifee.onForegroundEvent(({ type, detail }) => {
|
||
if (type === EventType.PRESS && detail.notification?.data) {
|
||
onOpen?.(detail.notification.data as NotificationData);
|
||
}
|
||
});
|
||
|
||
return () => {
|
||
unsubscribeFCM();
|
||
unsubscribeNotifee();
|
||
};
|
||
}
|
||
|
||
export function registerBackgroundHandler(): void {
|
||
// Display data-only FCM messages that arrive while app is in background
|
||
messaging().setBackgroundMessageHandler(async remoteMessage => {
|
||
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);
|
||
}
|
||
});
|
||
|
||
// Handle tap on a notification while app is in background or killed
|
||
notifee.onBackgroundEvent(async ({ type, detail }) => {
|
||
if (type === EventType.PRESS) {
|
||
const data = detail.notification?.data as Record<string, string> | undefined;
|
||
if (data?.type === 'lead' && data?.lead_id) {
|
||
navigateToLeadDetails(data.lead_id);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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. Android channel (permission is requested explicitly after login)
|
||
await createDefaultChannel();
|
||
|
||
// 2. 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,
|
||
};
|