wesal/frontend/lib/services/notification_service.dart
2025-07-20 10:01:32 +03:00

432 lines
14 KiB
Dart

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<String> topics = [
'all',
'newposts',
'newinvites',
'invitesfollowup',
'appnews',
];
Future<void> 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<void> 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<void> _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<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);
}
Future<NotificationStatus> 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<String?> _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<bool> sendNotificationToToken(
String token,
String title,
String body, {
Map<String, String>? 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<bool> sendNotificationToTopic(
String topic,
String title,
String body, {
Map<String, String>? 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<bool> 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<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}',
);
}
}