AeThex-Connect/packages/mobile/src/services/PushNotificationService.ts
2026-01-10 08:00:59 +00:00

338 lines
8.5 KiB
TypeScript

import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance, EventType } from '@notifee/react-native';
import { Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import APIClient from '@aethex/core';
interface NotificationData {
type: 'message' | 'call' | 'friend_request' | 'voice_channel';
conversationId?: string;
callId?: string;
userId?: string;
title: string;
body: string;
imageUrl?: string;
actions?: Array<{ action: string; title: string }>;
}
class PushNotificationService {
private apiClient: APIClient | null = null;
async initialize(apiClient: APIClient) {
this.apiClient = apiClient;
// Request permissions
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Push notification permission granted');
await this.getFCMToken();
}
// Create notification channels (Android)
if (Platform.OS === 'android') {
await this.createChannels();
}
// Handle foreground messages
messaging().onMessage(async (remoteMessage) => {
await this.displayNotification(remoteMessage);
});
// Handle background messages
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
console.log('Background message:', remoteMessage);
await this.displayNotification(remoteMessage);
});
// Handle notification actions
notifee.onBackgroundEvent(async ({ type, detail }) => {
await this.handleNotificationEvent(type, detail);
});
notifee.onForegroundEvent(async ({ type, detail }) => {
await this.handleNotificationEvent(type, detail);
});
// Handle token refresh
messaging().onTokenRefresh(async (token) => {
await this.updateFCMToken(token);
});
}
async getFCMToken(): Promise<string> {
const token = await messaging().getToken();
console.log('FCM Token:', token);
await this.updateFCMToken(token);
return token;
}
private async updateFCMToken(token: string) {
try {
// Save locally
await AsyncStorage.setItem('fcm_token', token);
// Send to server
if (this.apiClient) {
await this.apiClient.request({
method: 'POST',
url: '/api/users/fcm-token',
data: { token, platform: Platform.OS },
});
}
} catch (error) {
console.error('Error updating FCM token:', error);
}
}
async createChannels() {
// Messages channel
await notifee.createChannel({
id: 'messages',
name: 'Messages',
importance: AndroidImportance.HIGH,
sound: 'message_sound',
vibration: true,
lights: true,
lightColor: '#667eea',
});
// Calls channel
await notifee.createChannel({
id: 'calls',
name: 'Calls',
importance: AndroidImportance.HIGH,
sound: 'call_sound',
vibration: true,
vibrationPattern: [300, 500],
});
// Voice channel
await notifee.createChannel({
id: 'voice_channel',
name: 'Voice Channel',
importance: AndroidImportance.LOW,
sound: false,
});
// Friend requests
await notifee.createChannel({
id: 'social',
name: 'Social',
importance: AndroidImportance.DEFAULT,
sound: 'default',
});
}
async displayNotification(remoteMessage: any) {
const { notification, data } = remoteMessage;
const notifData: NotificationData = data as NotificationData;
const channelId = this.getChannelId(notifData.type);
const notificationConfig: any = {
title: notification?.title || notifData.title,
body: notification?.body || notifData.body,
android: {
channelId: channelId,
smallIcon: 'ic_notification',
color: '#667eea',
pressAction: {
id: 'default',
launchActivity: 'default',
},
actions: this.getActions(notifData.type, notifData),
},
ios: {
categoryId: notifData.type.toUpperCase(),
attachments: notifData.imageUrl
? [
{
url: notifData.imageUrl,
thumbnailHidden: false,
},
]
: undefined,
sound: this.getSound(notifData.type),
},
data: data,
};
await notifee.displayNotification(notificationConfig);
}
private getChannelId(type: string): string {
switch (type) {
case 'call':
return 'calls';
case 'voice_channel':
return 'voice_channel';
case 'friend_request':
return 'social';
case 'message':
default:
return 'messages';
}
}
private getActions(type: string, data: NotificationData) {
if (Platform.OS !== 'android') return undefined;
switch (type) {
case 'message':
return [
{
title: 'Reply',
pressAction: { id: 'reply' },
input: {
placeholder: 'Type a message...',
allowFreeFormInput: true,
},
},
{
title: 'Mark as Read',
pressAction: { id: 'mark_read' },
},
];
case 'call':
return [
{
title: 'Answer',
pressAction: { id: 'answer_call' },
},
{
title: 'Decline',
pressAction: { id: 'decline_call' },
},
];
case 'friend_request':
return [
{
title: 'Accept',
pressAction: { id: 'accept_friend' },
},
{
title: 'Decline',
pressAction: { id: 'decline_friend' },
},
];
default:
return undefined;
}
}
private getSound(type: string): string {
switch (type) {
case 'call':
return 'call_sound.wav';
case 'message':
return 'message_sound.wav';
default:
return 'default';
}
}
private async handleNotificationEvent(type: EventType, detail: any) {
const { notification, pressAction, input } = detail;
if (type === EventType.PRESS) {
// User tapped notification
const data = notification?.data;
console.log('Notification pressed:', data);
// Navigate to appropriate screen
}
if (type === EventType.ACTION_PRESS && pressAction) {
await this.handleAction(pressAction.id, notification?.data, input);
}
if (type === EventType.DISMISSED) {
console.log('Notification dismissed');
}
}
private async handleAction(actionId: string, data: any, input?: string) {
if (!this.apiClient) return;
try {
switch (actionId) {
case 'reply':
if (input && data?.conversationId) {
await this.apiClient.sendMessage(data.conversationId, input);
}
break;
case 'mark_read':
if (data?.conversationId) {
await this.apiClient.request({
method: 'POST',
url: `/api/conversations/${data.conversationId}/read`,
});
}
break;
case 'answer_call':
if (data?.callId) {
await this.apiClient.joinCall(data.callId);
}
break;
case 'decline_call':
if (data?.callId) {
await this.apiClient.request({
method: 'POST',
url: `/api/calls/${data.callId}/decline`,
});
}
break;
case 'accept_friend':
if (data?.requestId) {
await this.apiClient.acceptFriendRequest(data.requestId);
}
break;
case 'decline_friend':
if (data?.requestId) {
await this.apiClient.request({
method: 'POST',
url: `/api/nexus/friends/reject/${data.requestId}`,
});
}
break;
}
} catch (error) {
console.error('Error handling notification action:', error);
}
}
async cancelNotification(notificationId: string) {
await notifee.cancelNotification(notificationId);
}
async cancelAllNotifications() {
await notifee.cancelAllNotifications();
}
async getBadgeCount(): Promise<number> {
if (Platform.OS === 'ios') {
return await notifee.getBadgeCount();
}
return 0;
}
async setBadgeCount(count: number) {
if (Platform.OS === 'ios') {
await notifee.setBadgeCount(count);
}
}
}
export default new PushNotificationService();