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'; 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 (JSON string) static const String _serviceAccountJson = ''' { "type": "service_account", "project_id": "wesalapp-bc676", "private_key_id": "90f1ac73e8f8b59e5ad7b0caed638ffa57675d39", "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCe/p4CkGeDft2F\\nLMDNyjbU0g1mLQ4bhaqy2WOHr8BX7kHt/WlpB3WeaSUItW2cXFtXhI66mW7Ejlg7\\n/Los4YLpVXner4P5Vj7sOVrhD1Jt9qmdsEjfsTaKs0t06tfUE9PdFqBl6F5HG9B1\\nkwdV2mQfiurpTb+zUXLpfvMbGT1ny7gJSXhYpRaC6UJUy7DNsxeChviXy1tPGr/r\\nmIaENt0KAZzRSSCp0bzN+gKoAm6qyOGYJ9++HlGXTDYmkGPkdc0lbSWjA2/+K7zR\\nWQNy4oOXCstdHL4lMp87UT8lL1ZnntROELyTBUslmYywxXtZinkOgBrWeUifqLW4\\nbcHy6jJ5AgMBAAECggEACSBwjZEggAneSXDCOI3tC9Zq8nyPnMDVhaK49eb+0Y1Z\\nt4GedWr6M3exqohPnHQowiNX1hpMo3fQVNEzFrRzQVWow0Gr/7oVrpW0Q8sPXkSU\\ng/rElCKmENwt7q40aXYh6UUNPAxUrRxJoRYpi6IXsT/WMEJISNDaGdExv1J5leWi\\no8Op2AhREV/ukFpTnNfzfWKjXN+i3psNCYqZAdAh+a4ZJH0vNpiaCq6uVFw7HzdR\\nF2mMha+MYtp2VupzDJ8DkL4ExQl1KCOCllahzqVrhqmEhhhTfDxPOj5q24Hnfn1p\\npzR+fC8Ex0dGB/j+9jKjQyilo/LzEJdrPxt/9QUdiQKBgQDQ2L7sQsEIihVUq3lW\\n3Od2GNjnloTo24rNLl8hxwgl9EA2VnG50gZPGAkcUA2eeA23T6C6gPbm0QBsfqSP\\nPNTbd6UYF508EE7PMaScFoJMcKDin8x4q5tfVjgao2r8LCOUXfU1ktreQR3bIMKk\\nsgsgBazfBT84ioxvDwoD+4EJqwKBgQDC5HEfouCTxvKwzmpxZ+kkHKPO5QjyxnOH\\nLnt/7jx5t7T1nWNUnusYj+uowaqKAmLz7kBhCbRGADdKuBAr0hY/aEOG3zhTH35K\\nc+8wJ3yDFkh8BhFsOYCxopIPAjEGxw5cnM4+r8QDqy61j4hsR9kSr40WwhRuSxu+\\nHqe38Vl4awKBgBYFJGxIxY2e8YzR36NW+1iqWgRhDHZ433Ou1fz7vVIzJKoWBzuu\\nd1fTkvJXRnhU9C1Fyg6gFmhT1RWbbMJliZPyU4fsxXlVxtl1xINopChnH6+FZcu7\\nXFB7CMNWQ6t/A+la1sXlTApvFzTJiXxQAXhI4OdK6FWP1irHjSjKVdqtAoGAcLQA\\ngyYCrxKux/YmcfyAQ2TYic3DNfnzVypXOuz/RfgpipwAlC/ujl60Dfwo7fRhWuTd\\nkAA3ov9++hOlLmIogXR/EGDHxrIAq3eNy5AaHghl1GsB6k76kD8OLeW7ikrUkFQR\\npQip1uFIerBNWSjXbEne0llbzUhb+77oiKPmdI8CgYBRBIUC4d/T3cgmF9uRqlpL\\nSsa8IxqohU1huAucf5UqNP9OXZ4TtZhM0PbR5SNHcGBXAl+XowCtUeCE9LlWWzpg\\ne/xTu4Mu1hwnRZ8ybujAyTPnN8KEfK8HDjORZnxzzdyPkO4BN+KOH6WZyKhKDTuR\\n6KCch1ooA8YlV43vchpKXg==\\n-----END PRIVATE KEY-----\\n", "client_email": "firebase-adminsdk-fbsvc@wesalapp-bc676.iam.gserviceaccount.com", "client_id": "112586481303824467416", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40wesalapp-bc676.iam.gserviceaccount.com", "universe_domain": "googleapis.com" } '''; 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, 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); } 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 _getAccessToken() async { try { 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, 'notification': {'title': title, 'body': body}, 'data': data ?? {}, 'webpush': { 'notification': { 'icon': 'icons/ios/192.png', 'badge': 'icons/ios/192.png', }, }, }, }; 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, 'notification': {'title': title, 'body': body}, 'data': data ?? {}, 'webpush': { 'notification': { 'icon': 'icons/ios/192.png', 'badge': 'icons/ios/192.png', }, }, }, }; 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}', ); } }