Add notification
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -50,3 +50,4 @@ app.*.map.json
|
||||
.env.*.local
|
||||
firestore.rules
|
||||
storage.rules
|
||||
/functions/node_modules
|
||||
125
functions/index.js
Normal file
125
functions/index.js
Normal file
@@ -0,0 +1,125 @@
|
||||
const functions = require("firebase-functions");
|
||||
const admin = require("firebase-admin");
|
||||
|
||||
admin.initializeApp();
|
||||
|
||||
// Helper function to send notifications
|
||||
async function sendNotificationToTripParticipants(tripId, title, body, excludeUserId) {
|
||||
try {
|
||||
const tripDoc = await admin.firestore().collection("trips").doc(tripId).get();
|
||||
if (!tripDoc.exists) {
|
||||
console.log(`Trip ${tripId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const trip = tripDoc.data();
|
||||
const participants = trip.participants || [];
|
||||
// Add creator if not in participants list (though usually they are)
|
||||
if (trip.createdBy && !participants.includes(trip.createdBy)) {
|
||||
participants.push(trip.createdBy);
|
||||
}
|
||||
|
||||
const tokens = [];
|
||||
|
||||
for (const userId of participants) {
|
||||
if (userId === excludeUserId) continue;
|
||||
|
||||
const userDoc = await admin.firestore().collection("users").doc(userId).get();
|
||||
if (userDoc.exists) {
|
||||
const userData = userDoc.data();
|
||||
if (userData.fcmToken) {
|
||||
tokens.push(userData.fcmToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens.length > 0) {
|
||||
const message = {
|
||||
notification: {
|
||||
title: title,
|
||||
body: body,
|
||||
},
|
||||
tokens: tokens,
|
||||
data: {
|
||||
tripId: tripId,
|
||||
click_action: "FLUTTER_NOTIFICATION_CLICK",
|
||||
},
|
||||
};
|
||||
|
||||
const response = await admin.messaging().sendMulticast(message);
|
||||
console.log(`${response.successCount} messages were sent successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending notification:", error);
|
||||
}
|
||||
}
|
||||
|
||||
exports.onActivityCreated = functions.firestore
|
||||
.document("trips/{tripId}/activities/{activityId}")
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const activity = snapshot.data();
|
||||
const tripId = context.params.tripId;
|
||||
const createdBy = activity.createdBy || "Unknown"; // Assuming createdBy field exists
|
||||
|
||||
// Fetch creator name if possible, otherwise use generic message
|
||||
let creatorName = "Quelqu'un";
|
||||
if (createdBy !== "Unknown") {
|
||||
const userDoc = await admin.firestore().collection("users").doc(createdBy).get();
|
||||
if (userDoc.exists) {
|
||||
creatorName = userDoc.data().prenom || "Quelqu'un";
|
||||
}
|
||||
}
|
||||
|
||||
await sendNotificationToTripParticipants(
|
||||
tripId,
|
||||
"Nouvelle activité !",
|
||||
`${creatorName} a ajouté une nouvelle activité : ${activity.title}`,
|
||||
createdBy
|
||||
);
|
||||
});
|
||||
|
||||
exports.onMessageCreated = functions.firestore
|
||||
.document("trips/{tripId}/messages/{messageId}")
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const message = snapshot.data();
|
||||
const tripId = context.params.tripId;
|
||||
const senderId = message.senderId;
|
||||
|
||||
let senderName = "Quelqu'un";
|
||||
if (senderId) {
|
||||
const userDoc = await admin.firestore().collection("users").doc(senderId).get();
|
||||
if (userDoc.exists) {
|
||||
senderName = userDoc.data().prenom || "Quelqu'un";
|
||||
}
|
||||
}
|
||||
|
||||
await sendNotificationToTripParticipants(
|
||||
tripId,
|
||||
"Nouveau message",
|
||||
`${senderName} : ${message.content}`,
|
||||
senderId
|
||||
);
|
||||
});
|
||||
|
||||
exports.onExpenseCreated = functions.firestore
|
||||
.document("trips/{tripId}/expenses/{expenseId}")
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const expense = snapshot.data();
|
||||
const tripId = context.params.tripId;
|
||||
const paidBy = expense.paidBy;
|
||||
|
||||
let payerName = "Quelqu'un";
|
||||
if (paidBy) {
|
||||
const userDoc = await admin.firestore().collection("users").doc(paidBy).get();
|
||||
if (userDoc.exists) {
|
||||
payerName = userDoc.data().prenom || "Quelqu'un";
|
||||
}
|
||||
}
|
||||
|
||||
await sendNotificationToTripParticipants(
|
||||
tripId,
|
||||
"Nouvelle dépense",
|
||||
`${payerName} a ajouté une dépense : ${expense.amount} ${expense.currency || '€'}`,
|
||||
paidBy
|
||||
);
|
||||
});
|
||||
4284
functions/package-lock.json
generated
Normal file
4284
functions/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
functions/package.json
Normal file
25
functions/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "functions",
|
||||
"description": "Cloud Functions for Travel Mate",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"serve": "firebase emulators:start --only functions",
|
||||
"shell": "firebase functions:shell",
|
||||
"start": "npm run shell",
|
||||
"deploy": "firebase deploy --only functions",
|
||||
"logs": "firebase functions:log"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"firebase-admin": "^11.8.0",
|
||||
"firebase-functions": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.15.0",
|
||||
"eslint-config-google": "^0.14.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -53,13 +53,6 @@ class SettingsContent extends StatelessWidget {
|
||||
onTap: () {},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: const Text('Langue'),
|
||||
trailing: const Icon(Icons.arrow_forward_ios),
|
||||
onTap: () {},
|
||||
),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.privacy_tip),
|
||||
title: const Text('Confidentialité'),
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import '../models/user.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/error_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
|
||||
/// Repository for authentication operations and user data management.
|
||||
///
|
||||
@@ -57,6 +58,7 @@ class AuthRepository {
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
await _saveFCMToken(firebaseUser.user!.uid);
|
||||
return await getUserFromFirestore(firebaseUser.user!.uid);
|
||||
} catch (e) {
|
||||
_errorService.showError(message: 'Utilisateur ou mot de passe incorrect');
|
||||
@@ -102,6 +104,9 @@ class AuthRepository {
|
||||
);
|
||||
|
||||
await _firestore.collection('users').doc(user.id).set(user.toMap());
|
||||
if (user.id != null) {
|
||||
await _saveFCMToken(user.id!);
|
||||
}
|
||||
return user;
|
||||
} catch (e) {
|
||||
_errorService.showError(message: 'Erreur lors de la création du compte');
|
||||
@@ -149,6 +154,9 @@ class AuthRepository {
|
||||
);
|
||||
|
||||
await _firestore.collection('users').doc(user.id).set(user.toMap());
|
||||
if (user.id != null) {
|
||||
await _saveFCMToken(user.id!);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
@@ -163,6 +171,9 @@ class AuthRepository {
|
||||
final firebaseUser = await _authService.signInWithGoogle();
|
||||
final user = await getUserFromFirestore(firebaseUser.user!.uid);
|
||||
if (user != null) {
|
||||
if (user.id != null) {
|
||||
await _saveFCMToken(user.id!);
|
||||
}
|
||||
return user;
|
||||
} else {
|
||||
throw Exception('Utilisateur non trouvé');
|
||||
@@ -211,6 +222,9 @@ class AuthRepository {
|
||||
);
|
||||
|
||||
await _firestore.collection('users').doc(user.id).set(user.toMap());
|
||||
if (user.id != null) {
|
||||
await _saveFCMToken(user.id!);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
@@ -225,6 +239,9 @@ class AuthRepository {
|
||||
final firebaseUser = await _authService.signInWithApple();
|
||||
final user = await getUserFromFirestore(firebaseUser.user!.uid);
|
||||
if (user != null) {
|
||||
if (user.id != null) {
|
||||
await _saveFCMToken(user.id!);
|
||||
}
|
||||
return user;
|
||||
} else {
|
||||
throw Exception('Utilisateur non trouvé');
|
||||
@@ -258,7 +275,7 @@ class AuthRepository {
|
||||
///
|
||||
/// [uid] - The Firebase user ID to look up
|
||||
///
|
||||
/// Returns the [User] model if found, null otherwise.
|
||||
/// Returns the [User] model if successful, null otherwise.
|
||||
Future<User?> getUserFromFirestore(String uid) async {
|
||||
try {
|
||||
final doc = await _firestore.collection('users').doc(uid).get();
|
||||
@@ -271,4 +288,19 @@ class AuthRepository {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method to save the FCM token for the authenticated user.
|
||||
Future<void> _saveFCMToken(String userId) async {
|
||||
try {
|
||||
final token = await NotificationService().getFCMToken();
|
||||
if (token != null) {
|
||||
await _firestore.collection('users').doc(userId).set({
|
||||
'fcmToken': token,
|
||||
}, SetOptions(merge: true));
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-blocking error
|
||||
print('Error saving FCM token: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
98
lib/services/notification_service.dart
Normal file
98
lib/services/notification_service.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:travel_mate/services/logger_service.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService _instance = NotificationService._internal();
|
||||
factory NotificationService() => _instance;
|
||||
NotificationService._internal();
|
||||
|
||||
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
|
||||
final FlutterLocalNotificationsPlugin _localNotifications =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
// Request permissions
|
||||
await _requestPermissions();
|
||||
|
||||
// Initialize local notifications
|
||||
const androidSettings = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
);
|
||||
const iosSettings = DarwinInitializationSettings();
|
||||
const initSettings = InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: iosSettings,
|
||||
);
|
||||
|
||||
await _localNotifications.initialize(
|
||||
initSettings,
|
||||
onDidReceiveNotificationResponse: (details) {
|
||||
// Handle notification tap
|
||||
LoggerService.info('Notification tapped: ${details.payload}');
|
||||
},
|
||||
);
|
||||
|
||||
// Handle foreground messages
|
||||
FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
|
||||
|
||||
_isInitialized = true;
|
||||
LoggerService.info('NotificationService initialized');
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
NotificationSettings settings = await _firebaseMessaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
LoggerService.info(
|
||||
'User granted permission: ${settings.authorizationStatus}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> getFCMToken() async {
|
||||
try {
|
||||
return await _firebaseMessaging.getToken();
|
||||
} catch (e) {
|
||||
LoggerService.error('Error getting FCM token: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleForegroundMessage(RemoteMessage message) async {
|
||||
LoggerService.info('Got a message whilst in the foreground!');
|
||||
LoggerService.info('Message data: ${message.data}');
|
||||
|
||||
if (message.notification != null) {
|
||||
LoggerService.info(
|
||||
'Message also contained a notification: ${message.notification}',
|
||||
);
|
||||
|
||||
// Show local notification
|
||||
const androidDetails = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
);
|
||||
const iosDetails = DarwinNotificationDetails();
|
||||
const details = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: iosDetails,
|
||||
);
|
||||
|
||||
await _localNotifications.show(
|
||||
message.hashCode,
|
||||
message.notification?.title,
|
||||
message.notification?.body,
|
||||
details,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
pubspec.lock
76
pubspec.lock
@@ -5,10 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: f871a7d1b686bea1f13722aa51ab31554d05c81f47054d6de48cc8c45153508b
|
||||
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.63"
|
||||
version: "1.3.64"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -293,10 +293,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "132e1c311bc41e7d387b575df0aacdf24efbf4930365eb61042be5bde3978f03"
|
||||
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
version: "4.2.1"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -309,10 +309,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: ecde2def458292404a4fcd3731ee4992fd631a0ec359d2d67c33baa8da5ec8ae
|
||||
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.3.0"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_messaging
|
||||
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "16.0.4"
|
||||
firebase_messaging_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_platform_interface
|
||||
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.7.4"
|
||||
firebase_messaging_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_messaging_web
|
||||
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
firebase_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -390,6 +414,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "19.5.0"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
flutter_local_notifications_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_windows
|
||||
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1157,6 +1213,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.1"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -61,6 +61,8 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
path: ^1.9.1
|
||||
image: ^4.5.4
|
||||
firebase_messaging: ^16.0.4
|
||||
flutter_local_notifications: ^19.5.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
Reference in New Issue
Block a user