Amba-Agent-App/lib/services/fcmService.ts
2026-01-16 00:22:35 +03:00

349 lines
11 KiB
TypeScript

/**
* 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<boolean> {
// 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<string | null> {
// 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<any | null> {
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);
});
}
}