349 lines
11 KiB
TypeScript
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);
|
|
});
|
|
}
|
|
}
|