446 lines
12 KiB
Dart
446 lines
12 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';
|
|
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<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 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<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<void> _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<String?> _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<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,
|
|
// 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<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,
|
|
// 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<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}',
|
|
);
|
|
}
|
|
}
|