feat: Upgrade Firebase Functions dependencies, enhance notification service with APNS support and FCM

This commit is contained in:
Van Leemput Dayron
2025-11-28 20:18:24 +01:00
parent 68f546d0e8
commit b4bcc8f498
14 changed files with 7101 additions and 4405 deletions

5
.firebaserc Normal file
View File

@@ -0,0 +1,5 @@
{
"projects": {
"default": "travelmate-a47f5"
}
}

View File

@@ -22,6 +22,7 @@ android {
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
} }
kotlinOptions { kotlinOptions {
@@ -59,6 +60,10 @@ android {
} }
} }
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter { flutter {
source = "../.." source = "../.."
} }

View File

@@ -1 +1,37 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"travelmate-a47f5","appId":"1:521527250907:android:be3db7fc84f053ec7da1fe","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"travelmate-a47f5","configurations":{"android":"1:521527250907:android:be3db7fc84f053ec7da1fe","ios":"1:521527250907:ios:64b41be39c54db1c7da1fe","windows":"1:521527250907:web:53ff98bcdb8c218f7da1fe"}}}}}} {
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "travelmate-a47f5",
"appId": "1:521527250907:android:be3db7fc84f053ec7da1fe",
"fileOutput": "android/app/google-services.json"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "travelmate-a47f5",
"configurations": {
"android": "1:521527250907:android:be3db7fc84f053ec7da1fe",
"ios": "1:521527250907:ios:64b41be39c54db1c7da1fe",
"windows": "1:521527250907:web:53ff98bcdb8c218f7da1fe"
}
}
}
}
},
"functions": [
{
"source": "functions",
"codebase": "default",
"disallowLegacyRuntimeConfig": true,
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log",
"*.local"
]
}
]
}

2
functions/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
*.local

View File

@@ -1,27 +1,14 @@
const functions = require("firebase-functions"); const functions = require("firebase-functions/v1");
const admin = require("firebase-admin"); const admin = require("firebase-admin");
admin.initializeApp(); admin.initializeApp();
// Helper function to send notifications // Helper function to send notifications to a list of users
async function sendNotificationToTripParticipants(tripId, title, body, excludeUserId) { async function sendNotificationToUsers(userIds, title, body, excludeUserId, data = {}) {
try { 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 = []; const tokens = [];
for (const userId of participants) { for (const userId of userIds) {
if (userId === excludeUserId) continue; if (userId === excludeUserId) continue;
const userDoc = await admin.firestore().collection("users").doc(userId).get(); const userDoc = await admin.firestore().collection("users").doc(userId).get();
@@ -41,8 +28,8 @@ async function sendNotificationToTripParticipants(tripId, title, body, excludeUs
}, },
tokens: tokens, tokens: tokens,
data: { data: {
tripId: tripId,
click_action: "FLUTTER_NOTIFICATION_CLICK", click_action: "FLUTTER_NOTIFICATION_CLICK",
...data
}, },
}; };
@@ -55,13 +42,31 @@ async function sendNotificationToTripParticipants(tripId, title, body, excludeUs
} }
exports.onActivityCreated = functions.firestore exports.onActivityCreated = functions.firestore
.document("trips/{tripId}/activities/{activityId}") .document("activities/{activityId}")
.onCreate(async (snapshot, context) => { .onCreate(async (snapshot, context) => {
const activity = snapshot.data(); const activity = snapshot.data();
const tripId = context.params.tripId; const tripId = activity.tripId;
const createdBy = activity.createdBy || "Unknown"; // Assuming createdBy field exists const createdBy = activity.createdBy || "Unknown";
// Fetch creator name if possible, otherwise use generic message if (!tripId) {
console.log("No tripId found in activity");
return;
}
// Fetch trip to get participants
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 || [];
if (trip.createdBy && !participants.includes(trip.createdBy)) {
participants.push(trip.createdBy);
}
// Fetch creator name
let creatorName = "Quelqu'un"; let creatorName = "Quelqu'un";
if (createdBy !== "Unknown") { if (createdBy !== "Unknown") {
const userDoc = await admin.firestore().collection("users").doc(createdBy).get(); const userDoc = await admin.firestore().collection("users").doc(createdBy).get();
@@ -70,56 +75,72 @@ exports.onActivityCreated = functions.firestore
} }
} }
await sendNotificationToTripParticipants( await sendNotificationToUsers(
tripId, participants,
"Nouvelle activité !", "Nouvelle activité !",
`${creatorName} a ajouté une nouvelle activité : ${activity.title}`, `${creatorName} a ajouté une nouvelle activité : ${activity.name || activity.title}`,
createdBy createdBy,
{ tripId: tripId }
); );
}); });
exports.onMessageCreated = functions.firestore exports.onMessageCreated = functions.firestore
.document("trips/{tripId}/messages/{messageId}") .document("groups/{groupId}/messages/{messageId}")
.onCreate(async (snapshot, context) => { .onCreate(async (snapshot, context) => {
const message = snapshot.data(); const message = snapshot.data();
const tripId = context.params.tripId; const groupId = context.params.groupId;
const senderId = message.senderId; const senderId = message.senderId;
let senderName = "Quelqu'un"; // Fetch group to get members
if (senderId) { const groupDoc = await admin.firestore().collection("groups").doc(groupId).get();
const userDoc = await admin.firestore().collection("users").doc(senderId).get(); if (!groupDoc.exists) {
if (userDoc.exists) { console.log(`Group ${groupId} not found`);
senderName = userDoc.data().prenom || "Quelqu'un"; return;
}
} }
await sendNotificationToTripParticipants( const group = groupDoc.data();
tripId, const memberIds = group.memberIds || [];
let senderName = message.senderName || "Quelqu'un";
await sendNotificationToUsers(
memberIds,
"Nouveau message", "Nouveau message",
`${senderName} : ${message.content}`, `${senderName} : ${message.text}`,
senderId senderId,
{ groupId: groupId }
); );
}); });
exports.onExpenseCreated = functions.firestore exports.onExpenseCreated = functions.firestore
.document("trips/{tripId}/expenses/{expenseId}") .document("expenses/{expenseId}")
.onCreate(async (snapshot, context) => { .onCreate(async (snapshot, context) => {
const expense = snapshot.data(); const expense = snapshot.data();
const tripId = context.params.tripId; const groupId = expense.groupId;
const paidBy = expense.paidBy; const paidBy = expense.paidById || expense.paidBy;
let payerName = "Quelqu'un"; if (!groupId) {
if (paidBy) { console.log("No groupId found in expense");
const userDoc = await admin.firestore().collection("users").doc(paidBy).get(); return;
if (userDoc.exists) {
payerName = userDoc.data().prenom || "Quelqu'un";
}
} }
await sendNotificationToTripParticipants( // Fetch group to get members
tripId, const groupDoc = await admin.firestore().collection("groups").doc(groupId).get();
if (!groupDoc.exists) {
console.log(`Group ${groupId} not found`);
return;
}
const group = groupDoc.data();
const memberIds = group.memberIds || [];
let payerName = expense.paidByName || "Quelqu'un";
await sendNotificationToUsers(
memberIds,
"Nouvelle dépense", "Nouvelle dépense",
`${payerName} a ajouté une dépense : ${expense.amount} ${expense.currency || '€'}`, `${payerName} a ajouté une dépense : ${expense.amount} ${expense.currency || '€'}`,
paidBy paidBy,
{ groupId: groupId }
); );
}); });

11099
functions/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,23 @@
{ {
"name": "functions", "name": "functions",
"description": "Cloud Functions for Travel Mate", "description": "Cloud Functions for Firebase",
"scripts": { "scripts": {
"lint": "eslint .", "serve": "firebase emulators:start --only functions",
"serve": "firebase emulators:start --only functions", "shell": "firebase functions:shell",
"shell": "firebase functions:shell", "start": "npm run shell",
"start": "npm run shell", "deploy": "firebase deploy --only functions",
"deploy": "firebase deploy --only functions", "logs": "firebase functions:log"
"logs": "firebase functions:log" },
}, "engines": {
"engines": { "node": "20"
"node": "18" },
}, "main": "index.js",
"main": "index.js", "dependencies": {
"dependencies": { "firebase-admin": "^13.6.0",
"firebase-admin": "^11.8.0", "firebase-functions": "^7.0.0"
"firebase-functions": "^4.3.1" },
}, "devDependencies": {
"devDependencies": { "firebase-functions-test": "^3.4.1"
"eslint": "^8.15.0", },
"eslint-config-google": "^0.14.0" "private": true
},
"private": true
} }

View File

@@ -1216,6 +1216,9 @@ PODS:
- Firebase/Firestore (12.4.0): - Firebase/Firestore (12.4.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseFirestore (~> 12.4.0) - FirebaseFirestore (~> 12.4.0)
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- Firebase/Storage (12.4.0): - Firebase/Storage (12.4.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseStorage (~> 12.4.0) - FirebaseStorage (~> 12.4.0)
@@ -1223,9 +1226,13 @@ PODS:
- Firebase/Auth (= 12.4.0) - Firebase/Auth (= 12.4.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (4.2.0): - firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0) - Firebase/CoreOnly (= 12.4.0)
- Flutter - Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_core
- Flutter
- firebase_storage (13.0.3): - firebase_storage (13.0.3):
- Firebase/Storage (= 12.4.0) - Firebase/Storage (= 12.4.0)
- firebase_core - firebase_core
@@ -1269,6 +1276,20 @@ PODS:
- gRPC-Core (~> 1.69.0) - gRPC-Core (~> 1.69.0)
- leveldb-library (~> 1.22) - leveldb-library (~> 1.22)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseInstallations (12.4.0):
- FirebaseCore (~> 12.4.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseSharedSwift (12.4.0) - FirebaseSharedSwift (12.4.0)
- FirebaseStorage (12.4.0): - FirebaseStorage (12.4.0):
- FirebaseAppCheckInterop (~> 12.4.0) - FirebaseAppCheckInterop (~> 12.4.0)
@@ -1278,6 +1299,8 @@ PODS:
- GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Environment (~> 8.1)
- GTMSessionFetcher/Core (< 6.0, >= 3.4) - GTMSessionFetcher/Core (< 6.0, >= 3.4)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- geolocator_apple (1.2.0): - geolocator_apple (1.2.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -1292,6 +1315,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- GoogleSignIn (~> 9.0) - GoogleSignIn (~> 9.0)
- GTMSessionFetcher (>= 3.4.0) - GTMSessionFetcher (>= 3.4.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMaps (9.4.0): - GoogleMaps (9.4.0):
- GoogleMaps/Maps (= 9.4.0) - GoogleMaps/Maps (= 9.4.0)
- GoogleMaps/Maps (9.4.0) - GoogleMaps/Maps (9.4.0)
@@ -1456,8 +1482,10 @@ DEPENDENCIES:
- cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_storage (from `.symlinks/plugins/firebase_storage/ios`) - firebase_storage (from `.symlinks/plugins/firebase_storage/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
@@ -1485,9 +1513,12 @@ SPEC REPOS:
- FirebaseCoreInternal - FirebaseCoreInternal
- FirebaseFirestore - FirebaseFirestore
- FirebaseFirestoreInternal - FirebaseFirestoreInternal
- FirebaseInstallations
- FirebaseMessaging
- FirebaseSharedSwift - FirebaseSharedSwift
- FirebaseStorage - FirebaseStorage
- Google-Maps-iOS-Utils - Google-Maps-iOS-Utils
- GoogleDataTransport
- GoogleMaps - GoogleMaps
- GoogleSignIn - GoogleSignIn
- GoogleUtilities - GoogleUtilities
@@ -1507,10 +1538,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_auth/ios" :path: ".symlinks/plugins/firebase_auth/ios"
firebase_core: firebase_core:
:path: ".symlinks/plugins/firebase_core/ios" :path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
firebase_storage: firebase_storage:
:path: ".symlinks/plugins/firebase_storage/ios" :path: ".symlinks/plugins/firebase_storage/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
geolocator_apple: geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin" :path: ".symlinks/plugins/geolocator_apple/darwin"
google_maps_flutter_ios: google_maps_flutter_ios:
@@ -1542,7 +1577,8 @@ SPEC CHECKSUMS:
cloud_firestore: 79014bb3b303d451717ed5fe69fded8a2b2e8dc2 cloud_firestore: 79014bb3b303d451717ed5fe69fded8a2b2e8dc2
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_auth: c2b8be95d602d4e8a9148fae72333ef78e69cc20 firebase_auth: c2b8be95d602d4e8a9148fae72333ef78e69cc20
firebase_core: 744984dbbed8b3036abf34f0b98d80f130a7e464 firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
firebase_storage: 0ba617a05b24aec050395e4d5d3773c0d7518a15 firebase_storage: 0ba617a05b24aec050395e4d5d3773c0d7518a15
FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0 FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0
FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889 FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889
@@ -1552,13 +1588,17 @@ SPEC CHECKSUMS:
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
FirebaseFirestore: 2a6183381cf7679b1bb000eb76a8e3178e25dee2 FirebaseFirestore: 2a6183381cf7679b1bb000eb76a8e3178e25dee2
FirebaseFirestoreInternal: 6577a27cd5dc3722b900042527f86d4ea1626134 FirebaseFirestoreInternal: 6577a27cd5dc3722b900042527f86d4ea1626134
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
FirebaseSharedSwift: 93426a1de92f19e1199fac5295a4f8df16458daa FirebaseSharedSwift: 93426a1de92f19e1199fac5295a4f8df16458daa
FirebaseStorage: 20d6b56fb8a40ebaa03d6a2889fe33dac64adb73 FirebaseStorage: 20d6b56fb8a40ebaa03d6a2889fe33dac64adb73
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96
google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264
google_sign_in_ios: 205742c688aea0e64db9da03c33121694a365109 google_sign_in_ios: 205742c688aea0e64db9da03c33121694a365109
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438
GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77 GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1

View File

@@ -1,23 +1,24 @@
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/services/notification_service.dart';
import 'user_event.dart' as event; import 'user_event.dart' as event;
import 'user_state.dart' as state; import 'user_state.dart' as state;
/// BLoC for managing user data and operations. /// BLoC for managing user data and operations.
/// ///
/// This BLoC handles user-related operations including loading user data, /// This BLoC handles user-related operations including loading user data,
/// updating user information, and managing user state throughout the application. /// updating user information, and managing user state throughout the application.
/// It coordinates with Firebase Auth and Firestore to manage user data persistence. /// It coordinates with Firebase Auth and Firestore to manage user data persistence.
class UserBloc extends Bloc<event.UserEvent, state.UserState> { class UserBloc extends Bloc<event.UserEvent, state.UserState> {
/// Firebase Auth instance for authentication operations. /// Firebase Auth instance for authentication operations.
final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseAuth _auth = FirebaseAuth.instance;
/// Firestore instance for user data operations. /// Firestore instance for user data operations.
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
/// Creates a new [UserBloc] with initial state. /// Creates a new [UserBloc] with initial state.
/// ///
/// Registers event handlers for all user-related events. /// Registers event handlers for all user-related events.
UserBloc() : super(state.UserInitial()) { UserBloc() : super(state.UserInitial()) {
on<event.UserInitialized>(_onUserInitialized); on<event.UserInitialized>(_onUserInitialized);
@@ -25,9 +26,9 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
on<event.UserUpdated>(_onUserUpdated); on<event.UserUpdated>(_onUserUpdated);
on<event.UserLoggedOut>(_onUserLoggedOut); on<event.UserLoggedOut>(_onUserLoggedOut);
} }
/// Handles [UserInitialized] events. /// Handles [UserInitialized] events.
/// ///
/// Initializes the current authenticated user's data by fetching it from Firestore. /// Initializes the current authenticated user's data by fetching it from Firestore.
/// If the user doesn't exist in Firestore, creates a default user document. /// If the user doesn't exist in Firestore, creates a default user document.
/// This is typically called when the app starts or after successful authentication. /// This is typically called when the app starts or after successful authentication.
@@ -36,49 +37,70 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
Emitter<state.UserState> emit, Emitter<state.UserState> emit,
) async { ) async {
emit(state.UserLoading()); emit(state.UserLoading());
try { try {
final currentUser = _auth.currentUser; final currentUser = _auth.currentUser;
if (currentUser == null) { if (currentUser == null) {
emit(state.UserError('No user currently authenticated')); emit(state.UserError('No user currently authenticated'));
return; return;
} }
// Initialize notifications and update token
final notificationService = NotificationService();
await notificationService.initialize();
final fcmToken = await notificationService.getFCMToken();
print('DEBUG: UserBloc - FCM Token retrieved: $fcmToken');
// Fetch user data from Firestore // Fetch user data from Firestore
final userDoc = await _firestore final userDoc = await _firestore
.collection('users') .collection('users')
.doc(currentUser.uid) .doc(currentUser.uid)
.get(); .get();
if (!userDoc.exists) { if (!userDoc.exists) {
// Create a default user if it doesn't exist // Create a default user if it doesn't exist
final defaultUser = state.UserModel( final defaultUser = state.UserModel(
id: currentUser.uid, id: currentUser.uid,
email: currentUser.email ?? '', email: currentUser.email ?? '',
prenom: currentUser.displayName ?? 'Voyageur', prenom: currentUser.displayName ?? 'Voyageur',
fcmToken: fcmToken,
); );
await _firestore await _firestore
.collection('users') .collection('users')
.doc(currentUser.uid) .doc(currentUser.uid)
.set(defaultUser.toJson()); .set(defaultUser.toJson());
emit(state.UserLoaded(defaultUser)); emit(state.UserLoaded(defaultUser));
} else { } else {
final user = state.UserModel.fromJson({ final user = state.UserModel.fromJson({
'id': currentUser.uid, 'id': currentUser.uid,
...userDoc.data()!, ...userDoc.data()!,
}); });
// Update FCM token if it changed
if (fcmToken != null && user.fcmToken != fcmToken) {
print('DEBUG: UserBloc - Updating FCM token in Firestore');
await _firestore.collection('users').doc(currentUser.uid).update({
'fcmToken': fcmToken,
});
print('DEBUG: UserBloc - FCM token updated');
} else {
print(
'DEBUG: UserBloc - FCM token not updated. Local: $fcmToken, Firestore: ${user.fcmToken}',
);
}
emit(state.UserLoaded(user)); emit(state.UserLoaded(user));
} }
} catch (e) { } catch (e) {
emit(state.UserError('Error loading user: $e')); emit(state.UserError('Error loading user: $e'));
} }
} }
/// Handles [LoadUser] events. /// Handles [LoadUser] events.
/// ///
/// Loads a specific user's data from Firestore by their user ID. /// Loads a specific user's data from Firestore by their user ID.
/// This is useful when you need to display information about other users. /// This is useful when you need to display information about other users.
Future<void> _onLoadUser( Future<void> _onLoadUser(
@@ -86,13 +108,13 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
Emitter<state.UserState> emit, Emitter<state.UserState> emit,
) async { ) async {
emit(state.UserLoading()); emit(state.UserLoading());
try { try {
final userDoc = await _firestore final userDoc = await _firestore
.collection('users') .collection('users')
.doc(event.userId) .doc(event.userId)
.get(); .get();
if (userDoc.exists) { if (userDoc.exists) {
final user = state.UserModel.fromJson({ final user = state.UserModel.fromJson({
'id': event.userId, 'id': event.userId,
@@ -106,9 +128,9 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
emit(state.UserError('Error loading user: $e')); emit(state.UserError('Error loading user: $e'));
} }
} }
/// Handles [UserUpdated] events. /// Handles [UserUpdated] events.
/// ///
/// Updates the current user's data in Firestore with the provided information. /// Updates the current user's data in Firestore with the provided information.
/// Only updates the fields specified in the userData map, allowing for partial updates. /// Only updates the fields specified in the userData map, allowing for partial updates.
/// After successful update, reloads the user data to reflect changes. /// After successful update, reloads the user data to reflect changes.
@@ -118,32 +140,32 @@ class UserBloc extends Bloc<event.UserEvent, state.UserState> {
) async { ) async {
if (this.state is state.UserLoaded) { if (this.state is state.UserLoaded) {
final currentUser = (this.state as state.UserLoaded).user; final currentUser = (this.state as state.UserLoaded).user;
try { try {
await _firestore await _firestore
.collection('users') .collection('users')
.doc(currentUser.id) .doc(currentUser.id)
.update(event.userData); .update(event.userData);
final updatedDoc = await _firestore final updatedDoc = await _firestore
.collection('users') .collection('users')
.doc(currentUser.id) .doc(currentUser.id)
.get(); .get();
final updatedUser = state.UserModel.fromJson({ final updatedUser = state.UserModel.fromJson({
'id': currentUser.id, 'id': currentUser.id,
...updatedDoc.data()!, ...updatedDoc.data()!,
}); });
emit(state.UserLoaded(updatedUser)); emit(state.UserLoaded(updatedUser));
} catch (e) { } catch (e) {
emit(state.UserError('Error updating user: $e')); emit(state.UserError('Error updating user: $e'));
} }
} }
} }
/// Handles [UserLoggedOut] events. /// Handles [UserLoggedOut] events.
/// ///
/// Resets the user bloc state to initial when the user logs out. /// Resets the user bloc state to initial when the user logs out.
/// This clears any cached user data from the application state. /// This clears any cached user data from the application state.
Future<void> _onUserLoggedOut( Future<void> _onUserLoggedOut(

View File

@@ -1,7 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
/// Abstract base class for all user-related states. /// Abstract base class for all user-related states.
/// ///
/// This class extends [Equatable] to enable value equality for state comparison. /// This class extends [Equatable] to enable value equality for state comparison.
/// All user states in the application should inherit from this class. /// All user states in the application should inherit from this class.
abstract class UserState extends Equatable { abstract class UserState extends Equatable {
@@ -10,79 +10,82 @@ abstract class UserState extends Equatable {
} }
/// Initial state of the user bloc. /// Initial state of the user bloc.
/// ///
/// This state represents the initial state before any user operations /// This state represents the initial state before any user operations
/// have been performed or when the user has logged out. /// have been performed or when the user has logged out.
class UserInitial extends UserState {} class UserInitial extends UserState {}
/// State indicating that a user operation is in progress. /// State indicating that a user operation is in progress.
/// ///
/// This state is used to show loading indicators during user data /// This state is used to show loading indicators during user data
/// operations like loading, updating, or initializing user information. /// operations like loading, updating, or initializing user information.
class UserLoading extends UserState {} class UserLoading extends UserState {}
/// State indicating that user data has been successfully loaded. /// State indicating that user data has been successfully loaded.
/// ///
/// This state contains the loaded user information and is used /// This state contains the loaded user information and is used
/// throughout the app to access current user data. /// throughout the app to access current user data.
class UserLoaded extends UserState { class UserLoaded extends UserState {
/// The loaded user data. /// The loaded user data.
final UserModel user; final UserModel user;
/// Creates a [UserLoaded] state with the given [user] data. /// Creates a [UserLoaded] state with the given [user] data.
UserLoaded(this.user); UserLoaded(this.user);
@override @override
List<Object?> get props => [user]; List<Object?> get props => [user];
} }
/// State indicating that a user operation has failed. /// State indicating that a user operation has failed.
/// ///
/// This state contains an error message that can be displayed to the user /// This state contains an error message that can be displayed to the user
/// when user operations fail. /// when user operations fail.
class UserError extends UserState { class UserError extends UserState {
/// The error message describing what went wrong. /// The error message describing what went wrong.
final String message; final String message;
/// Creates a [UserError] state with the given error [message]. /// Creates a [UserError] state with the given error [message].
UserError(this.message); UserError(this.message);
@override @override
List<Object?> get props => [message]; List<Object?> get props => [message];
} }
/// Simple user model for representing user data in the application. /// Simple user model for representing user data in the application.
/// ///
/// This model contains basic user information and provides methods for /// This model contains basic user information and provides methods for
/// serialization/deserialization with Firestore operations. /// serialization/deserialization with Firestore operations.
/// Simple user model for representing user data in the application. /// Simple user model for representing user data in the application.
/// ///
/// This model contains basic user information and provides methods for /// This model contains basic user information and provides methods for
/// serialization/deserialization with Firestore operations. /// serialization/deserialization with Firestore operations.
class UserModel { class UserModel {
/// Unique identifier for the user (Firebase UID). /// Unique identifier for the user (Firebase UID).
final String id; final String id;
/// User's email address. /// User's email address.
final String email; final String email;
/// User's first name. /// User's first name.
final String prenom; final String prenom;
/// User's last name (optional). /// User's last name (optional).
final String? nom; final String? nom;
/// Platform used for authentication (e.g., 'google', 'apple', 'email'). /// Platform used for authentication (e.g., 'google', 'apple', 'email').
final String? authMethod; final String? authMethod;
/// User's phone number (optional). /// User's phone number (optional).
final String? phoneNumber; final String? phoneNumber;
/// User's profile picture URL (optional). /// User's profile picture URL (optional).
final String? profilePictureUrl; final String? profilePictureUrl;
/// Firebase Cloud Messaging token for push notifications.
final String? fcmToken;
/// Creates a new [UserModel] instance. /// Creates a new [UserModel] instance.
/// ///
/// [id], [email], and [prenom] are required fields. /// [id], [email], and [prenom] are required fields.
/// [nom], [authMethod], [phoneNumber], and [profilePictureUrl] are optional and can be null. /// [nom], [authMethod], [phoneNumber], and [profilePictureUrl] are optional and can be null.
UserModel({ UserModel({
@@ -93,10 +96,11 @@ class UserModel {
this.authMethod, this.authMethod,
this.phoneNumber, this.phoneNumber,
this.profilePictureUrl, this.profilePictureUrl,
this.fcmToken,
}); });
/// Creates a [UserModel] instance from a JSON map. /// Creates a [UserModel] instance from a JSON map.
/// ///
/// Handles null values gracefully by providing default values. /// Handles null values gracefully by providing default values.
/// [prenom] defaults to 'Voyageur' (Traveler) if not provided. /// [prenom] defaults to 'Voyageur' (Traveler) if not provided.
factory UserModel.fromJson(Map<String, dynamic> json) { factory UserModel.fromJson(Map<String, dynamic> json) {
@@ -108,11 +112,12 @@ class UserModel {
authMethod: json['authMethod'] ?? json['platform'], authMethod: json['authMethod'] ?? json['platform'],
phoneNumber: json['phoneNumber'], phoneNumber: json['phoneNumber'],
profilePictureUrl: json['profilePictureUrl'], profilePictureUrl: json['profilePictureUrl'],
fcmToken: json['fcmToken'],
); );
} }
/// Converts the [UserModel] instance to a JSON map. /// Converts the [UserModel] instance to a JSON map.
/// ///
/// Useful for storing user data in Firestore or other JSON-based operations. /// Useful for storing user data in Firestore or other JSON-based operations.
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
@@ -123,6 +128,7 @@ class UserModel {
'authMethod': authMethod, 'authMethod': authMethod,
'phoneNumber': phoneNumber, 'phoneNumber': phoneNumber,
'profilePictureUrl': profilePictureUrl, 'profilePictureUrl': profilePictureUrl,
'fcmToken': fcmToken,
}; };
} }
} }

View File

@@ -213,8 +213,8 @@ class _GroupExpensesPageState extends State<GroupExpensesPage>
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'group_expenses_fab',
onPressed: _showAddExpenseDialog, onPressed: _showAddExpenseDialog,
heroTag: "add_expense_fab",
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
elevation: 4, elevation: 4,

View File

@@ -155,6 +155,7 @@ class _HomeContentState extends State<HomeContent>
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'home_fab',
onPressed: () async { onPressed: () async {
final tripBloc = context.read<TripBloc>(); final tripBloc = context.read<TripBloc>();

View File

@@ -663,6 +663,7 @@ class _MapContentState extends State<MapContent> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
heroTag: 'map_fab',
onPressed: _getCurrentLocation, onPressed: _getCurrentLocation,
tooltip: 'Ma position', tooltip: 'Ma position',
child: const Icon(Icons.my_location), child: const Icon(Icons.my_location),

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:travel_mate/services/logger_service.dart'; import 'package:travel_mate/services/logger_service.dart';
@@ -58,7 +59,26 @@ class NotificationService {
Future<String?> getFCMToken() async { Future<String?> getFCMToken() async {
try { try {
return await _firebaseMessaging.getToken(); if (Platform.isIOS) {
String? apnsToken = await _firebaseMessaging.getAPNSToken();
int retries = 0;
while (apnsToken == null && retries < 3) {
LoggerService.info(
'Waiting for APNS token... (Attempt ${retries + 1}/3)',
);
await Future.delayed(const Duration(seconds: 1));
apnsToken = await _firebaseMessaging.getAPNSToken();
retries++;
}
if (apnsToken == null) {
LoggerService.error('APNS token not available after retries');
return null;
}
}
final token = await _firebaseMessaging.getToken();
LoggerService.info('NotificationService - FCM Token: $token');
return token;
} catch (e) { } catch (e) {
LoggerService.error('Error getting FCM token: $e'); LoggerService.error('Error getting FCM token: $e');
return null; return null;