import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:googleapis_auth/auth_io.dart'; import 'package:flutter/services.dart' show rootBundle; class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; NotificationService._internal(); FirebaseMessaging? _messaging; static const String vapidKey = 'BKrFSFm2cb2DNtEpTNmEy3acpi2ziRA5DhzKSyjshqAWANaoydztUTa0Cn3jwh1v7KN6pHUQfsODFXUWrKG6aSU'; // Firebase project configuration static const String _projectId = 'wesalapp-bc676'; // Service account credentials loaded from file static String? _serviceAccountJson; static const List topics = [ 'all', 'newposts', 'newinvites', 'invitesfollowup', 'appnews', ]; Future initialize() async { try { if (!kIsWeb) { print('Notifications are only supported on web platform'); return; } if (!_isNotificationSupported()) { print('Notifications are not supported in this browser'); return; } _messaging = FirebaseMessaging.instance; await _setupMessageHandlers(); } catch (e) { print('Error initializing notifications: $e'); } } Future requestPermissionAndSetup() async { try { if (!kIsWeb) { print('Notifications are only supported on web platform'); return; } if (!_isNotificationSupported()) { print('Notifications are not supported in this browser'); return; } if (_messaging == null) { _messaging = FirebaseMessaging.instance; await _setupMessageHandlers(); } await _requestPermission(); await _subscribeToTopics(); } catch (e) { print('Error requesting notification permission: $e'); } } bool _isNotificationSupported() { if (!kIsWeb) return false; // Check if the browser supports notifications try { // This will throw an error if notifications are not supported return true; // Firebase already handles browser support checks } catch (e) { return false; } } Future _requestPermission() async { if (_messaging == null) return; try { 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'); } } catch (e) { print('Error requesting notification permission: $e'); } } 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 PWA, we rely on the service worker to show notifications // This method is only for logging since service worker handles the UI print('Foreground notification received - Title: ${notification.title}'); print('Foreground notification received - Body: ${notification.body}'); // Note: Do not show browser notification here as service worker handles it } 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); } Future getNotificationStatus() async { if (!kIsWeb) { return NotificationStatus.notSupported; } if (!_isNotificationSupported()) { return NotificationStatus.notSupported; } if (_messaging == null) { return NotificationStatus.notInitialized; } try { NotificationSettings settings = await _messaging! .getNotificationSettings(); switch (settings.authorizationStatus) { case AuthorizationStatus.authorized: return NotificationStatus.enabled; case AuthorizationStatus.provisional: return NotificationStatus.provisional; case AuthorizationStatus.denied: return NotificationStatus.denied; case AuthorizationStatus.notDetermined: return NotificationStatus.notDetermined; default: return NotificationStatus.denied; } } catch (e) { print('Error getting notification status: $e'); return NotificationStatus.error; } } Future _loadServiceAccountCredentials() async { if (_serviceAccountJson != null) return; try { _serviceAccountJson = await rootBundle.loadString( 'firebase-service-account.json', ); } catch (e) { print('Error loading service account credentials: $e'); _serviceAccountJson = null; } } Future _getAccessToken() async { try { await _loadServiceAccountCredentials(); if (_serviceAccountJson == null) { print('Service account credentials not loaded'); return null; } final credentials = ServiceAccountCredentials.fromJson( _serviceAccountJson!, ); final client = await clientViaServiceAccount(credentials, [ 'https://www.googleapis.com/auth/firebase.messaging', ]); final token = client.credentials.accessToken.data; client.close(); return token; } catch (e) { print('Error getting access token: $e'); return null; } } Future sendNotificationToToken( String token, String title, String body, { Map? data, }) async { try { final accessToken = await _getAccessToken(); if (accessToken == null) { print('Failed to get access token. Cannot send notifications.'); return false; } final url = Uri.parse( 'https://fcm.googleapis.com/v1/projects/$_projectId/messages:send', ); final headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken', }; final payload = { 'message': { 'token': token, // Use data-only for PWA to prevent duplicate notifications 'data': { 'title': title, 'body': body, ...(data ?? {}), }, 'webpush': { 'headers': { 'Urgency': 'normal', }, }, }, }; print('Sending notification to token: ${token.substring(0, 20)}...'); print('Payload: ${json.encode(payload)}'); final response = await http.post( url, headers: headers, body: json.encode(payload), ); if (response.statusCode == 200) { print('Notification sent successfully to token!'); print('Response: ${response.body}'); return true; } else { print('Failed to send notification. Status: ${response.statusCode}'); print('Response: ${response.body}'); return false; } } catch (e) { print('Error sending notification: $e'); return false; } } Future sendNotificationToTopic( String topic, String title, String body, { Map? data, }) async { try { final accessToken = await _getAccessToken(); if (accessToken == null) { print('Failed to get access token. Cannot send notifications.'); return false; } final url = Uri.parse( 'https://fcm.googleapis.com/v1/projects/$_projectId/messages:send', ); final headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken', }; final payload = { 'message': { 'topic': topic, // Use data-only for PWA to prevent duplicate notifications 'data': { 'title': title, 'body': body, ...(data ?? {}), }, 'webpush': { 'headers': { 'Urgency': 'normal', }, }, }, }; final response = await http.post( url, headers: headers, body: json.encode(payload), ); if (response.statusCode == 200) { print('Notification sent successfully to topic: $topic'); print('Response: ${response.body}'); return true; } else { print('Failed to send notification. Status: ${response.statusCode}'); print('Response: ${response.body}'); return false; } } catch (e) { print('Error sending notification: $e'); return false; } } Future sendCoffeeInviteAcceptedNotification() async { // Send to specific token for testing const testToken = 'evEF6UT53IkV98ku-MR1bH:APA91bGE52wWTUBm_cOoQ0GeGy6gUgpHhaZquB0Y0L_eyoosFZvE8lMuiQrhhVZ81tw-lB_baC7en0Bk1JIVGAuIj6KyFHKeuoPJVbntHh7HtEZDBEeJRkc'; return await sendNotificationToToken( testToken, 'Coffee Time! ☕', 'You\'re set for coffee! Your invitation has been accepted.', data: { 'type': 'coffee_invite_accepted', 'timestamp': DateTime.now().toIso8601String(), }, ); } } enum NotificationStatus { enabled, provisional, denied, notDetermined, notSupported, notInitialized, error, } // 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}', ); } }