typedef NotificationTapHandler = void Function(NotificationDeepLink link);
typedef TokenSaver = Future<void> Function(String token);
class FirebaseMessagingService {
TokenSaver? _saveToken;
FirebaseMessagingService._internal();
static final FirebaseMessagingService _instance =
FirebaseMessagingService._internal();
factory FirebaseMessagingService.instance() => _instance;
LocalNotificationsService? _localNotificationsService;
NotificationTapHandler? _onTap;
Future<void> init({
required LocalNotificationsService localNotificationService,
NotificationTapHandler? onNotificationTap,
TokenSaver? onTokenUpdated,
}) async {
_localNotificationsService = localNotificationService;
_onTap = onNotificationTap;
_saveToken = onTokenUpdated;
await _handlePushNotificationToken();
await _requestPermission();
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpenedApp);
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
_onMessageOpenedApp(initialMessage);
}
}
Future<void> _handlePushNotificationToken() async {
final token = await FirebaseMessaging.instance.getToken();
if (token != null && _saveToken != null) {
await _saveToken!(token);
}
FirebaseMessaging.instance.onTokenRefresh.listen(
(fcmToken) async {
if (_saveToken != null) {
await _saveToken!(fcmToken);
}
},
);
}
Future<void> _requestPermission() async {
final result = await FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
badge: true,
carPlay: true,
criticalAlert: true,
provisional: true,
sound: true,
providesAppNotificationSettings: true,
);
}
void _onForegroundMessage(RemoteMessage message) {
final payloadJson = jsonEncode(message.data);
String? title = message.notification?.title;
String? body = message.notification?.body;
final composed = _composeLocalizedContent(message);
title = composed.$1 ?? title;
body = composed.$2 ?? body;
if (body != null || title != null) {
_localNotificationsService?.showNotification(
title,
body,
payloadJson,
);
}
}
void _onMessageOpenedApp(RemoteMessage message) {
final link = _mapMessageToDeepLink(message);
if (link != null && _onTap != null) {
_onTap!(link);
}
}
NotificationDeepLink? mapMessageToDeepLink(RemoteMessage message) {
return _mapMessageToDeepLink(message);
}
(String?, String?) _composeLocalizedContent(RemoteMessage message) {
try {
final data = message.data;
final locale = PlatformDispatcher.instance.locale;
final l10n = lookupAppLocalizations(locale);
final type = _parseType(data['type']);
if (type == null) return (null, null);
final actorName = (data['actorName'] as String?) ?? l10n.unknownActor;
String? title;
String? body;
switch (type) {
case NotificationType.friendRequest:
title = l10n.notificationsTitle;
body = l10n.notifFriendRequest(actorName);
break;
case NotificationType.friendAccepted:
title = l10n.notificationsTitle;
body = l10n.notifFriendAccepted(actorName);
break;
case NotificationType.postUpdate:
title = l10n.newPost;
body = l10n.notifPostUpdate(actorName);
break;
case NotificationType.commentOnMyPost:
title = l10n.notificationsTitle;
body = l10n.notifCommentOnMyPost(actorName);
break;
case NotificationType.likeOnMyPost:
title = l10n.notificationsTitle;
body = l10n.notifLikeOnMyPost(actorName);
break;
case NotificationType.replyOnMyComment:
title = l10n.notificationsTitle;
body = l10n.notifReplyOnMyComment(actorName);
break;
case NotificationType.likeOnMyComment:
title = l10n.notificationsTitle;
body = l10n.notifLikeOnMyComment(actorName);
break;
case NotificationType.likeOnMyReply:
title = l10n.notificationsTitle;
body = l10n.notifLikeOnMyReply(actorName);
break;
case NotificationType.guestbookOnMyHome:
title = l10n.notificationsTitle;
body = l10n.notifGuestbookOnMyHome(actorName);
break;
case NotificationType.likeOnMyGuestbook:
title = l10n.notificationsTitle;
body = l10n.notifLikeOnMyGuestbook(actorName);
break;
}
return (title, body);
} catch (_) {
return (null, null);
}
}
NotificationDeepLink? _mapMessageToDeepLink(RemoteMessage message) {
final data = message.data;
if (data.isEmpty) return null;
final typeString = data['type'];
final type = _parseType(typeString);
if (type == null) return null;
switch (type) {
case NotificationType.commentOnMyPost:
final entityId = data['entityId'];
if (entityId == null) return null;
return NotificationDeepLink.postDetail(entityId);
case NotificationType.friendAccepted:
final friendId = data['actorId'];
if (friendId == null) return null;
return NotificationDeepLink.friendHome(friendId);
case NotificationType.friendRequest:
final friendId = data['actorId'];
if (friendId == null) return null;
return NotificationDeepLink.friendHome(friendId);
case NotificationType.likeOnMyComment:
final postId = data['postId'];
if (postId == null) return null;
return NotificationDeepLink.postDetail(postId);
case NotificationType.likeOnMyPost:
final entityId = data['entityId'];
if (entityId == null) return null;
return NotificationDeepLink.postDetail(entityId);
case NotificationType.likeOnMyReply:
final postId = data['postId'];
if (postId == null) return null;
return NotificationDeepLink.postDetail(postId);
case NotificationType.postUpdate:
final entityId = data['entityId'];
if (entityId == null) return null;
return NotificationDeepLink.postDetail(entityId);
case NotificationType.replyOnMyComment:
final postId = data['postId'];
if (postId == null) return null;
return NotificationDeepLink.postDetail(postId);
case NotificationType.guestbookOnMyHome:
final ownerId = data['ownerId'];
if (ownerId == null) return null;
return NotificationDeepLink.guestBook(ownerId);
default:
return null;
}
}
NotificationType? _parseType(String? value) {
if (value == null) return null;
try {
return NotificationType.values.firstWhere(
(type) => type.name == value,
);
} catch (e) {
return null;
}
}
}
class NotificationDeepLink {
const NotificationDeepLink._({this.route, this.arguments});
final String? route;
final Map<String, dynamic>? arguments;
factory NotificationDeepLink.postDetail(String entityId) {
return NotificationDeepLink._(
route: '${RoutePaths.posts}/$entityId',
arguments: {'entityId': entityId},
);
}
factory NotificationDeepLink.friendHome(String friendId) {
return NotificationDeepLink._(
route: '/friends/$friendId/home',
arguments: {'friendId': friendId},
);
}
factory NotificationDeepLink.guestBook(String ownerId) {
final route = RoutePaths.guestBook.replaceAll(':ownerId', ownerId);
return NotificationDeepLink._(
route: route,
arguments: {'ownerId': ownerId},
);
}
}