first commit
This commit is contained in:
9
lib/app/core/notification/fcm_background_handler.dart
Normal file
9
lib/app/core/notification/fcm_background_handler.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_notification_handler.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(
|
||||
RemoteMessage message,
|
||||
) async {
|
||||
await FcmNotificationHandler.handleBackground(message);
|
||||
}
|
||||
15
lib/app/core/notification/fcm_config.dart
Normal file
15
lib/app/core/notification/fcm_config.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class FcmConfig {
|
||||
FcmConfig._();
|
||||
|
||||
static const String defaultChannelId = 'high_importance_channel';
|
||||
static const String defaultChannelName = 'High Importance Notifications';
|
||||
static const String defaultChannelDesc =
|
||||
'Channel for important notifications';
|
||||
|
||||
static AndroidNotificationPriority priority =
|
||||
AndroidNotificationPriority.highPriority;
|
||||
|
||||
static const bool enableForegroundNotification = true;
|
||||
}
|
||||
41
lib/app/core/notification/fcm_notification_handler.dart
Normal file
41
lib/app/core/notification/fcm_notification_handler.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
import 'dart:developer';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/models/fcm_payload.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_route/notification_router.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/local_notification_service.dart';
|
||||
|
||||
class FcmNotificationHandler {
|
||||
FcmNotificationHandler._();
|
||||
|
||||
static Future<void> handleForeground(RemoteMessage message) async {
|
||||
log('Foreground FCM: ${message.data}', name: 'FCM');
|
||||
|
||||
final payload = FcmPayload.fromMessage({
|
||||
'notification': message.notification?.toMap(),
|
||||
'data': message.data,
|
||||
});
|
||||
|
||||
if (payload.title != null && payload.body != null) {
|
||||
await LocalNotificationService.show(
|
||||
title: payload.title!,
|
||||
body: payload.body!,
|
||||
payload: payload.data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> handleOpenedApp(RemoteMessage message) async {
|
||||
log('Opened from FCM: ${message.data}', name: 'FCM');
|
||||
await NotificationRouter.handle(message.data);
|
||||
}
|
||||
|
||||
static Future<void> handleLocalTap(String? payload) async {
|
||||
if (payload == null) return;
|
||||
await NotificationRouter.handleRaw(payload);
|
||||
}
|
||||
|
||||
static Future<void> handleBackground(RemoteMessage message) async {
|
||||
log('Background FCM: ${message.data}', name: 'FCM');
|
||||
await NotificationRouter.handle(message.data);
|
||||
}
|
||||
}
|
||||
110
lib/app/core/notification/fcm_route/notification_router.dart
Normal file
110
lib/app/core/notification/fcm_route/notification_router.dart
Normal file
@@ -0,0 +1,110 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:frontend_eccp_mobile/app/core/constants/globalkey.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/di/injection.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/app_navigator.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/app_routes.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_route/notification_routes.dart';
|
||||
import 'package:frontend_eccp_mobile/modules/auth/login/data/model/user_session.dart';
|
||||
|
||||
class NotificationRouter {
|
||||
NotificationRouter._();
|
||||
static Future<void> handle(Map<String, dynamic> data) async {
|
||||
log('NotificationRouter.handle: $data', name: 'FCM');
|
||||
|
||||
final page = data['page']?.toString();
|
||||
if (page == null || page.isEmpty) {
|
||||
log('Missing page in notification payload', name: 'FCM');
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? params;
|
||||
|
||||
final paramsRaw = data['params'];
|
||||
if (paramsRaw is String && paramsRaw.isNotEmpty) {
|
||||
try {
|
||||
params = jsonDecode(paramsRaw) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
log('Failed to decode params string', name: 'FCM');
|
||||
}
|
||||
} else if (paramsRaw is Map<String, dynamic>) {
|
||||
params = paramsRaw;
|
||||
}
|
||||
|
||||
params ??= Map<String, dynamic>.from(data)
|
||||
..remove('page')
|
||||
..remove('notification');
|
||||
|
||||
if (params.isEmpty) params = null;
|
||||
|
||||
await _navigate(page: page, params: params);
|
||||
}
|
||||
|
||||
static Future<void> handleRaw(String raw) async {
|
||||
log('NotificationRouter.handleRaw: $raw', name: 'FCM');
|
||||
|
||||
if (raw.trim().isEmpty) return;
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
await handle(decoded);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (raw.contains('=') && raw.contains('&')) {
|
||||
try {
|
||||
final uri = Uri(query: raw);
|
||||
await handle(Map<String, dynamic>.from(uri.queryParameters));
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
final normalized = raw
|
||||
.replaceAll('{', '')
|
||||
.replaceAll('}', '')
|
||||
.split(',')
|
||||
.map((e) => e.split(':'))
|
||||
.where((e) => e.length == 2)
|
||||
.fold<Map<String, dynamic>>({}, (map, pair) {
|
||||
final key = pair[0].trim();
|
||||
final value = pair[1].trim();
|
||||
|
||||
map[key] = int.tryParse(value) ?? value;
|
||||
return map;
|
||||
});
|
||||
|
||||
if (normalized.isNotEmpty) {
|
||||
await handle(normalized);
|
||||
return;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
log('handleRaw failed to parse payload', name: 'FCM');
|
||||
}
|
||||
|
||||
static Future<void> _navigate({
|
||||
required String? page,
|
||||
Map<String, dynamic>? params,
|
||||
}) async {
|
||||
final navContext = navigatorKey.currentContext;
|
||||
if (navContext == null) return;
|
||||
|
||||
if (!navContext.mounted) return;
|
||||
|
||||
if (!getIt<UserSession>().isLoggedIn) {
|
||||
await AppNavigator.clearAndPush(
|
||||
navContext,
|
||||
AppRoutes.login,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page != null) {
|
||||
if (!navContext.mounted) return;
|
||||
await NotificationRoutes.routeToPage(navContext, page, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
lib/app/core/notification/fcm_route/notification_routes.dart
Normal file
50
lib/app/core/notification/fcm_route/notification_routes.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/app_navigator.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/navigation/app_routes.dart';
|
||||
|
||||
class NotificationRoutes {
|
||||
static Future<void> routeToPage(
|
||||
BuildContext context,
|
||||
String page,
|
||||
Map<String, dynamic>? params,
|
||||
) async {
|
||||
log('Routing to page=$page params=$params', name: 'FCM');
|
||||
switch (page) {
|
||||
case 'setting':
|
||||
await AppNavigator.clearAndPush(
|
||||
context,
|
||||
AppRoutes.settings,
|
||||
);
|
||||
return;
|
||||
|
||||
case 'berkas':
|
||||
final id = params?['id'];
|
||||
|
||||
await AppNavigator.clearAndPush(
|
||||
context,
|
||||
AppRoutes.berkas,
|
||||
onOpened: () async {
|
||||
if (id != null) {
|
||||
await AppNavigator.push(
|
||||
context,
|
||||
AppRoutes.berkas,
|
||||
extra: {
|
||||
'berkasId': id.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
|
||||
default:
|
||||
await AppNavigator.clearAndPush(
|
||||
context,
|
||||
AppRoutes.home,
|
||||
);
|
||||
|
||||
log('Unknown notification page: $page', name: 'FCM');
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lib/app/core/notification/fcm_service.dart
Normal file
44
lib/app/core/notification/fcm_service.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:developer';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_background_handler.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_notification_handler.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_token_manager.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/local_notification_service.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/notification_intent_store.dart';
|
||||
|
||||
class FcmService {
|
||||
FcmService._();
|
||||
|
||||
static final FirebaseMessaging _fcm = FirebaseMessaging.instance;
|
||||
|
||||
static Future<void> init() async {
|
||||
await _fcm.requestPermission();
|
||||
|
||||
await LocalNotificationService.init(
|
||||
onSelectNotification: FcmNotificationHandler.handleLocalTap,
|
||||
);
|
||||
|
||||
final token = await FcmTokenManager.getToken();
|
||||
log('Initial FCM Token: $token', name: 'FCM');
|
||||
|
||||
FcmTokenManager.listenRefresh((token) {
|
||||
log('FCM Token refreshed: $token', name: 'FCM');
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessage.listen(
|
||||
FcmNotificationHandler.handleForeground,
|
||||
);
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen(
|
||||
FcmNotificationHandler.handleOpenedApp,
|
||||
);
|
||||
final initialMessage = await _fcm.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
NotificationIntentStore.pending = initialMessage.data;
|
||||
}
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/app/core/notification/fcm_token_manager.dart
Normal file
51
lib/app/core/notification/fcm_token_manager.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'dart:developer';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class FcmTokenManager {
|
||||
FcmTokenManager._();
|
||||
|
||||
static final FirebaseMessaging _fcm = FirebaseMessaging.instance;
|
||||
|
||||
static Future<void> waitForAPNSToken() async {
|
||||
String? apnsToken;
|
||||
|
||||
while (apnsToken == null) {
|
||||
await Future<dynamic>.delayed(const Duration(milliseconds: 500));
|
||||
apnsToken = await _fcm.getAPNSToken();
|
||||
}
|
||||
|
||||
log('APNS Token ready: $apnsToken', name: 'FCM');
|
||||
}
|
||||
|
||||
static Future<String?> getToken() async {
|
||||
try {
|
||||
await _fcm.requestPermission();
|
||||
|
||||
// await waitForAPNSToken();
|
||||
|
||||
final token = await _fcm.getToken();
|
||||
log('FCM Token: $token', name: 'FCM');
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
log('Get token error: $e', name: 'FCM');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> deleteToken() async {
|
||||
try {
|
||||
await _fcm.deleteToken();
|
||||
log('FCM Token deleted', name: 'FCM');
|
||||
} catch (e) {
|
||||
log('Delete token error: $e', name: 'FCM');
|
||||
}
|
||||
}
|
||||
|
||||
static void listenRefresh(void Function(String token) onRefresh) {
|
||||
FirebaseMessaging.instance.onTokenRefresh.listen((token) {
|
||||
log('FCM Token refreshed: $token', name: 'FCM');
|
||||
onRefresh(token);
|
||||
});
|
||||
}
|
||||
}
|
||||
54
lib/app/core/notification/local_notification_service.dart
Normal file
54
lib/app/core/notification/local_notification_service.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:frontend_eccp_mobile/app/core/notification/fcm_config.dart';
|
||||
|
||||
typedef NotificationTapCallback = void Function(String? payload);
|
||||
|
||||
class LocalNotificationService {
|
||||
LocalNotificationService._();
|
||||
|
||||
static final FlutterLocalNotificationsPlugin _plugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
static Future<void> init({
|
||||
NotificationTapCallback? onSelectNotification,
|
||||
}) async {
|
||||
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
const ios = DarwinInitializationSettings();
|
||||
|
||||
const settings = InitializationSettings(
|
||||
android: android,
|
||||
iOS: ios,
|
||||
);
|
||||
|
||||
await _plugin.initialize(
|
||||
settings: settings,
|
||||
onDidReceiveNotificationResponse: (response) {
|
||||
onSelectNotification?.call(response.payload);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> show({
|
||||
required String title,
|
||||
required String body,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
FcmConfig.defaultChannelId,
|
||||
FcmConfig.defaultChannelName,
|
||||
channelDescription: FcmConfig.defaultChannelDesc,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
);
|
||||
|
||||
const details = NotificationDetails(android: androidDetails);
|
||||
|
||||
await _plugin.show(
|
||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: details,
|
||||
payload: payload?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
lib/app/core/notification/notification_intent_store.dart
Normal file
19
lib/app/core/notification/notification_intent_store.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class NotificationIntentStore {
|
||||
NotificationIntentStore._();
|
||||
|
||||
static Map<String, dynamic>? _pendingData;
|
||||
|
||||
static set pending(Map<String, dynamic> data) {
|
||||
_pendingData = Map<String, dynamic>.from(data);
|
||||
}
|
||||
|
||||
static Map<String, dynamic>? get pending => _pendingData;
|
||||
|
||||
static Map<String, dynamic>? consume() {
|
||||
final data = _pendingData;
|
||||
_pendingData = null;
|
||||
return data;
|
||||
}
|
||||
|
||||
static bool get hasPending => _pendingData != null;
|
||||
}
|
||||
Reference in New Issue
Block a user