Browse Source

feat: friends requests and location sharing

Léo Salé 3 năm trước cách đây
mục cha
commit
4d3cef9ade

+ 43 - 0
app/lib/friends/friends_page.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'widgets/friends_list.dart';
+import 'widgets/requests.dart';
+
+class FriendsPage extends StatefulWidget {
+  const FriendsPage({Key? key}) : super(key: key);
+
+  @override
+  State<FriendsPage> createState() => _FriendsPageState();
+}
+
+class _FriendsPageState extends State<FriendsPage> {
+  int _selectedIndex = 0;
+
+  static const List<Widget> _widgets = [
+    FriendsList(),
+    Requests(),
+  ];
+
+  void _onItemTapped(int index) {
+    setState(() {
+      _selectedIndex = index;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+        bottomNavigationBar: BottomNavigationBar(
+          items: const [
+            BottomNavigationBarItem(icon: Icon(Icons.people), label: 'Friends'),
+            BottomNavigationBarItem(icon: Icon(Icons.notifications), label: 'Requests'),
+          ],
+          currentIndex: _selectedIndex,
+          onTap: _onItemTapped,
+        ),
+        body: Center(
+            child: Padding(
+          padding: const EdgeInsets.all(32.0),
+          child: _widgets[_selectedIndex],
+        )));
+  }
+}

+ 17 - 0
app/lib/friends/models/challenge_location_request.dart

@@ -0,0 +1,17 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+
+class ChallengeLocationRequest {
+  final String id;
+  final String name;
+  final String surname;
+  final String locationName;
+  final DocumentReference locationRef;
+
+  const ChallengeLocationRequest({
+    required this.id,
+    required this.name,
+    required this.surname,
+    required this.locationName,
+    required this.locationRef,
+  });
+}

+ 22 - 0
app/lib/friends/models/friend.dart

@@ -0,0 +1,22 @@
+import 'package:physigo/friends/models/friend_relation.dart';
+
+class Friend {
+  final String id;
+  final String name;
+  final String surname;
+  final RequestStatus status;
+  final String relationId;
+
+  const Friend({
+    required this.id,
+    required this.name,
+    required this.surname,
+    required this.status,
+    required this.relationId,
+  });
+
+  @override
+  String toString() {
+    return "$name $surname: ${status.name}";
+  }
+}

+ 40 - 0
app/lib/friends/models/friend_relation.dart

@@ -0,0 +1,40 @@
+class FriendRelation {
+  final String id;
+  final String friendId;
+  final RequestStatus requestStatus;
+
+  const FriendRelation({
+    required this.id,
+    required this.friendId,
+    required this.requestStatus,
+  });
+
+  factory FriendRelation.fromMap(String relationId, Map<String, dynamic> map, String userId) {
+    return FriendRelation(
+      id: relationId,
+      friendId: _getId(map, userId),
+      requestStatus: getRequestStatus(map["request_status"]),
+    );
+  }
+
+  static String _getId(Map<String, dynamic> map, String userId) {
+    if (map['user_id'] == userId) {
+      return map['friend_id'];
+    } else {
+      return map['user_id'];
+    }
+  }
+
+  static RequestStatus getRequestStatus(String status) {
+    if (status == "pending") return RequestStatus.pending;
+    if (status == "accepted") return RequestStatus.accepted;
+    if (status == "rejected") return RequestStatus.rejected;
+    throw Exception("Request status invalid");
+  }
+}
+
+enum RequestStatus {
+  pending,
+  accepted,
+  rejected,
+}

+ 7 - 0
app/lib/friends/models/friend_request.dart

@@ -0,0 +1,7 @@
+class FriendRequest {
+  final String requestId;
+  final String friendName;
+  final String friendSurname;
+
+  const FriendRequest({required this.requestId, required this.friendName, required this.friendSurname});
+}

+ 84 - 0
app/lib/friends/services/challenge_location_service.dart

@@ -0,0 +1,84 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:physigo/friends/models/challenge_location_request.dart';
+
+const userId = "tlmysIvwTBaoZKWqBofx";
+
+class ChallengeLocationService {
+  static final _db = FirebaseFirestore.instance;
+  static final _users = _db.collection("Users");
+  static final _shareLocationRequests = _db.collection("ShareLocationRequests");
+  static final _friends = _db.collection("Friends");
+
+  static Future<List<ChallengeLocationRequest>> getShareChallengeLocationRequests() async {
+    final requests = await _shareLocationRequests.where("user_id", isEqualTo: userId).get();
+    if (requests.docs.isEmpty) {
+      return [];
+    }
+    final requesterIdLocationId = {
+      for (final request in requests.docs)
+        request.data()['requester_id']: {"requestId": request.id, "locationRef": request.data()['location']}
+    };
+    final requesters = await _users.where(FieldPath.documentId, whereIn: requesterIdLocationId.keys.toList()).get();
+    final shareChallengeLocationRequests = await Future.wait(requesters.docs.map((requester) async {
+      final data = requester.data();
+      final place = (await requesterIdLocationId[requester.id]!["locationRef"].get()).data();
+      return ChallengeLocationRequest(
+          id: requesterIdLocationId[requester.id]!["requestId"]!,
+          name: data['name'],
+          surname: data['surname'],
+          locationName: place['name'],
+          locationRef: requesterIdLocationId[requester.id]!["locationRef"]!);
+    }));
+    return shareChallengeLocationRequests;
+  }
+
+  static Future<void> shareChallengeLocation(String friendId) async {
+    await Future.wait([_checkExistingFriendRelation(friendId), _checkExistingChallengeLocationRequest(friendId)]);
+    final user = await _users.doc(userId).get();
+    final shareLocationRequest = {"user_id": friendId, "requester_id": userId, "location": user['weekly_place']};
+    await _shareLocationRequests.add(shareLocationRequest);
+  }
+
+  static Future<void> acceptChallengeLocationRequest(ChallengeLocationRequest request) async {
+    final newLocation = {"weekly_place": request.locationRef};
+    await _users.doc(userId).update(newLocation);
+    await _shareLocationRequests.doc(request.id).delete();
+  }
+
+  static Future<void> refuseChallengeLocationRequest(ChallengeLocationRequest request) async {
+    await _shareLocationRequests.doc(request.id).delete();
+  }
+
+  static Future<void> _checkExistingFriendRelation(String friendId) async {
+    if (friendId == userId) {
+      return Future.error("You cannot share a location with yourself");
+    }
+
+    var friendRelationByUserId = _friends
+        .where("user_id", isEqualTo: userId)
+        .where("friend_id", isEqualTo: friendId)
+        .where("request_status", isEqualTo: "accepted")
+        .get();
+
+    var friendRelationByFriendId = _friends
+        .where("friend_id", isEqualTo: userId)
+        .where("user_id", isEqualTo: friendId)
+        .where("request_status", isEqualTo: "accepted")
+        .get();
+
+    final relations = await Future.wait([friendRelationByUserId, friendRelationByFriendId]);
+    if (relations.expand((relation) => relation.docs).isEmpty) {
+      return Future.error("You must be friend to share a location");
+    }
+  }
+
+  static Future<void> _checkExistingChallengeLocationRequest(String friendId) async {
+    final requests = await _shareLocationRequests
+        .where("requester_id", isEqualTo: userId)
+        .where("user_id", isEqualTo: friendId)
+        .get();
+    if (requests.docs.isNotEmpty) {
+      return Future.error("You already shared your location with this person");
+    }
+  }
+}

+ 114 - 0
app/lib/friends/services/friends_service.dart

@@ -0,0 +1,114 @@
+import 'package:cloud_firestore/cloud_firestore.dart';
+import 'package:physigo/friends/models/friend_request.dart';
+import '../models/friend.dart';
+import '../models/friend_relation.dart';
+
+const userId = "tlmysIvwTBaoZKWqBofx";
+
+class FriendsService {
+  static final _db = FirebaseFirestore.instance;
+  static final _friends = _db.collection("Friends");
+  static final _users = _db.collection("Users");
+
+  static Future<List<Friend>> getFriends() async {
+    return _getFriendsByStatus(RequestStatus.accepted);
+  }
+
+  static Future<List<FriendRequest>> getPendingRequests() async {
+    final friendRequests =
+        await _friends.where("friend_id", isEqualTo: userId).where("request_status", isEqualTo: "pending").get();
+    if (friendRequests.docs.isEmpty) {
+      return [];
+    }
+    final friendsIdRequestsId = {for (final r in friendRequests.docs) r.data()['user_id']: r.id};
+    final friendsInfo = await _users.where(FieldPath.documentId, whereIn: friendsIdRequestsId.keys.toList()).get();
+    return friendsInfo.docs
+        .map((f) => FriendRequest(
+              requestId: friendsIdRequestsId[f.id]!,
+              friendName: f.data()['name'],
+              friendSurname: f.data()['surname'],
+            ))
+        .toList();
+  }
+
+  static Future<void> addFriend(String friendSharedId) async {
+    try {
+      final query = (await _users.where("shared_id", isEqualTo: friendSharedId).get());
+      if (query.docs.isEmpty) {
+        return Future.error("This account doesn't exist");
+      }
+      final friend = query.docs.first;
+      await _checkExistingFriendRelation(friend.id);
+      final friendRequest = {"user_id": userId, "friend_id": friend.id, "request_status": "pending"};
+      await _friends.add(friendRequest);
+    } catch (error) {
+      return Future.error(error);
+    }
+  }
+
+  static Future<void> acceptFriendRequest(FriendRequest friendRequest) async {
+    final newStatus = {"request_status": "accepted"};
+    await _friends.doc(friendRequest.requestId).update(newStatus);
+  }
+
+  static Future<void> refuseFriendRequest(FriendRequest friendRequest) async {
+    final newStatus = {"request_status": "rejected"};
+    await _friends.doc(friendRequest.requestId).update(newStatus);
+  }
+
+  static Future<List<Friend>> _getFriendsByStatus(RequestStatus status) async {
+    final friendsByUserId = _friends.where("user_id", isEqualTo: userId).get();
+    final friendsByFriendId = _friends.where("friend_id", isEqualTo: userId).get();
+    final friendRelations = (await Future.wait([friendsByUserId, friendsByFriendId]))
+        .expand((query) => query.docs)
+        .map((friendRelation) => FriendRelation.fromMap(friendRelation.id, friendRelation.data(), userId))
+        .where((friendRelation) => friendRelation.requestStatus == status)
+        .toList();
+
+    final friends = await _getFriendsFromFriendRelations(friendRelations);
+    return friends;
+  }
+
+  static Future<List<Friend>> _getFriendsFromFriendRelations(List<FriendRelation> friendRelations) async {
+    final users = await _db
+        .collection('Users')
+        .where(FieldPath.documentId, whereIn: friendRelations.map((e) => e.friendId).toList())
+        .get();
+    final friends = users.docs.map((friend) {
+      final data = friend.data();
+      final friendRelation = friendRelations.firstWhere((element) => element.friendId == friend.id);
+      return Friend(
+        id: friend.id,
+        name: data['name'],
+        surname: data['surname'],
+        status: friendRelation.requestStatus,
+        relationId: friendRelation.id,
+      );
+    }).toList();
+    return friends;
+  }
+
+  static Future<void> _checkExistingFriendRelation(String friendId) async {
+    if (friendId == userId) {
+      return Future.error("You already are your own friend");
+    }
+
+    var friendRelation = await _db
+        .collection("Friends")
+        .where("user_id", isEqualTo: userId)
+        .where("friend_id", isEqualTo: friendId)
+        .get();
+    if (friendRelation.docs.isNotEmpty) {
+      return Future.error("You already sent a request to this person");
+    }
+
+    friendRelation = await _db
+        .collection("Friends")
+        .where("friend_id", isEqualTo: userId)
+        .where("user_id", isEqualTo: friendId)
+        .get();
+    if (friendRelation.docs.isNotEmpty) {
+      return Future.error("They already sent you a request");
+    }
+  }
+}

+ 69 - 0
app/lib/friends/widgets/add_friend.dart

@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:physigo/friends/services/friends_service.dart';
+
+class AddFriend extends StatefulWidget {
+  const AddFriend({Key? key}) : super(key: key);
+
+  @override
+  State<AddFriend> createState() => _AddFriendState();
+}
+
+class _AddFriendState extends State<AddFriend> {
+  String _friendSharedId = "";
+
+  void _onTextUpdate(String newFriendSharedId) {
+    setState(() {
+      _friendSharedId = newFriendSharedId;
+    });
+  }
+
+  void _addFriend() async {
+    try {
+      await FriendsService.addFriend(_friendSharedId);
+      setState(() {
+        _friendSharedId = "";
+      });
+      _showSnackBar("Friend request sent!", Colors.green);
+    } catch (error) {
+      _showSnackBar(error.toString(), Colors.red);
+    }
+  }
+
+  void _showSnackBar(String message, Color color) {
+    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+      content: Text(message),
+      backgroundColor: color,
+      duration: const Duration(milliseconds: 1500),
+    ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        TextField(
+          onSubmitted: (_) => _addFriend(),
+          onChanged: _onTextUpdate,
+          decoration: const InputDecoration(
+            border: OutlineInputBorder(),
+            hintText: "Enter a friend's ID to add them",
+          ),
+        ),
+        const SizedBox(height: 32),
+        ElevatedButton(
+          onPressed: _addFriend,
+          style: ElevatedButton.styleFrom(
+            padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32),
+          ),
+          child: const Text("ADD",
+              style: TextStyle(
+                fontSize: 20,
+                fontWeight: FontWeight.bold,
+                letterSpacing: 1.3,
+              )),
+        )
+      ],
+    );
+  }
+}

+ 87 - 0
app/lib/friends/widgets/challenge_location_requests.dart

@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:physigo/friends/models/challenge_location_request.dart';
+import 'package:physigo/friends/services/challenge_location_service.dart';
+
+class ChallengeLocationRequests extends StatefulWidget {
+  const ChallengeLocationRequests({Key? key}) : super(key: key);
+
+  @override
+  State<ChallengeLocationRequests> createState() => _ChallengeLocationRequestsState();
+}
+
+class _ChallengeLocationRequestsState extends State<ChallengeLocationRequests> {
+  List<ChallengeLocationRequest> _requests = [];
+  late Future<List<ChallengeLocationRequest>> _requestsQuery;
+
+  @override
+  void initState() {
+    _requestsQuery = ChallengeLocationService.getShareChallengeLocationRequests();
+    _requestsQuery.then((requests) => setState(() => _requests = requests));
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        const Text(
+          "Challenge Location's Requests",
+          style: TextStyle(fontSize: 24),
+          textAlign: TextAlign.center,
+        ),
+        FutureBuilder<List<ChallengeLocationRequest>>(
+          future: _requestsQuery,
+          builder: (context, snapshot) {
+            if (!snapshot.hasData) {
+              return const CircularProgressIndicator();
+            }
+            if (_requests.isEmpty) {
+              return const Text("You don't have any share location's requests");
+            }
+            return ListView.builder(
+              shrinkWrap: true,
+              itemBuilder: ((context, index) => _requestTile(_requests[index])),
+              itemCount: _requests.length,
+            );
+          },
+        ),
+      ],
+    );
+  }
+
+  void _acceptRequest(ChallengeLocationRequest request) async {
+    await ChallengeLocationService.acceptChallengeLocationRequest(request);
+    setState(() {
+      _requests = _requests.where((r) => r.id != request.id).toList();
+    });
+  }
+
+  void _refuseRequest(ChallengeLocationRequest request) async {
+    await ChallengeLocationService.refuseChallengeLocationRequest(request);
+    setState(() {
+      _requests = _requests.where((r) => r.id != request.id).toList();
+    });
+  }
+
+  Widget _requestTile(ChallengeLocationRequest request) {
+    return Card(
+      child: ListTile(
+        title: Text(request.locationName, maxLines: 2, overflow: TextOverflow.ellipsis,),
+        subtitle: Text("From ${request.name} ${request.surname}"),
+        trailing: Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextButton(
+              child: const Text("Refuse"),
+              onPressed: () => _refuseRequest(request),
+            ),
+            TextButton(
+              child: const Text("Accept"),
+              onPressed: () => _acceptRequest(request),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 69 - 0
app/lib/friends/widgets/friends_list.dart

@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:physigo/friends/services/challenge_location_service.dart';
+import 'package:physigo/friends/widgets/add_friend.dart';
+
+import '../models/friend.dart';
+import '../services/friends_service.dart';
+
+class FriendsList extends StatefulWidget {
+  const FriendsList({Key? key}) : super(key: key);
+
+  @override
+  State<FriendsList> createState() => _FriendsListState();
+}
+
+class _FriendsListState extends State<FriendsList> {
+
+  void _shareLocation(String friendId) async {
+    try {
+      await ChallengeLocationService.shareChallengeLocation(friendId);
+      _showSnackBar("Challenge's location shared!", Colors.green);
+    } catch (error) {
+      _showSnackBar(error.toString(), Colors.red);
+    }
+  }
+
+  void _showSnackBar(String message, Color color) {
+    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+      content: Text(message),
+      backgroundColor: color,
+      duration: const Duration(milliseconds: 1500),
+    ));
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        const AddFriend(),
+        FutureBuilder<List<Friend>>(
+          future: FriendsService.getFriends(),
+          builder: (context, snapshot) {
+            if (!snapshot.hasData) {
+              return const CircularProgressIndicator();
+            }
+            final friends = snapshot.data!;
+            return ListView.builder(
+              shrinkWrap: true,
+              itemBuilder: ((context, index) => _friendTile(friends[index])),
+              itemCount: friends.length,
+            );
+          },
+        ),
+      ],
+    );
+  }
+
+  Widget _friendTile(Friend friend) {
+    return Card(
+      child: ListTile(
+        title: Text("${friend.name} ${friend.surname}"),
+        trailing: TextButton(
+          child: const Text("Share challenge location"),
+          onPressed: () => _shareLocation(friend.id),
+        ),
+      ),
+    );
+  }
+}

+ 87 - 0
app/lib/friends/widgets/friends_requests.dart

@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+
+import '../models/friend_request.dart';
+import '../services/friends_service.dart';
+
+class FriendsRequests extends StatefulWidget {
+  const FriendsRequests({Key? key}) : super(key: key);
+
+  @override
+  State<FriendsRequests> createState() => _FriendsRequestsState();
+}
+
+class _FriendsRequestsState extends State<FriendsRequests> {
+    List<FriendRequest> _requests = [];
+  late Future<List<FriendRequest>> _requestsQuery;
+
+  @override
+  void initState() {
+    _requestsQuery = FriendsService.getPendingRequests();
+    _requestsQuery.then((requests) => setState(() => _requests = requests));
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        const Text(
+          "Friend's Requests",
+          style: TextStyle(fontSize: 24),
+          textAlign: TextAlign.center,
+        ),
+        FutureBuilder<List<FriendRequest>>(
+          future: _requestsQuery,
+          builder: (context, snapshot) {
+            if (!snapshot.hasData) {
+              return const CircularProgressIndicator();
+            }
+            if (_requests.isEmpty) {
+              return const Text("You don't have any friend's requests");
+            }
+            return ListView.builder(
+              shrinkWrap: true,
+              itemBuilder: ((context, index) => _requestTile(_requests[index])),
+              itemCount: _requests.length,
+            );
+          },
+        ),
+      ],
+    );
+  }
+
+  void _acceptRequest(FriendRequest request) async {
+    await FriendsService.acceptFriendRequest(request);
+    setState(() {
+      _requests = _requests.where((r) => r.requestId != request.requestId).toList();
+    });
+  }
+
+  void _refuseRequest(FriendRequest request) async {
+    await FriendsService.refuseFriendRequest(request);
+    setState(() {
+      _requests = _requests.where((r) => r.requestId != request.requestId).toList();
+    });
+  }
+
+  Widget _requestTile(FriendRequest friendRequest) {
+    return Card(
+      child: ListTile(
+        title: Text("${friendRequest.friendName} ${friendRequest.friendSurname}"),
+        trailing: Row(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            TextButton(
+              child: const Text("Refuse"),
+              onPressed: () => _refuseRequest(friendRequest),
+            ),
+            TextButton(
+              child: const Text("Accept"),
+              onPressed: () => _acceptRequest(friendRequest),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 19 - 0
app/lib/friends/widgets/requests.dart

@@ -0,0 +1,19 @@
+import 'package:flutter/material.dart';
+import 'package:physigo/friends/widgets/friends_requests.dart';
+import 'package:physigo/friends/widgets/challenge_location_requests.dart';
+
+class Requests extends StatelessWidget {
+  const Requests({ Key? key }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: const [
+        ChallengeLocationRequests(),
+        SizedBox(height: 32),
+        FriendsRequests(),
+      ],
+    );
+  }
+}

+ 47 - 30
app/lib/main.dart

@@ -2,6 +2,7 @@ import 'package:firebase_core/firebase_core.dart';
 import 'package:flutter/material.dart';
 import 'package:latlong2/latlong.dart';
 import 'package:physigo/exercises/exercises_page.dart';
+import 'package:physigo/friends/friends_page.dart';
 import 'navigation/navigation_page.dart';
 
 import 'firebase_options.dart';
@@ -33,38 +34,54 @@ class HomePage extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Scaffold(
-      body: Column(
-        mainAxisAlignment: MainAxisAlignment.center,
-        children: [
-          Center(
-            child: TextButton(
-              onPressed: () {
-                Navigator.push(
-                  context,
-                  MaterialPageRoute(
-                    // Example on how to use NavigationPage
-                    builder: (context) => NavigationPage(
-                      destination: LatLng(51.78036111980833, 19.451262207821234),
+    return SafeArea(
+      child: Scaffold(
+        body: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Center(
+              child: TextButton(
+                  onPressed: () {
+                    Navigator.push(
+                      context,
+                      MaterialPageRoute(
+                        // Example on how to use NavigationPage
+                        builder: (context) => NavigationPage(
+                          destination: LatLng(51.78036111980833, 19.451262207821234),
+                        ),
+                      ),
+                    );
+                  },
+                  child: const Text('Navigation')),
+            ),
+            Center(
+              child: TextButton(
+                onPressed: () {
+                  Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => const ExercisesPage(),
+                    ),
+                  );
+                },
+                child: const Text('Exercises'),
+              ),
+            ),
+            Center(
+              child: TextButton(
+                onPressed: () {
+                  Navigator.push(
+                    context,
+                    MaterialPageRoute(
+                      builder: (context) => const FriendsPage(),
                     ),
-                  ),
-                );
-              },
-              child: const Text('Navigation'),
+                  );
+                },
+                child: const Text('Friends'),
+              ),
             ),
-          ),
-          TextButton(
-            onPressed: () {
-              Navigator.push(
-                context,
-                MaterialPageRoute(
-                  builder: (context) => const ExercisesPage(),
-                ),
-              );
-            },
-            child: const Text('Exercises'),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }

+ 23 - 2
app/pubspec.lock

@@ -43,6 +43,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.1.0"
+  cloud_firestore:
+    dependency: "direct main"
+    description:
+      name: cloud_firestore
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.15"
+  cloud_firestore_platform_interface:
+    dependency: transitive
+    description:
+      name: cloud_firestore_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.5.6"
+  cloud_firestore_web:
+    dependency: transitive
+    description:
+      name: cloud_firestore_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.6.15"
   collection:
     dependency: transitive
     description:
@@ -77,14 +98,14 @@ packages:
       name: firebase_core_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "4.2.5"
+    version: "4.4.0"
   firebase_core_web:
     dependency: transitive
     description:
       name: firebase_core_web
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.6.2"
+    version: "1.6.4"
   flutter:
     dependency: "direct main"
     description: flutter

+ 1 - 0
app/pubspec.yaml

@@ -41,6 +41,7 @@ dependencies:
   permission_handler: ^9.2.0
   body_detection: ^0.0.3
   rxdart: ^0.27.3
+  cloud_firestore: ^3.1.15
 
 dev_dependencies:
   flutter_test: