Add notification

This commit is contained in:
Van Leemput Dayron
2025-11-28 19:16:37 +01:00
parent 0668fcad57
commit 68f546d0e8
9 changed files with 4638 additions and 14 deletions

1
.gitignore vendored
View File

@@ -50,3 +50,4 @@ app.*.map.json
.env.*.local
firestore.rules
storage.rules
/functions/node_modules

125
functions/index.js Normal file
View 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

File diff suppressed because it is too large Load Diff

25
functions/package.json Normal file
View 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
}

View File

@@ -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é'),

View File

@@ -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');
}
}
}

View 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,
);
}
}
}

View File

@@ -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:

View File

@@ -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