native_convex_CRM/app/services/notifications.ts

263 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};