first commit

This commit is contained in:
2026-04-29 12:53:22 +07:00
commit e6a30eddd3
394 changed files with 16408 additions and 0 deletions

View 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);
}

View 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;
}

View 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);
}
}

View 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);
}
}
}

View 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');
}
}
}

View 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,
);
}
}

View 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);
});
}
}

View 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(),
);
}
}

View 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;
}