feat: basic test web notifications using PWA

This commit is contained in:
sBubshait 2025-07-17 10:20:19 +03:00
parent cedea7fbb0
commit b2a4ffeb9b
17 changed files with 561 additions and 2 deletions

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@ -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

74
lib/firebase_options.dart Normal file
View File

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

View File

@ -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() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await NotificationService().initialize();
void main() {
runApp(MyApp());
}

View File

@ -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<FeedPage> {
},
),
),
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',
),
);
}
}

View File

@ -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<String> topics = [
'all',
'newposts',
'newinvites',
'invitesfollowup',
'appnews',
];
Future<void> initialize() async {
if (!kIsWeb) {
print('Notifications are only supported on web platform');
return;
}
_messaging = FirebaseMessaging.instance;
await _requestPermission();
await _subscribeToTopics();
await _setupMessageHandlers();
}
Future<void> _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<void> _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<void> _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<void> 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<void> 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<String?> getToken() async {
if (_messaging == null) return null;
return await _messaging!.getToken(vapidKey: vapidKey);
}
}
// Background message handler must be a top-level function
Future<void> _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}');
}
}

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -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"))
}

42
macos/Podfile Normal file
View File

@ -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

View File

@ -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"

View File

@ -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:

View File

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

View File

@ -34,5 +34,20 @@
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
<!-- Firebase Messaging Service Worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/firebase-messaging-sw.js')
.then(function(registration) {
console.log('Firebase Messaging Service Worker registered successfully:', registration);
})
.catch(function(error) {
console.log('Firebase Messaging Service Worker registration failed:', error);
});
});
}
</script>
</body>
</html>

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST