From b2a4ffeb9b244a5eb8d9f85027cd3d920c405014 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Thu, 17 Jul 2025 10:20:19 +0300 Subject: [PATCH] feat: basic test web notifications using PWA --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 43 +++++ lib/firebase_options.dart | 74 ++++++++ lib/main.dart | 12 +- lib/screens/pages/feed_page.dart | 12 ++ lib/services/notification_service.dart | 163 ++++++++++++++++++ macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Podfile | 42 +++++ pubspec.lock | 79 ++++++++- pubspec.yaml | 2 + web/firebase-messaging-sw.js | 109 ++++++++++++ web/index.html | 15 ++ .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 17 files changed, 561 insertions(+), 2 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/firebase_options.dart create mode 100644 lib/services/notification_service.dart create mode 100644 macos/Podfile create mode 100644 web/firebase-messaging-sw.js diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..45e9666 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,74 @@ +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA', + appId: '1:865533380916:web:46725564ea0e1d4e70fd61', + messagingSenderId: '865533380916', + projectId: 'wesalapp-bc676', + authDomain: 'wesalapp-bc676.firebaseapp.com', + storageBucket: 'wesalapp-bc676.firebasestorage.app', + measurementId: 'G-V4BQJQB24E', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA', + appId: '1:865533380916:android:46725564ea0e1d4e70fd61', + messagingSenderId: '865533380916', + projectId: 'wesalapp-bc676', + storageBucket: 'wesalapp-bc676.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA', + appId: '1:865533380916:ios:46725564ea0e1d4e70fd61', + messagingSenderId: '865533380916', + projectId: 'wesalapp-bc676', + storageBucket: 'wesalapp-bc676.firebasestorage.app', + iosBundleId: 'com.example.wesalApp', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA', + appId: '1:865533380916:macos:46725564ea0e1d4e70fd61', + messagingSenderId: '865533380916', + projectId: 'wesalapp-bc676', + storageBucket: 'wesalapp-bc676.firebasestorage.app', + iosBundleId: 'com.example.wesalApp', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA', + appId: '1:865533380916:windows:46725564ea0e1d4e70fd61', + messagingSenderId: '865533380916', + projectId: 'wesalapp-bc676', + storageBucket: 'wesalapp-bc676.firebasestorage.app', + ); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index b2c478e..6447fe0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; import 'screens/home_screen.dart'; +import 'services/notification_service.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + await NotificationService().initialize(); + runApp(MyApp()); } diff --git a/lib/screens/pages/feed_page.dart b/lib/screens/pages/feed_page.dart index 8d77ebc..a345717 100644 --- a/lib/screens/pages/feed_page.dart +++ b/lib/screens/pages/feed_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../services/notification_service.dart'; class FeedPage extends StatefulWidget { @override @@ -105,6 +106,17 @@ class _FeedPageState extends State { }, ), ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + final token = await NotificationService().getToken(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('FCM Token: ${token ?? "Not available"}')), + ); + }, + backgroundColor: Color(0xFF6A4C93), + child: Icon(Icons.notifications, color: Colors.white), + tooltip: 'Show FCM Token', + ), ); } } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 0000000..565fdbe --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,163 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; + +class NotificationService { + static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; + NotificationService._internal(); + + FirebaseMessaging? _messaging; + + static const String vapidKey = 'BKrFSFm2cb2DNtEpTNmEy3acpi2ziRA5DhzKSyjshqAWANaoydztUTa0Cn3jwh1v7KN6pHUQfsODFXUWrKG6aSU'; + + static const List topics = [ + 'all', + 'newposts', + 'newinvites', + 'invitesfollowup', + 'appnews', + ]; + + Future initialize() async { + if (!kIsWeb) { + print('Notifications are only supported on web platform'); + return; + } + + _messaging = FirebaseMessaging.instance; + + await _requestPermission(); + await _subscribeToTopics(); + await _setupMessageHandlers(); + } + + Future _requestPermission() async { + if (_messaging == null) return; + + NotificationSettings settings = await _messaging!.requestPermission( + alert: true, + announcement: false, + badge: true, + carPlay: false, + criticalAlert: false, + provisional: false, + sound: true, + ); + + print('User granted permission: ${settings.authorizationStatus}'); + + if (settings.authorizationStatus == AuthorizationStatus.authorized) { + print('User granted permission for notifications'); + } else if (settings.authorizationStatus == AuthorizationStatus.provisional) { + print('User granted provisional permission for notifications'); + } else { + print('User declined or has not accepted permission for notifications'); + } + } + + Future _subscribeToTopics() async { + if (_messaging == null) return; + + for (String topic in topics) { + try { + await _messaging!.subscribeToTopic(topic); + print('Subscribed to topic: $topic'); + } catch (e) { + print('Error subscribing to topic $topic: $e'); + } + } + } + + Future _setupMessageHandlers() async { + if (_messaging == null) return; + + // Handle foreground messages + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print('Got a message whilst in the foreground!'); + print('Message data: ${message.data}'); + + if (message.notification != null) { + print('Message also contained a notification: ${message.notification}'); + _showNotification(message.notification!); + } + }); + + // Handle background messages + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + + // Handle notification taps + FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { + print('A new onMessageOpenedApp event was published!'); + print('Message data: ${message.data}'); + _handleNotificationTap(message); + }); + + // Get the initial message if the app was opened from a notification + RemoteMessage? initialMessage = await _messaging!.getInitialMessage(); + if (initialMessage != null) { + print('App opened from notification: ${initialMessage.data}'); + _handleNotificationTap(initialMessage); + } + + // Get the FCM token for this device + String? token = await _messaging!.getToken(vapidKey: vapidKey); + print('FCM Token: $token'); + } + + void _showNotification(RemoteNotification notification) { + // For web, we rely on the service worker to show notifications + // This is mainly for logging and debugging + print('Notification Title: ${notification.title}'); + print('Notification Body: ${notification.body}'); + } + + void _handleNotificationTap(RemoteMessage message) { + // Handle notification tap actions here + print('Notification tapped: ${message.data}'); + + // You can navigate to specific screens based on the notification data + // For example: + // if (message.data['type'] == 'newpost') { + // // Navigate to posts screen + // } else if (message.data['type'] == 'newinvite') { + // // Navigate to invitations screen + // } + } + + Future unsubscribeFromTopic(String topic) async { + if (_messaging == null) return; + + try { + await _messaging!.unsubscribeFromTopic(topic); + print('Unsubscribed from topic: $topic'); + } catch (e) { + print('Error unsubscribing from topic $topic: $e'); + } + } + + Future subscribeToTopic(String topic) async { + if (_messaging == null) return; + + try { + await _messaging!.subscribeToTopic(topic); + print('Subscribed to topic: $topic'); + } catch (e) { + print('Error subscribing to topic $topic: $e'); + } + } + + Future getToken() async { + if (_messaging == null) return null; + return await _messaging!.getToken(vapidKey: vapidKey); + } +} + +// Background message handler must be a top-level function +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + print('Handling a background message: ${message.messageId}'); + print('Message data: ${message.data}'); + + if (message.notification != null) { + print('Background message contained a notification: ${message.notification}'); + } +} \ No newline at end of file diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..9ad992d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import firebase_core +import firebase_messaging func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index eaa659f..23f560d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: a5788040810bd84400bc209913fbc40f388cded7cdf95ee2f5d2bff7e38d5241 + url: "https://pub.dev" + source: hosted + version: "1.3.58" async: dependency: transitive description: @@ -57,6 +65,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: c6e8a6bf883d8ddd0dec39be90872daca65beaa6f4cff0051ed3b16c56b82e9f + url: "https://pub.dev" + source: hosted + version: "3.15.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5dbc900677dcbe5873d22ad7fbd64b047750124f1f9b7ebe2a33b9ddccc838eb" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "0f3363f97672eb9f65609fa00ed2f62cc8ec93e7e2d4def99726f9165d3d8a73" + url: "https://pub.dev" + source: hosted + version: "15.2.9" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "7a05ef119a14c5f6a9440d1e0223bcba20c8daf555450e119c4c477bf2c3baa9" + url: "https://pub.dev" + source: hosted + version: "4.6.9" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: a4547f76da2a905190f899eb4d0150e1d0fd52206fce469d9f05ae15bb68b2c5 + url: "https://pub.dev" + source: hosted + version: "3.10.9" flutter: dependency: "direct main" description: flutter @@ -75,6 +131,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -139,6 +200,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -208,6 +277,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b2b4e0..7d5b271 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 + firebase_core: ^3.15.1 + firebase_messaging: ^15.2.9 dev_dependencies: flutter_test: diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js new file mode 100644 index 0000000..c31a6bc --- /dev/null +++ b/web/firebase-messaging-sw.js @@ -0,0 +1,109 @@ +importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-messaging-compat.js'); + +// Firebase configuration +const firebaseConfig = { + apiKey: "AIzaSyB5C-HmErqNFRlJwM4S4wfs-arMkJRVmGA", + authDomain: "wesalapp-bc676.firebaseapp.com", + projectId: "wesalapp-bc676", + storageBucket: "wesalapp-bc676.firebasestorage.app", + messagingSenderId: "865533380916", + appId: "1:865533380916:web:46725564ea0e1d4e70fd61", + measurementId: "G-V4BQJQB24E" +}; + +// Initialize Firebase +firebase.initializeApp(firebaseConfig); + +// Initialize Firebase Messaging +const messaging = firebase.messaging(); + +// Handle background messages +messaging.onBackgroundMessage((payload) => { + console.log('Received background message ', payload); + + const notificationTitle = payload.notification?.title || 'Wesal App'; + const notificationOptions = { + body: payload.notification?.body || 'You have a new notification', + icon: '/icons/Icon-192.png', + badge: '/icons/Icon-192.png', + data: payload.data, + tag: payload.data?.type || 'general', + requireInteraction: true, + actions: [ + { + action: 'open', + title: 'Open App', + }, + { + action: 'close', + title: 'Close', + } + ] + }; + + return self.registration.showNotification(notificationTitle, notificationOptions); +}); + +// Handle notification clicks +self.addEventListener('notificationclick', (event) => { + console.log('Notification clicked:', event); + + event.notification.close(); + + if (event.action === 'close') { + return; + } + + // Handle notification click - open the app + event.waitUntil( + clients.matchAll({ type: 'window' }).then((clientList) => { + // If the app is already open, focus it + for (const client of clientList) { + if (client.url === '/' && 'focus' in client) { + return client.focus(); + } + } + + // Otherwise, open a new window + if (clients.openWindow) { + return clients.openWindow('/'); + } + }) + ); +}); + +// Handle push events +self.addEventListener('push', (event) => { + console.log('Push event received:', event); + + if (event.data) { + const data = event.data.json(); + console.log('Push data:', data); + + const notificationTitle = data.notification?.title || 'Wesal App'; + const notificationOptions = { + body: data.notification?.body || 'You have a new notification', + icon: '/icons/Icon-192.png', + badge: '/icons/Icon-192.png', + data: data.data, + tag: data.data?.type || 'general', + requireInteraction: true, + }; + + event.waitUntil( + self.registration.showNotification(notificationTitle, notificationOptions) + ); + } +}); + +// Handle service worker activation +self.addEventListener('activate', (event) => { + console.log('Service worker activated'); +}); + +// Handle service worker installation +self.addEventListener('install', (event) => { + console.log('Service worker installed'); + self.skipWaiting(); +}); \ No newline at end of file diff --git a/web/index.html b/web/index.html index b8161c0..b6c319c 100644 --- a/web/index.html +++ b/web/index.html @@ -34,5 +34,20 @@ + + + diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..1a82e7d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..fa8a39b 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST