setup fcm

This commit is contained in:
Efril 2026-05-07 17:44:03 +07:00
parent cdc65f8f8f
commit dc09aabbe8
15 changed files with 425 additions and 1 deletions

View File

@ -14,6 +14,7 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
@ -45,6 +46,8 @@ flutter {
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.4.0"))

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,13 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart';
@module
abstract class FirebaseDi {
@lazySingleton
FirebaseMessaging get messaging => FirebaseMessaging.instance;
@lazySingleton
FlutterLocalNotificationsPlugin get localNotifications =>
FlutterLocalNotificationsPlugin();
}

View File

@ -0,0 +1,87 @@
// CONTOH PENGGUNAAN FCM SERVICE
// File ini hanya untuk referensi, tidak perlu diimport ke app
import 'package:apskel_pos_flutter_v2/common/service/fcm_service.dart';
import 'package:apskel_pos_flutter_v2/injection.dart';
/// Contoh 1: Ambil FCM token
Future<void> exampleGetToken() async {
final fcmService = getIt<FcmService>();
final token = await fcmService.getToken();
print('FCM Token: $token');
// Kirim token ke server untuk push notification
// await apiClient.sendTokenToServer(token);
}
/// Contoh 2: Subscribe ke topic
Future<void> exampleSubscribeToTopic() async {
final fcmService = getIt<FcmService>();
// Subscribe ke topic "all_users"
await fcmService.subscribeToTopic('all_users');
// Subscribe ke topic berdasarkan outlet_id
await fcmService.subscribeToTopic('outlet_123');
}
/// Contoh 3: Unsubscribe dari topic
Future<void> exampleUnsubscribeFromTopic() async {
final fcmService = getIt<FcmService>();
await fcmService.unsubscribeFromTopic('outlet_123');
}
/// Contoh 4: Gunakan di BLoC
///
/// class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
/// final FcmService _fcmService;
///
/// NotificationBloc(this._fcmService) : super(NotificationInitial()) {
/// on<GetFcmToken>(_onGetFcmToken);
/// on<SubscribeToTopic>(_onSubscribeToTopic);
/// }
///
/// Future<void> _onGetFcmToken(
/// GetFcmToken event,
/// Emitter<NotificationState> emit,
/// ) async {
/// final token = await _fcmService.getToken();
/// emit(NotificationTokenLoaded(token));
/// }
///
/// Future<void> _onSubscribeToTopic(
/// SubscribeToTopic event,
/// Emitter<NotificationState> emit,
/// ) async {
/// await _fcmService.subscribeToTopic(event.topic);
/// emit(NotificationTopicSubscribed(event.topic));
/// }
/// }
/// Contoh 5: Gunakan di Repository
///
/// @LazySingleton(as: IAuthRepository)
/// class AuthRepository implements IAuthRepository {
/// final FcmService _fcmService;
/// final ApiClient _apiClient;
///
/// AuthRepository(this._fcmService, this._apiClient);
///
/// @override
/// Future<Either<Failure, User>> login(String email, String password) async {
/// try {
/// final response = await _apiClient.login(email, password);
///
/// // Kirim FCM token ke server setelah login berhasil
/// final fcmToken = await _fcmService.getToken();
/// if (fcmToken != null) {
/// await _apiClient.updateFcmToken(fcmToken);
/// }
///
/// return Right(response);
/// } catch (e) {
/// return Left(ServerFailure(e.toString()));
/// }
/// }
/// }

View File

@ -0,0 +1,191 @@
import 'dart:developer';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:injectable/injectable.dart';
/// Background message handler harus top-level function
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
log('[FCM] Background message: ${message.messageId}');
}
@lazySingleton
class FcmService {
FcmService(this._messaging, this._localNotifications);
final FirebaseMessaging _messaging;
final FlutterLocalNotificationsPlugin _localNotifications;
/// Inisialisasi FCM: minta permission, set handler, ambil token
Future<void> initialize() async {
// Setup local notifications
await _setupLocalNotifications();
// Register background handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Minta permission notifikasi (iOS & Android 13+)
final settings = await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
log('[FCM] Permission status: ${settings.authorizationStatus}');
if (settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional) {
await _setupTokenHandling();
_setupForegroundHandler();
_setupOpenedAppHandler();
}
}
Future<void> _setupLocalNotifications() async {
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTapped,
);
// Android notification channel
const channel = AndroidNotificationChannel(
'high_importance_channel',
'High Importance Notifications',
description: 'This channel is used for important notifications.',
importance: Importance.high,
);
await _localNotifications
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
void _onNotificationTapped(NotificationResponse response) {
log('[FCM] Notification tapped: ${response.payload}');
// TODO: handle navigation berdasarkan payload
}
Future<void> _setupTokenHandling() async {
final token = await getToken();
log('========================================');
log('[FCM] TOKEN: $token');
log('========================================');
// TODO: kirim token ke server jika diperlukan
_messaging.onTokenRefresh.listen((newToken) {
log('========================================');
log('[FCM] TOKEN REFRESHED: $newToken');
log('========================================');
// TODO: kirim token baru ke server jika diperlukan
});
}
void _setupForegroundHandler() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
log('[FCM] Foreground message: ${message.messageId}');
log('[FCM] Title: ${message.notification?.title}');
log('[FCM] Body: ${message.notification?.body}');
log('[FCM] Data: ${message.data}');
// Tampilkan notifikasi lokal saat app di foreground
final notification = message.notification;
if (notification != null) {
_showLocalNotification(
id: message.hashCode,
title: notification.title ?? '',
body: notification.body ?? '',
payload: message.data.toString(),
);
}
});
}
void _setupOpenedAppHandler() {
// App dibuka dari notifikasi saat terminated
_messaging.getInitialMessage().then((RemoteMessage? message) {
if (message != null) {
log('[FCM] App opened from terminated via: ${message.messageId}');
_handleMessageNavigation(message);
}
});
// App dibuka dari notifikasi saat background
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
log('[FCM] App opened from background via: ${message.messageId}');
_handleMessageNavigation(message);
});
}
void _handleMessageNavigation(RemoteMessage message) {
// TODO: tambahkan logika navigasi berdasarkan message.data
log('[FCM] Handle navigation for: ${message.data}');
}
Future<void> _showLocalNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
const androidDetails = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
channelDescription: 'This channel is used for important notifications.',
importance: Importance.high,
priority: Priority.high,
showWhen: true,
icon: 'ic_notification',
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _localNotifications.show(id, title, body, details, payload: payload);
log('[FCM] Local notification shown: $title');
}
/// Ambil FCM token perangkat
Future<String?> getToken() async {
try {
return await _messaging.getToken();
} catch (e) {
log('[FCM] Failed to get token: $e');
return null;
}
}
/// Subscribe ke topic tertentu
Future<void> subscribeToTopic(String topic) async {
await _messaging.subscribeToTopic(topic);
log('[FCM] Subscribed to topic: $topic');
}
/// Unsubscribe dari topic tertentu
Future<void> unsubscribeFromTopic(String topic) async {
await _messaging.unsubscribeFromTopic(topic);
log('[FCM] Unsubscribed from topic: $topic');
}
}

View File

@ -80,10 +80,12 @@ import 'package:apskel_pos_flutter_v2/common/di/di_auto_route.dart' as _i729;
import 'package:apskel_pos_flutter_v2/common/di/di_connectivity.dart' as _i807;
import 'package:apskel_pos_flutter_v2/common/di/di_database.dart' as _i209;
import 'package:apskel_pos_flutter_v2/common/di/di_dio.dart' as _i86;
import 'package:apskel_pos_flutter_v2/common/di/di_firebase.dart' as _i857;
import 'package:apskel_pos_flutter_v2/common/di/di_shared_preferences.dart'
as _i135;
import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
as _i171;
import 'package:apskel_pos_flutter_v2/common/service/fcm_service.dart' as _i312;
import 'package:apskel_pos_flutter_v2/domain/analytic/analytic.dart' as _i346;
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
@ -148,6 +150,9 @@ import 'package:apskel_pos_flutter_v2/presentation/router/app_router.dart'
as _i800;
import 'package:connectivity_plus/connectivity_plus.dart' as _i895;
import 'package:dio/dio.dart' as _i361;
import 'package:firebase_messaging/firebase_messaging.dart' as _i892;
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
as _i163;
import 'package:get_it/get_it.dart' as _i174;
import 'package:injectable/injectable.dart' as _i526;
import 'package:shared_preferences/shared_preferences.dart' as _i460;
@ -167,6 +172,7 @@ extension GetItInjectableX on _i174.GetIt {
final autoRouteDi = _$AutoRouteDi();
final connectivityDi = _$ConnectivityDi();
final dioDi = _$DioDi();
final firebaseDi = _$FirebaseDi();
gh.factory<_i13.CheckoutFormBloc>(() => _i13.CheckoutFormBloc());
gh.factory<_i96.PrinterBloc>(() => _i96.PrinterBloc());
gh.factory<_i257.ReportBloc>(() => _i257.ReportBloc());
@ -179,6 +185,10 @@ extension GetItInjectableX on _i174.GetIt {
gh.lazySingleton<_i800.AppRouter>(() => autoRouteDi.appRouter);
gh.lazySingleton<_i895.Connectivity>(() => connectivityDi.connectivity);
gh.lazySingleton<_i361.Dio>(() => dioDi.dio);
gh.lazySingleton<_i892.FirebaseMessaging>(() => firebaseDi.messaging);
gh.lazySingleton<_i163.FlutterLocalNotificationsPlugin>(
() => firebaseDi.localNotifications,
);
gh.lazySingleton<_i171.NetworkClient>(
() => _i171.NetworkClient(gh<_i895.Connectivity>()),
);
@ -192,6 +202,12 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i464.ProductLocalDataProvider>(
() => _i464.ProductLocalDataProvider(gh<_i487.DatabaseHelper>()),
);
gh.lazySingleton<_i312.FcmService>(
() => _i312.FcmService(
gh<_i892.FirebaseMessaging>(),
gh<_i163.FlutterLocalNotificationsPlugin>(),
),
);
gh.factory<_i204.AuthLocalDataProvider>(
() => _i204.AuthLocalDataProvider(gh<_i460.SharedPreferences>()),
);
@ -397,3 +413,5 @@ class _$AutoRouteDi extends _i729.AutoRouteDi {}
class _$ConnectivityDi extends _i807.ConnectivityDi {}
class _$DioDi extends _i86.DioDi {}
class _$FirebaseDi extends _i857.FirebaseDi {}

View File

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:injectable/injectable.dart';
import 'common/service/fcm_service.dart';
import 'injection.dart';
import 'presentation/app_widget.dart';
@ -47,10 +48,12 @@ void main() async {
kReleaseMode ? Environment.prod : Environment.dev,
);
// Inisialisasi FCM setelah DI siap
await getIt<FcmService>().initialize();
runApp(const AppWidget());
},
(error, stack) {
// Ini udah bener
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
},
);

View File

@ -8,6 +8,8 @@ import Foundation
import connectivity_plus
import firebase_core
import firebase_crashlytics
import firebase_messaging
import flutter_local_notifications
import path_provider_foundation
import shared_preferences_foundation
import sqflite_darwin
@ -16,6 +18,8 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))

View File

@ -441,6 +441,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.8.14"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "5021279acd1cb5ccaceaa388e616e82cc4a2e4d862f02637df0e8ab766e6900a"
url: "https://pub.dev"
source: hosted
version: "16.0.3"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: f3a16c51f02055ace2a7c16ccb341c1f1b36b67c13270a48bcef68c1d970bbe8
url: "https://pub.dev"
source: hosted
version: "4.7.3"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "3eb9a1382caeb95b370f21e36d4a460496af777c9c2ef5df9b90d4803982c069"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
fixnum:
dependency: transitive
description:
@ -526,6 +550,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_spinkit:
dependency: "direct main"
description:
@ -1269,6 +1317,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.5"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
timing:
dependency: transitive
description:

View File

@ -29,6 +29,8 @@ dependencies:
shared_preferences: ^2.5.3
firebase_core: ^4.2.0
firebase_crashlytics: ^5.0.3
firebase_messaging: ^16.0.3
flutter_local_notifications: ^18.0.1
another_flushbar: ^1.12.32
flutter_spinkit: ^5.2.2
bloc: ^9.1.0

View File

@ -0,0 +1,47 @@
import 'dart:io';
import 'package:image/image.dart' as img;
/// Jalankan dengan: dart run tool/generate_notification_icon.dart
void main() async {
final inputPath = 'assets/images/logo_white.png';
final outputBase = 'android/app/src/main/res';
final sizes = {
'drawable-mdpi': 24,
'drawable-hdpi': 36,
'drawable-xhdpi': 48,
'drawable-xxhdpi': 72,
'drawable-xxxhdpi': 96,
};
final inputFile = File(inputPath);
if (!inputFile.existsSync()) {
print('ERROR: File tidak ditemukan: $inputPath');
exit(1);
}
final originalBytes = inputFile.readAsBytesSync();
final original = img.decodeImage(originalBytes);
if (original == null) {
print('ERROR: Gagal decode image');
exit(1);
}
print('Source: $inputPath (${original.width}x${original.height})');
for (final entry in sizes.entries) {
final folder = '$outputBase/${entry.key}';
final outputPath = '$folder/ic_notification.png';
Directory(folder).createSync(recursive: true);
final size = entry.value;
final resized = img.copyResize(original, width: size, height: size);
final pngBytes = img.encodePng(resized);
File(outputPath).writeAsBytesSync(pngBytes);
print('Generated: $outputPath (${size}x${size})');
}
print('\nDone! Gunakan icon: "ic_notification" di fcm_service.dart');
}