native_convex_CRM/app/services/notifications.ts

311 lines
8.6 KiB
TypeScript

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<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,
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<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,
importance: AndroidImportance.HIGH,
smallIcon: 'ic_launcher', // use ic_notification once drawable is added to 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<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. 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,
};