From dc09aabbe8e3275b9ddd38c00669f51c9534061b Mon Sep 17 00:00:00 2001 From: Efril Date: Thu, 7 May 2026 17:44:03 +0700 Subject: [PATCH] setup fcm --- android/app/build.gradle.kts | 3 + .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 846 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 431 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 1359 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 2708 bytes .../res/drawable-xxxhdpi/ic_notification.png | Bin 0 -> 4668 bytes lib/common/di/di_firebase.dart | 13 ++ lib/common/service/fcm_example_usage.dart | 87 ++++++++ lib/common/service/fcm_service.dart | 191 ++++++++++++++++++ lib/injection.config.dart | 18 ++ lib/main.dart | 5 +- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 56 +++++ pubspec.yaml | 2 + tool/generate_notification_icon.dart | 47 +++++ 15 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_notification.png create mode 100644 lib/common/di/di_firebase.dart create mode 100644 lib/common/service/fcm_example_usage.dart create mode 100644 lib/common/service/fcm_service.dart create mode 100644 tool/generate_notification_icon.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 10ce40f..da646ac 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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")) diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..00e1cd6ff702f3669717be163710317dd1f62986 GIT binary patch literal 846 zcmV-U1F`&xP)T857k}KeUwa)lBd60typCX4H=C&=)#^+y(^TyASp; zhRK;l0TxI*k*iq<_F=F{_gBz$jA`a)%ur^cLr*fSO}@t@i1y+lO3O*4|^jIu6;D)%|&2hFUQ2pj0u}Sh|BD;DjCVu zJhN$a>IQ}*KfaITV0LZlml(Z{Y01n2)NsMC4k%BQSAQ+TTeYlJWY%mXc4ep0oD1Wd z5vXgxtO80kQRJL%E#k!5PG}Yuy2-C@N%JazSU{+RDa8OHQMtw!n(E}r(OV%(`bl0E zjk^K0EUK!MrdERuE1t_IbDmPmv={HiuB3c1^3y7ymP48>YjQ}s19*>RG1>_R$71`j ze>MW7*3GRoVGi^`N%w`T%Q1NuQvUDGLz_*QzY%M(x?)pT$p!8yt~yS(%k?!@7&;v~ z22`48nVs*2!j`O?a6Cws6Rd4&S?kZv+ewo(%_{u>&=TiV-|F_E7vQw3r%q+{8)DS9 zv@~Bs?q^MyYMSsimNg1;CNUbrEPtH9dnZw zhr2W?8;4+5m6G)a%^04ZpC4{Q_>2~KO{=?^s+uQFCzJ zV{rnw3U3$EVyYxk=K>^=5=+7)IE6G$@Y!9qNO~=~0Jl&Bmo%AiivS$Oa}6FWF$57+ zpbXW)caYN#j>$L7O=Y9}+ZJ600JS1LA+!a1KA?+RP?*@`3KTzBkr^_riq_mepBTM_ zz0*Jjodh23VBu~wSr7KNi(*1txdVBJO#%AY&V!35$b`URYm=XPJBS7wv;docx9v%G zF8t~lm|@bP)uXm>%6@g!MyoDoYV8b2{)ou4@XOj57|m^tKyD7T+2DQQ)@k2wI^S$~ zQ5#VsdhkcP10)fNH!=$<{w&8Mpdp65J3z-qbBAu_utJt8-K5&&FTH}p0<(+WiI4y3 Z{{eQ^qK$p}l^_5B002ovPDHLkV1j-oxqAQr literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..a6dd8dec801727da0123f482417d4778604171e4 GIT binary patch literal 1359 zcmV-V1+e;wP)?s9LmbY51*Se`B#P{=SN-bb zL4I&Ke6#|4Xc$08ysLJ>j!A_D(TBCb-1dgTJ(X==Sq07yQI$Wv@q zx9CNrtN$tN75Zi6d5SAdz4}3V0W{FSm-hPvqC$2gdjQ>w02G!2U$E|5q||*#QKa$; zfs%!QViAJd8VX;L)!eRFf%Ga>R}g{1jRe%g0$EOgUY*1$0FoulF>EQeyM!+6AP7d- z|0Ov?s-X?vaa?hMWD$dR3o0B^rYf!rs@e zwh;~Gbp*T(K=PhV!1xuwKWmC^mC?Zs;Gk$_cPr)p(N|B9b; zRZ!!oIehG!QSTnXEPx6(Kn$1GqRb}MS)G*`jYdORoJ&Wq2x@q%_ZkcC)>%7&Oz7>W z{mPfpXVPtAz3mF<5T%0h#k0MAe%uG*1meq3I_y4yaZxGbdLCu~y($$v_SeXYLS%k; z5I{)OIv#?7>QsA_FU3!j5tfItgOLdDo`}Q=%-C)b0DuF@pM~-rir@ArAC}MbJ1TUZ zt2FaTK{_$-qTp&R1mHgr*`<7HlDWjnw+KGV%2x&;b*wzw4&W7~gc2Hrlxfv!Q58Je6HE$V;KLCX+x={PSJphpo z(RJ1Aqk`lMAf{{e#}Yxhh0|rlQ6~_uK_`+&0OyARl)isPR#sN1AUYz`^WxR*5C{9l z4-Qsgdj71}c4lr4<%UJDi4&a!0M)|{$Mum2Ql}DPwQ$o0?Grm(iY;kl9ROFOn`sJp z8wVMt4Uei)@2C2%*6*)PDb|F{R@lp;Els`U`mDAdwPlPzvJ>6y#OYt=ij;eUOanml zzxi#OL^P@dewD;E(0I#SD6L;!0H9;oK%*JoA#8mB69$5|&n^o|ei2NJV?UWp;NRwMMXavQ5c`}^`QW<&~>t)2cjCliSM(}rXI`J0~{@whZ zzSc#^@l?XAGnia!jzTuZ$cB{OM)kE+o|$0`Fzp+(elFs_#ZZDUGKJI Ry;T4J002ovPDHLkV1j$GhmZgO literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..ca65742daf0f2d9488be8a97e652d3d38ec37fa5 GIT binary patch literal 2708 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 FirebaseMessaging.instance; + + @lazySingleton + FlutterLocalNotificationsPlugin get localNotifications => + FlutterLocalNotificationsPlugin(); +} diff --git a/lib/common/service/fcm_example_usage.dart b/lib/common/service/fcm_example_usage.dart new file mode 100644 index 0000000..5267d04 --- /dev/null +++ b/lib/common/service/fcm_example_usage.dart @@ -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 exampleGetToken() async { + final fcmService = getIt(); + 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 exampleSubscribeToTopic() async { + final fcmService = getIt(); + + // 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 exampleUnsubscribeFromTopic() async { + final fcmService = getIt(); + + await fcmService.unsubscribeFromTopic('outlet_123'); +} + +/// Contoh 4: Gunakan di BLoC +/// +/// class NotificationBloc extends Bloc { +/// final FcmService _fcmService; +/// +/// NotificationBloc(this._fcmService) : super(NotificationInitial()) { +/// on(_onGetFcmToken); +/// on(_onSubscribeToTopic); +/// } +/// +/// Future _onGetFcmToken( +/// GetFcmToken event, +/// Emitter emit, +/// ) async { +/// final token = await _fcmService.getToken(); +/// emit(NotificationTokenLoaded(token)); +/// } +/// +/// Future _onSubscribeToTopic( +/// SubscribeToTopic event, +/// Emitter 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> 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())); +/// } +/// } +/// } diff --git a/lib/common/service/fcm_service.dart b/lib/common/service/fcm_service.dart new file mode 100644 index 0000000..cc1683d --- /dev/null +++ b/lib/common/service/fcm_service.dart @@ -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 _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 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 _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 _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 _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 getToken() async { + try { + return await _messaging.getToken(); + } catch (e) { + log('[FCM] Failed to get token: $e'); + return null; + } + } + + /// Subscribe ke topic tertentu + Future subscribeToTopic(String topic) async { + await _messaging.subscribeToTopic(topic); + log('[FCM] Subscribed to topic: $topic'); + } + + /// Unsubscribe dari topic tertentu + Future unsubscribeFromTopic(String topic) async { + await _messaging.unsubscribeFromTopic(topic); + log('[FCM] Unsubscribed from topic: $topic'); + } +} diff --git a/lib/injection.config.dart b/lib/injection.config.dart index c551594..ac50a33 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -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 {} diff --git a/lib/main.dart b/lib/main.dart index 0b9f9fd..5344ce9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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().initialize(); + runApp(const AppWidget()); }, (error, stack) { - // ✅ Ini udah bener FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); }, ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 13d89ca..0584489 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index f6cad54..1cc3eea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 2b0d43f..4dc4db1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/tool/generate_notification_icon.dart b/tool/generate_notification_icon.dart new file mode 100644 index 0000000..7ff0cac --- /dev/null +++ b/tool/generate_notification_icon.dart @@ -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'); +}