🐛 Trying to fix windows notification issue
This commit is contained in:
		| @@ -2,7 +2,6 @@ import 'dart:async'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
|   | ||||
| @@ -1,230 +1,47 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/models/account.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/widgets/app_notification.dart'; | ||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | ||||
|     FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||
|  | ||||
| void _onAppLifecycleChanged(AppLifecycleState state) { | ||||
|   _appLifecycleState = state; | ||||
| } | ||||
| // Conditional imports based on platform | ||||
| import 'notify.windows.dart' as windows_notify; | ||||
| import 'notify.universal.dart' as universal_notify; | ||||
|  | ||||
| // Platform-specific delegation | ||||
| Future<void> initializeLocalNotifications() async { | ||||
|   const AndroidInitializationSettings initializationSettingsAndroid = | ||||
|       AndroidInitializationSettings('@mipmap/ic_launcher'); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsIOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsMacOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const LinuxInitializationSettings initializationSettingsLinux = | ||||
|       LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|  | ||||
|   const WindowsInitializationSettings initializationSettingsWindows = | ||||
|       WindowsInitializationSettings( | ||||
|         appName: 'Island', | ||||
|         appUserModelId: 'dev.solsynth.solian', | ||||
|         guid: 'dev.solsynth.solian', | ||||
|       ); | ||||
|  | ||||
|   const InitializationSettings initializationSettings = InitializationSettings( | ||||
|     android: initializationSettingsAndroid, | ||||
|     iOS: initializationSettingsIOS, | ||||
|     macOS: initializationSettingsMacOS, | ||||
|     linux: initializationSettingsLinux, | ||||
|     windows: initializationSettingsWindows, | ||||
|   ); | ||||
|  | ||||
|   await flutterLocalNotificationsPlugin.initialize( | ||||
|     initializationSettings, | ||||
|     onDidReceiveNotificationResponse: (NotificationResponse response) async { | ||||
|       final payload = response.payload; | ||||
|       if (payload != null) { | ||||
|         if (payload.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           rootNavigatorKey.currentContext?.push(payload); | ||||
|         } else { | ||||
|           // External URLs | ||||
|           launchUrlString(payload); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   WidgetsBinding.instance.addObserver( | ||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class LifecycleEventHandler extends WidgetsBindingObserver { | ||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||
|  | ||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     onAppLifecycleChanged(state); | ||||
|   if (Platform.isWindows) { | ||||
|     return windows_notify.initializeLocalNotifications(); | ||||
|   } else { | ||||
|     return universal_notify.initializeLocalNotifications(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
| StreamSubscription setupNotificationListener( | ||||
|   BuildContext context, | ||||
|   WidgetRef ref, | ||||
| ) { | ||||
|   final ws = ref.watch(websocketProvider); | ||||
|   return ws.dataStream.listen((pkt) async { | ||||
|     if (pkt.type == "notifications.new") { | ||||
|       final notification = SnNotification.fromJson(pkt.data!); | ||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||
|         // App is focused, show in-app notification | ||||
|         log( | ||||
|           '[Notification] Showing in-app notification: ${notification.title}', | ||||
|         ); | ||||
|         showTopSnackBar( | ||||
|           globalOverlay.currentState!, | ||||
|           Center( | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxWidth: 480), | ||||
|               child: NotificationCard(notification: notification), | ||||
|             ), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             if (notification.meta['action_uri'] != null) { | ||||
|               var uri = notification.meta['action_uri'] as String; | ||||
|               if (uri.startsWith('/')) { | ||||
|                 // In-app routes | ||||
|                 rootNavigatorKey.currentContext?.push( | ||||
|                   notification.meta['action_uri'], | ||||
|                 ); | ||||
|               } else { | ||||
|                 // External URLs | ||||
|                 launchUrlString(uri); | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           onDismissed: () {}, | ||||
|           dismissType: DismissType.onSwipe, | ||||
|           displayDuration: const Duration(seconds: 5), | ||||
|           snackBarPosition: SnackBarPosition.top, | ||||
|           padding: EdgeInsets.only( | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             top: | ||||
|                 (!kIsWeb && | ||||
|                         (Platform.isMacOS || | ||||
|                             Platform.isWindows || | ||||
|                             Platform.isLinux)) | ||||
|                     ? 28 | ||||
|                     // ignore: use_build_context_synchronously | ||||
|                     : MediaQuery.of(context).padding.top + 16, | ||||
|             bottom: 16, | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         // App is in background, show system notification (only on supported platforms) | ||||
|         if (!kIsWeb && !Platform.isIOS) { | ||||
|           log( | ||||
|             '[Notification] Showing system notification: ${notification.title}', | ||||
|           ); | ||||
|           const AndroidNotificationDetails androidNotificationDetails = | ||||
|               AndroidNotificationDetails( | ||||
|                 'channel_id', | ||||
|                 'channel_name', | ||||
|                 channelDescription: 'channel_description', | ||||
|                 importance: Importance.max, | ||||
|                 priority: Priority.high, | ||||
|                 ticker: 'ticker', | ||||
|               ); | ||||
|           const NotificationDetails notificationDetails = NotificationDetails( | ||||
|             android: androidNotificationDetails, | ||||
|           ); | ||||
|           await flutterLocalNotificationsPlugin.show( | ||||
|             0, | ||||
|             notification.title, | ||||
|             notification.content, | ||||
|             notificationDetails, | ||||
|             payload: notification.meta['action_uri'] as String?, | ||||
|           ); | ||||
|         } else { | ||||
|           log( | ||||
|             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   if (Platform.isWindows) { | ||||
|     return windows_notify.setupNotificationListener(context, ref); | ||||
|   } else { | ||||
|     return universal_notify.setupNotificationListener(context, ref); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (!kIsWeb && Platform.isLinux) { | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|     alert: true, | ||||
|     badge: true, | ||||
|     sound: true, | ||||
|   ); | ||||
|  | ||||
|   String? deviceToken; | ||||
|   if (kIsWeb) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken( | ||||
|       vapidKey: | ||||
|           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", | ||||
|     ); | ||||
|   } else if (Platform.isAndroid) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken(); | ||||
|   } else if (Platform.isIOS) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); | ||||
|   } | ||||
|  | ||||
|   FirebaseMessaging.instance.onTokenRefresh | ||||
|       .listen((fcmToken) { | ||||
|         _putTokenToRemote(apiClient, fcmToken, 1); | ||||
|       }) | ||||
|       .onError((err) { | ||||
|         log("Failed to get firebase cloud messaging push token: $err"); | ||||
|       }); | ||||
|  | ||||
|   if (deviceToken != null) { | ||||
|     _putTokenToRemote( | ||||
|   if (Platform.isWindows) { | ||||
|     return windows_notify.subscribePushNotification( | ||||
|       apiClient, | ||||
|       deviceToken, | ||||
|       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||
|       detailedErrors: detailedErrors, | ||||
|     ); | ||||
|   } else { | ||||
|     return universal_notify.subscribePushNotification( | ||||
|       apiClient, | ||||
|       detailedErrors: detailedErrors, | ||||
|     ); | ||||
|   } else if (detailedErrors) { | ||||
|     throw Exception("Failed to get device token for push notifications."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _putTokenToRemote( | ||||
|   Dio apiClient, | ||||
|   String token, | ||||
|   int provider, | ||||
| ) async { | ||||
|   await apiClient.put( | ||||
|     "/pusher/notifications/subscription", | ||||
|     data: {"provider": provider, "device_token": token}, | ||||
|   ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								lib/services/notify.universal.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/models/account.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/widgets/app_notification.dart'; | ||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | ||||
|     FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||
|  | ||||
| void _onAppLifecycleChanged(AppLifecycleState state) { | ||||
|   _appLifecycleState = state; | ||||
| } | ||||
|  | ||||
| Future<void> initializeLocalNotifications() async { | ||||
|   const AndroidInitializationSettings initializationSettingsAndroid = | ||||
|       AndroidInitializationSettings('@mipmap/ic_launcher'); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsIOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsMacOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const LinuxInitializationSettings initializationSettingsLinux = | ||||
|       LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|  | ||||
|   const WindowsInitializationSettings initializationSettingsWindows = | ||||
|       WindowsInitializationSettings( | ||||
|         appName: 'Island', | ||||
|         appUserModelId: 'dev.solsynth.solian', | ||||
|         guid: 'dev.solsynth.solian', | ||||
|       ); | ||||
|  | ||||
|   const InitializationSettings initializationSettings = InitializationSettings( | ||||
|     android: initializationSettingsAndroid, | ||||
|     iOS: initializationSettingsIOS, | ||||
|     macOS: initializationSettingsMacOS, | ||||
|     linux: initializationSettingsLinux, | ||||
|     windows: initializationSettingsWindows, | ||||
|   ); | ||||
|  | ||||
|   await flutterLocalNotificationsPlugin.initialize( | ||||
|     initializationSettings, | ||||
|     onDidReceiveNotificationResponse: (NotificationResponse response) async { | ||||
|       final payload = response.payload; | ||||
|       if (payload != null) { | ||||
|         if (payload.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           rootNavigatorKey.currentContext?.push(payload); | ||||
|         } else { | ||||
|           // External URLs | ||||
|           launchUrlString(payload); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   WidgetsBinding.instance.addObserver( | ||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class LifecycleEventHandler extends WidgetsBindingObserver { | ||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||
|  | ||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     onAppLifecycleChanged(state); | ||||
|   } | ||||
| } | ||||
|  | ||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|   BuildContext context, | ||||
|   WidgetRef ref, | ||||
| ) { | ||||
|   final ws = ref.watch(websocketProvider); | ||||
|   return ws.dataStream.listen((pkt) async { | ||||
|     if (pkt.type == "notifications.new") { | ||||
|       final notification = SnNotification.fromJson(pkt.data!); | ||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||
|         // App is focused, show in-app notification | ||||
|         log( | ||||
|           '[Notification] Showing in-app notification: ${notification.title}', | ||||
|         ); | ||||
|         showTopSnackBar( | ||||
|           globalOverlay.currentState!, | ||||
|           Center( | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxWidth: 480), | ||||
|               child: NotificationCard(notification: notification), | ||||
|             ), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             if (notification.meta['action_uri'] != null) { | ||||
|               var uri = notification.meta['action_uri'] as String; | ||||
|               if (uri.startsWith('/')) { | ||||
|                 // In-app routes | ||||
|                 rootNavigatorKey.currentContext?.push( | ||||
|                   notification.meta['action_uri'], | ||||
|                 ); | ||||
|               } else { | ||||
|                 // External URLs | ||||
|                 launchUrlString(uri); | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           onDismissed: () {}, | ||||
|           dismissType: DismissType.onSwipe, | ||||
|           displayDuration: const Duration(seconds: 5), | ||||
|           snackBarPosition: SnackBarPosition.top, | ||||
|           padding: EdgeInsets.only( | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             top: | ||||
|                 (!kIsWeb && | ||||
|                         (Platform.isMacOS || | ||||
|                             Platform.isWindows || | ||||
|                             Platform.isLinux)) | ||||
|                     ? 28 | ||||
|                     // ignore: use_build_context_synchronously | ||||
|                     : MediaQuery.of(context).padding.top + 16, | ||||
|             bottom: 16, | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         // App is in background, show system notification (only on supported platforms) | ||||
|         if (!kIsWeb && !Platform.isIOS) { | ||||
|           log( | ||||
|             '[Notification] Showing system notification: ${notification.title}', | ||||
|           ); | ||||
|  | ||||
|           // Use flutter_local_notifications for universal platforms | ||||
|           const AndroidNotificationDetails androidNotificationDetails = | ||||
|               AndroidNotificationDetails( | ||||
|                 'channel_id', | ||||
|                 'channel_name', | ||||
|                 channelDescription: 'channel_description', | ||||
|                 importance: Importance.max, | ||||
|                 priority: Priority.high, | ||||
|                 ticker: 'ticker', | ||||
|               ); | ||||
|           const NotificationDetails notificationDetails = NotificationDetails( | ||||
|             android: androidNotificationDetails, | ||||
|           ); | ||||
|           await flutterLocalNotificationsPlugin.show( | ||||
|             0, | ||||
|             notification.title, | ||||
|             notification.content, | ||||
|             notificationDetails, | ||||
|             payload: notification.meta['action_uri'] as String?, | ||||
|           ); | ||||
|         } else { | ||||
|           log( | ||||
|             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (!kIsWeb && Platform.isLinux) { | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|     alert: true, | ||||
|     badge: true, | ||||
|     sound: true, | ||||
|   ); | ||||
|  | ||||
|   String? deviceToken; | ||||
|   if (kIsWeb) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken( | ||||
|       vapidKey: | ||||
|           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", | ||||
|     ); | ||||
|   } else if (Platform.isAndroid) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken(); | ||||
|   } else if (Platform.isIOS) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); | ||||
|   } | ||||
|  | ||||
|   FirebaseMessaging.instance.onTokenRefresh | ||||
|       .listen((fcmToken) { | ||||
|         _putTokenToRemote(apiClient, fcmToken, 1); | ||||
|       }) | ||||
|       .onError((err) { | ||||
|         log("Failed to get firebase cloud messaging push token: $err"); | ||||
|       }); | ||||
|  | ||||
|   if (deviceToken != null) { | ||||
|     _putTokenToRemote( | ||||
|       apiClient, | ||||
|       deviceToken, | ||||
|       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||
|     ); | ||||
|   } else if (detailedErrors) { | ||||
|     throw Exception("Failed to get device token for push notifications."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _putTokenToRemote( | ||||
|   Dio apiClient, | ||||
|   String token, | ||||
|   int provider, | ||||
| ) async { | ||||
|   await apiClient.put( | ||||
|     "/pusher/notifications/subscription", | ||||
|     data: {"provider": provider, "device_token": token}, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								lib/services/notify.windows.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| import 'package:island/route.dart'; | ||||
| import 'package:island/models/account.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/widgets/app_notification.dart'; | ||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:windows_notification/windows_notification.dart' | ||||
|     as windows_notification; | ||||
| import 'package:windows_notification/notification_message.dart'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
|  | ||||
| // Windows notification instance | ||||
| windows_notification.WindowsNotification? windowsNotification; | ||||
|  | ||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||
|  | ||||
| void _onAppLifecycleChanged(AppLifecycleState state) { | ||||
|   _appLifecycleState = state; | ||||
| } | ||||
|  | ||||
| Future<void> initializeLocalNotifications() async { | ||||
|   // Initialize Windows notification for Windows platform | ||||
|   windowsNotification = windows_notification.WindowsNotification( | ||||
|     applicationId: 'dev.solsynth.solian', | ||||
|   ); | ||||
|  | ||||
|   WidgetsBinding.instance.addObserver( | ||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class LifecycleEventHandler extends WidgetsBindingObserver { | ||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||
|  | ||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     onAppLifecycleChanged(state); | ||||
|   } | ||||
| } | ||||
|  | ||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|   BuildContext context, | ||||
|   WidgetRef ref, | ||||
| ) { | ||||
|   final ws = ref.watch(websocketProvider); | ||||
|   return ws.dataStream.listen((pkt) async { | ||||
|     if (pkt.type == "notifications.new") { | ||||
|       final notification = SnNotification.fromJson(pkt.data!); | ||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||
|         // App is focused, show in-app notification | ||||
|         log( | ||||
|           '[Notification] Showing in-app notification: ${notification.title}', | ||||
|         ); | ||||
|         showTopSnackBar( | ||||
|           globalOverlay.currentState!, | ||||
|           Center( | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxWidth: 480), | ||||
|               child: NotificationCard(notification: notification), | ||||
|             ), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             if (notification.meta['action_uri'] != null) { | ||||
|               var uri = notification.meta['action_uri'] as String; | ||||
|               if (uri.startsWith('/')) { | ||||
|                 // In-app routes | ||||
|                 rootNavigatorKey.currentContext?.push( | ||||
|                   notification.meta['action_uri'], | ||||
|                 ); | ||||
|               } else { | ||||
|                 // External URLs | ||||
|                 launchUrlString(uri); | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           onDismissed: () {}, | ||||
|           dismissType: DismissType.onSwipe, | ||||
|           displayDuration: const Duration(seconds: 5), | ||||
|           snackBarPosition: SnackBarPosition.top, | ||||
|           padding: EdgeInsets.only( | ||||
|             left: 16, | ||||
|             right: 16, | ||||
|             top: 28, // Windows specific padding | ||||
|             bottom: 16, | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         // App is in background, show Windows system notification | ||||
|         log( | ||||
|           '[Notification] Showing Windows system notification: ${notification.title}', | ||||
|         ); | ||||
|  | ||||
|         if (windowsNotification != null) { | ||||
|           // Use Windows notification for Windows platform | ||||
|           final notificationMessage = NotificationMessage.fromPluginTemplate( | ||||
|             DateTime.now().millisecondsSinceEpoch.toString(), // unique id | ||||
|             notification.title, | ||||
|             notification.content, | ||||
|             launch: notification.meta['action_uri'] as String?, | ||||
|           ); | ||||
|           await windowsNotification!.showNotificationPluginTemplate( | ||||
|             notificationMessage, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (!kIsWeb && Platform.isLinux) { | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|     alert: true, | ||||
|     badge: true, | ||||
|     sound: true, | ||||
|   ); | ||||
|  | ||||
|   String? deviceToken; | ||||
|   if (kIsWeb) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken( | ||||
|       vapidKey: | ||||
|           "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", | ||||
|     ); | ||||
|   } else if (Platform.isAndroid) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getToken(); | ||||
|   } else if (Platform.isIOS) { | ||||
|     deviceToken = await FirebaseMessaging.instance.getAPNSToken(); | ||||
|   } | ||||
|  | ||||
|   FirebaseMessaging.instance.onTokenRefresh | ||||
|       .listen((fcmToken) { | ||||
|         _putTokenToRemote(apiClient, fcmToken, 1); | ||||
|       }) | ||||
|       .onError((err) { | ||||
|         log("Failed to get firebase cloud messaging push token: $err"); | ||||
|       }); | ||||
|  | ||||
|   if (deviceToken != null) { | ||||
|     _putTokenToRemote( | ||||
|       apiClient, | ||||
|       deviceToken, | ||||
|       !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, | ||||
|     ); | ||||
|   } else if (detailedErrors) { | ||||
|     throw Exception("Failed to get device token for push notifications."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _putTokenToRemote( | ||||
|   Dio apiClient, | ||||
|   String token, | ||||
|   int provider, | ||||
| ) async { | ||||
|   await apiClient.put( | ||||
|     "/pusher/notifications/subscription", | ||||
|     data: {"provider": provider, "device_token": token}, | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -1281,10 +1281,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" | ||||
|       sha256: a45bef33deb24839a51fb85a4d9e504ead2b1ad1c4779d02d09bf6a8857cdd52 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.13+1" | ||||
|     version: "0.8.13+2" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1449,10 +1449,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: local_auth_android | ||||
|       sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" | ||||
|       sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.52" | ||||
|     version: "1.0.53" | ||||
|   local_auth_darwin: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2576,10 +2576,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" | ||||
|       sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.18" | ||||
|     version: "6.3.20" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2760,10 +2760,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: webrtc_interface | ||||
|       sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca" | ||||
|       sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.2+hotfix.2" | ||||
|     version: "1.3.0" | ||||
|   win32: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2780,6 +2780,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.0" | ||||
|   windows_notification: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: windows_notification | ||||
|       sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.0" | ||||
|   xdg_directories: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2806,4 +2814,4 @@ packages: | ||||
|     version: "3.1.3" | ||||
| sdks: | ||||
|   dart: ">=3.9.0 <4.0.0" | ||||
|   flutter: ">=3.32.0" | ||||
|   flutter: ">=3.35.0" | ||||
|   | ||||
| @@ -76,7 +76,7 @@ dependencies: | ||||
|   file_picker: ^10.3.2 | ||||
|   riverpod_annotation: ^2.6.1 | ||||
|   image_picker_platform_interface: ^2.11.0 | ||||
|   image_picker_android: ^0.8.13+1 | ||||
|   image_picker_android: ^0.8.13+2 | ||||
|   super_context_menu: ^0.9.1 | ||||
|   modal_bottom_sheet: ^3.0.0 | ||||
|   firebase_messaging: ^16.0.1 | ||||
| @@ -147,6 +147,7 @@ dependencies: | ||||
|   slide_countdown: ^2.0.2 | ||||
|   shelf: ^1.4.2 | ||||
|   shelf_web_socket: ^3.0.0 | ||||
|   windows_notification: ^1.3.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -31,6 +31,7 @@ | ||||
| #include <tray_manager/tray_manager_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
| #include <volume_controller/volume_controller_plugin_c_api.h> | ||||
| #include <windows_notification/windows_notification_plugin_c_api.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|   BitsdojoWindowPluginRegisterWithRegistrar( | ||||
| @@ -83,4 +84,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
|   VolumeControllerPluginCApiRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); | ||||
|   WindowsNotificationPluginCApiRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi")); | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   tray_manager | ||||
|   url_launcher_windows | ||||
|   volume_controller | ||||
|   windows_notification | ||||
| ) | ||||
|  | ||||
| list(APPEND FLUTTER_FFI_PLUGIN_LIST | ||||
|   | ||||
		Reference in New Issue
	
	Block a user