diff --git a/data/.flutter-plugins b/data/.flutter-plugins index a0b08214..dfe2d5b8 100644 --- a/data/.flutter-plugins +++ b/data/.flutter-plugins @@ -1,22 +1,24 @@ # This is a generated file; do not edit or check into version control. -cloud_firestore=/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore-5.2.0/ -cloud_firestore_web=/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.1.0/ -device_info_plus=/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/ -firebase_auth=/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/ -firebase_auth_web=/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth_web-5.12.5/ -firebase_core=/home/mayank/.pub-cache/hosted/pub.dev/firebase_core-3.3.0/ -firebase_core_web=/home/mayank/.pub-cache/hosted/pub.dev/firebase_core_web-2.17.4/ -firebase_messaging=/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.4/ -firebase_messaging_web=/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging_web-3.8.12/ -firebase_storage=/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage-12.1.2/ -firebase_storage_web=/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage_web-3.9.12/ -flutter_timezone=/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/ -package_info_plus=/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/ -path_provider_linux=/home/mayank/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ -path_provider_windows=/home/mayank/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/ -shared_preferences=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences-2.3.1/ -shared_preferences_android=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.0/ -shared_preferences_foundation=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/ -shared_preferences_linux=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.0/ -shared_preferences_web=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.1/ -shared_preferences_windows=/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.0/ +cloud_firestore=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore-5.0.2/ +cloud_firestore_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.0.2/ +cloud_functions=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions-5.1.1/ +cloud_functions_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions_web-4.10.0/ +device_info_plus=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ +firebase_auth=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth-5.1.1/ +firebase_auth_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth_web-5.13.0/ +firebase_core=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core-3.4.1/ +firebase_core_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core_web-2.18.0/ +firebase_messaging=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.2/ +firebase_messaging_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging_web-3.9.0/ +firebase_storage=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage-12.1.0/ +firebase_storage_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage_web-3.10.0/ +flutter_timezone=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/flutter_timezone-2.0.1/ +package_info_plus=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/ +path_provider_linux=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ +path_provider_windows=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/ +shared_preferences=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences-2.2.3/ +shared_preferences_android=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_android-2.2.3/ +shared_preferences_foundation=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.4.0/ +shared_preferences_linux=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.3.2/ +shared_preferences_web=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_web-2.2.1/ +shared_preferences_windows=/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.3.2/ diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index 1fd2ab15..0841a659 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"cloud_firestore","path":"/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore-5.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_core-3.3.0/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.4/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage-12.1.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"cloud_firestore","path":"/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore-5.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_core-3.3.0/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.4/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage-12.1.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_android-2.3.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"cloud_firestore","path":"/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore-5.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_core-3.3.0/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.4/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage-12.1.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/home/mayank/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.0/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"cloud_firestore","path":"/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore-5.2.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","native_build":false,"dependencies":[]},{"name":"firebase_auth","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth-5.1.3/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_core-3.3.0/","native_build":true,"dependencies":[]},{"name":"firebase_storage","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage-12.1.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/home/mayank/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.0/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"cloud_firestore_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.1.0/","dependencies":["firebase_core_web"]},{"name":"device_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/device_info_plus-10.1.1/","dependencies":[]},{"name":"firebase_auth_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_auth_web-5.12.5/","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_core_web-2.17.4/","dependencies":[]},{"name":"firebase_messaging_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_messaging_web-3.8.12/","dependencies":["firebase_core_web"]},{"name":"firebase_storage_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/firebase_storage_web-3.9.12/","dependencies":["firebase_core_web"]},{"name":"flutter_timezone","path":"/home/mayank/.pub-cache/hosted/pub.dev/flutter_timezone-2.1.0/","dependencies":[]},{"name":"package_info_plus","path":"/home/mayank/.pub-cache/hosted/pub.dev/package_info_plus-8.0.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/home/mayank/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.1/","dependencies":[]}]},"dependencyGraph":[{"name":"cloud_firestore","dependencies":["cloud_firestore_web","firebase_core"]},{"name":"cloud_firestore_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"device_info_plus","dependencies":[]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_storage","dependencies":["firebase_core","firebase_storage_web"]},{"name":"firebase_storage_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"flutter_timezone","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2024-09-09 18:05:05.733961","version":"3.24.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"cloud_firestore","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore-5.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"cloud_functions","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core-3.4.1/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage-12.1.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/flutter_timezone-2.0.1/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"cloud_firestore","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore-5.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"cloud_functions","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core-3.4.1/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage-12.1.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/flutter_timezone-2.0.1/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_android-2.2.3/","native_build":true,"dependencies":[]}],"macos":[{"name":"cloud_firestore","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore-5.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"cloud_functions","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","native_build":true,"dependencies":[]},{"name":"firebase_auth","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core-3.4.1/","native_build":true,"dependencies":[]},{"name":"firebase_messaging","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging-15.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_storage","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage-12.1.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"flutter_timezone","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/flutter_timezone-2.0.1/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","native_build":false,"dependencies":[]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.3.2/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"cloud_firestore","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore-5.0.2/","native_build":true,"dependencies":["firebase_core"]},{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","native_build":false,"dependencies":[]},{"name":"firebase_auth","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth-5.1.1/","native_build":true,"dependencies":["firebase_core"]},{"name":"firebase_core","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core-3.4.1/","native_build":true,"dependencies":[]},{"name":"firebase_storage","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage-12.1.0/","native_build":true,"dependencies":["firebase_core"]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.3.2/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"cloud_firestore_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_firestore_web-4.0.2/","dependencies":["firebase_core_web"]},{"name":"cloud_functions_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/cloud_functions_web-4.10.0/","dependencies":["firebase_core_web"]},{"name":"device_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/","dependencies":[]},{"name":"firebase_auth_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_auth_web-5.13.0/","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_core_web-2.18.0/","dependencies":[]},{"name":"firebase_messaging_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_messaging_web-3.9.0/","dependencies":["firebase_core_web"]},{"name":"firebase_storage_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/firebase_storage_web-3.10.0/","dependencies":["firebase_core_web"]},{"name":"flutter_timezone","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/flutter_timezone-2.0.1/","dependencies":[]},{"name":"package_info_plus","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/package_info_plus-8.0.2/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/sidhdhi.p/.pub-cache/hosted/pub.dev/shared_preferences_web-2.2.1/","dependencies":[]}]},"dependencyGraph":[{"name":"cloud_firestore","dependencies":["cloud_firestore_web","firebase_core"]},{"name":"cloud_firestore_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"cloud_functions","dependencies":["cloud_functions_web","firebase_core"]},{"name":"cloud_functions_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"device_info_plus","dependencies":[]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"firebase_messaging","dependencies":["firebase_core","firebase_messaging_web"]},{"name":"firebase_messaging_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_storage","dependencies":["firebase_core","firebase_storage_web"]},{"name":"firebase_storage_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"flutter_timezone","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2024-09-16 11:42:12.161649","version":"3.24.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/data/lib/api/network/client.dart b/data/lib/api/network/client.dart index b745a41b..f9e9453e 100644 --- a/data/lib/api/network/client.dart +++ b/data/lib/api/network/client.dart @@ -1,9 +1,71 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../errors/app_error.dart'; +import '../../service/auth/auth_service.dart'; +import '../../utils/constant/firestore_constant.dart'; + +import 'package:http/http.dart' as http; + +import 'endpoint.dart'; +import 'interceptor/auth_client.dart'; + +final httpProvider = Provider((ref) { + final client = http.Client(); + return AuthHttpClient(client, ref.read(firebaseAuthProvider)); +}); + final rawDioProvider = Provider((ref) { return Dio() ..options.connectTimeout = const Duration(seconds: 30) ..options.sendTimeout = const Duration(seconds: 30) ..options.receiveTimeout = const Duration(seconds: 30); }); + +extension HttpExtensions on http.Client { + Future req(Endpoint endpoint) async { + + final request = http.Request( + endpoint.method.name, + Uri.parse('${DataConfig.instance.apiBaseUrl}${endpoint.path}'), + )..headers.addAll(endpoint.headers); + + if (endpoint.data != null) { + request.headers['content-type'] = 'application/json'; + request.body = jsonEncode(endpoint.data); + } + + http.Response response; + + try { + response = await http.Response.fromStream(await send(request)); + + if (response.statusCode >= 200 && response.statusCode < 300) { + return response; + } + } catch (error) { + if (error is SocketException) { + throw const NoConnectionError(); + } else if (error is TimeoutException) { + throw const NoConnectionError(); + } else { + rethrow; + } + } + + throw SomethingWentWrongError( + statusCode: response.statusCode.toString(), + message: response.body, + ); + } +} + +extension HttpResponseExtensions on http.Response { + Map get data { + return body.isNotEmpty ? jsonDecode(utf8.decode(bodyBytes)) : {}; + } +} diff --git a/data/lib/api/network/endpoint.dart b/data/lib/api/network/endpoint.dart new file mode 100644 index 00000000..778a0bd8 --- /dev/null +++ b/data/lib/api/network/endpoint.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +enum HttpMethod { get, post, put, delete, patch } + +@immutable +abstract class Endpoint { + HttpMethod get method => HttpMethod.get; + + String get path; + + Map? get queryParameters => null; + + Map get headers => const {}; + + dynamic get data => null; + + bool get needsAuth => true; + + String? get baseUrl => null; + + String? get contentType => null; +} diff --git a/data/lib/api/network/interceptor/auth_client.dart b/data/lib/api/network/interceptor/auth_client.dart new file mode 100644 index 00000000..d3af8719 --- /dev/null +++ b/data/lib/api/network/interceptor/auth_client.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:http/http.dart' as http; + +class AuthHttpClient extends http.BaseClient { + final http.Client inner; + final FirebaseAuth firebaseAuth; + + AuthHttpClient(this.inner, this.firebaseAuth); + + @override + Future send(http.BaseRequest request) async { + final user = firebaseAuth.currentUser; + + if (user != null) { + final idToken = await user.getIdToken(); + request.headers['Authorization'] = 'Bearer $idToken'; + } + + return inner.send(request); + } +} diff --git a/data/lib/errors/app_error.dart b/data/lib/errors/app_error.dart index fb62f4f8..fff7d799 100644 --- a/data/lib/errors/app_error.dart +++ b/data/lib/errors/app_error.dart @@ -33,7 +33,7 @@ class AppError implements Exception { } else if (error is TypeError) { return SomethingWentWrongError( message: error.toString(), - stackTrace: error.stackTrace, + stackTrace: stack, ); } else if (error is LargeAttachmentUploadError) { return const LargeAttachmentUploadError(); diff --git a/data/lib/service/auth/auth_service.dart b/data/lib/service/auth/auth_service.dart index d7b03f34..eabb5e39 100644 --- a/data/lib/service/auth/auth_service.dart +++ b/data/lib/service/auth/auth_service.dart @@ -10,9 +10,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/user/user_models.dart'; +final firebaseAuthProvider = Provider((ref) => FirebaseAuth.instance); + final authServiceProvider = Provider((ref) { return AuthService( - FirebaseAuth.instance, + ref.read(firebaseAuthProvider), ref.read(userServiceProvider), ref.read(currentUserJsonPod.notifier), ref.read(currentUserSessionJsonPod.notifier), diff --git a/data/lib/service/user/user_endpoint.dart b/data/lib/service/user/user_endpoint.dart new file mode 100644 index 00000000..bf3bc276 --- /dev/null +++ b/data/lib/service/user/user_endpoint.dart @@ -0,0 +1,20 @@ +import '../../api/network/endpoint.dart'; + +class CreateUserEndpoint extends Endpoint { + final String phone; + final String name; + + CreateUserEndpoint({ + required this.phone, + required this.name, + }); + + @override + String get path => 'user/create'; + + @override + HttpMethod get method => HttpMethod.post; + + @override + dynamic get data => {"phone": phone, "name": name}; +} diff --git a/data/lib/service/user/user_service.dart b/data/lib/service/user/user_service.dart index d4695693..27d509af 100644 --- a/data/lib/service/user/user_service.dart +++ b/data/lib/service/user/user_service.dart @@ -1,6 +1,8 @@ import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:http/http.dart' as http; import '../../errors/app_error.dart'; import '../../extensions/list_extensions.dart'; +import '../../api/network/client.dart'; import '../device/device_service.dart'; import '../../utils/constant/firestore_constant.dart'; import '../../utils/dummy_deactivated_account.dart'; @@ -8,11 +10,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/user/user_models.dart'; import '../../storage/app_preferences.dart'; +import 'user_endpoint.dart'; final userServiceProvider = Provider((ref) { final service = UserService( ref.read(currentUserPod), FirebaseFirestore.instance, + ref.read(httpProvider), ref.read(deviceServiceProvider), ); @@ -24,11 +28,13 @@ class UserService { UserModel? _currentUser; final FirebaseFirestore firestore; + final http.Client client; final DeviceService deviceService; UserService( this._currentUser, this.firestore, + this.client, this.deviceService, ); @@ -90,6 +96,24 @@ class UserService { } } + Future createNewUser( + String phoneNumber, + String displayName, + ) async { + try { + final response = await client.req( + CreateUserEndpoint( + name: displayName, + phone: phoneNumber, + ), + ); + + return UserModel.fromJson(response.data); + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } + Future updateUser(UserModel user) async { try { final userRef = _userRef.doc(user.id); @@ -117,6 +141,21 @@ class UserService { } } + Future getUserByPhoneNumber(String number) async { + try { + final snapshot = await _userRef + .where(FireStoreConst.phone, isEqualTo: number) + .get(); + if (snapshot.docs.isNotEmpty) { + return snapshot.docs.first.data(); + } else { + return null; + } + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } + Future> getUsersByIds(List ids) async { final List users = []; try { diff --git a/data/lib/utils/constant/firestore_constant.dart b/data/lib/utils/constant/firestore_constant.dart index 842f9f27..5604467d 100644 --- a/data/lib/utils/constant/firestore_constant.dart +++ b/data/lib/utils/constant/firestore_constant.dart @@ -45,6 +45,25 @@ class FireStoreConst { static const String nameLowercase = "name_lowercase"; static const String profileImageUrl = "profile_img_url"; + // users field const static const String deviceFcmToken = "device_fcm_token"; static const String notifications = "notifications"; + static const String phone = "phone"; + static const String name = "name"; } + +class DataConfig { + static late DataConfig _instance; + + static DataConfig get instance => _instance; + + static void init(DataConfig dataConfig) { + _instance = dataConfig; + } + + final String apiBaseUrl; + + DataConfig({ + required this.apiBaseUrl, + }); +} \ No newline at end of file diff --git a/data/pubspec.yaml b/data/pubspec.yaml index f033220e..efb59127 100644 --- a/data/pubspec.yaml +++ b/data/pubspec.yaml @@ -24,7 +24,9 @@ dependencies: firebase_auth: ^5.0.0 # io + http: ^1.2.1 dio: ^5.0.2 + cloud_functions: ^5.1.1 json_annotation: ^4.9.0 json_serializable: ^6.7.1 freezed_annotation: ^2.4.1 diff --git a/khelo/android/app/src/main/AndroidManifest.xml b/khelo/android/app/src/main/AndroidManifest.xml index 733088e9..cf3c5160 100644 --- a/khelo/android/app/src/main/AndroidManifest.xml +++ b/khelo/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + { + const oldSend = res.send; + res.send = (body) => { + if (res.statusCode < 200 || res.statusCode >= 300) { + (0, logger.error)(`${req.path}, ${res.statusCode}, ${body}`); + } + return oldSend.call(res, body); + }; + next(); +}); + +expressApp.post("/user/create", (req, res) => { + authService.onCallCreateAuthUser(req, res); +}); + exports.teamPlayerChangeObserver = (0, firestore_2.onDocumentUpdated)({region: REGION, document: "teams/{teamId}"}, async (event) => { const snapshot = event.data; if (!snapshot) { @@ -44,6 +66,8 @@ exports.teamPlayerChangeObserver = (0, firestore_2.onDocumentUpdated)({region: R await teamService.notifyOnAddedToTeam(oldTeam, newTeam); }); -exports.fiveMinuteCron = (0, scheduler.onSchedule)({timeZone: exports.TIMEZONE, schedule: "*/5 * * * *"}, async () => { +exports.fiveMinuteCron = (0, scheduler.onSchedule)({timeZone: exports.TIMEZONE, schedule: "*/5 * * * *", region: REGION}, async () => { await matchRepository.processUpcomingMatches(); }); + +exports.apiv1 = (0, callable.onRequest)({region: REGION, concurrency: 100, cors: true}, expressApp); diff --git a/khelo/functions/src/match/match_repository.js b/khelo/functions/src/match/match_repository.js index 8ab238b4..bad1eab2 100644 --- a/khelo/functions/src/match/match_repository.js +++ b/khelo/functions/src/match/match_repository.js @@ -11,23 +11,39 @@ class MatchRepository { matchRef() { return this.db.collection("matches"); } + getAdminTimestampWithZeroSeconds() { + const now = new Date(); + now.setSeconds(0); + now.setMilliseconds(0); + return admin.firestore.Timestamp.fromDate(now); + } async processUpcomingMatches() { - const currentTimestamp = admin.firestore.Timestamp.now(); const NOTIFICATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes in milliseconds + const NOTIFICATION_WINDOW = 5 * 60 * 1000; // 5 minutes in milliseconds + + const currentTimestamp = this.getAdminTimestampWithZeroSeconds(); ; + const startThresholdInSeconds = currentTimestamp.seconds + NOTIFICATION_THRESHOLD / 1000; + const endThresholdInSeconds = currentTimestamp.seconds + (NOTIFICATION_THRESHOLD + NOTIFICATION_WINDOW) / 1000; + + const startThreshold = new admin.firestore.Timestamp(startThresholdInSeconds, 0); + const endThreshold = new admin.firestore.Timestamp(endThresholdInSeconds, 0); + + console.log(`MatchRepository: Getting matches within threshold from ${startThreshold.toDate()} to ${endThreshold.toDate()}`); const upcomingMatchesQuery = this.matchRef() - .where("start_time", ">=", currentTimestamp) - .where("start_time", "<=", new admin.firestore.Timestamp(currentTimestamp.seconds + NOTIFICATION_THRESHOLD / 1000, 0)); + .where("start_at", ">=", startThreshold) + .where("start_at", "<=", endThreshold); try { const upcomingMatchesSnapshot = await upcomingMatchesQuery.get(); if (!upcomingMatchesSnapshot.empty) { + console.log(`MatchRepository: ${upcomingMatchesSnapshot.docs.length} upcoming matches found within threshold`); const promises = upcomingMatchesSnapshot.docs.map(async (matchDoc) => { const matchData = matchDoc.data(); await this.matchService.notifyBeforeMatchStart(matchData); }); await Promise.all(promises); } else { - console.log("No upcoming matches found within the notification threshold."); + console.log("MatchRepository: No upcoming matches found within threshold"); } } catch (e) { console.error("MatchRepository: Error getting upcoming matches:", e); diff --git a/khelo/functions/src/match/match_service.js b/khelo/functions/src/match/match_service.js index c169d11f..9f5918aa 100644 --- a/khelo/functions/src/match/match_service.js +++ b/khelo/functions/src/match/match_service.js @@ -11,26 +11,26 @@ class MatchService { async notifyBeforeMatchStart(match) { const [teamA, teamB] = await this.teamRepository.getTeams(match.team_ids); const matchContributorIds = new Set([ - match.created_by, - match.referee_id, - ...match.players, - ...match.scorer_ids, - ...match.umpire_ids, - ...match.commentator_ids, + ...(match.created_by !== null ? [match.created_by] : []), + ...(match.referee_id !== null ? [match.referee_id] : []), + ...match.players.filter((e) => e !== null), + ...match.scorer_ids.filter((e) => e !== null), + ...match.umpire_ids.filter((e) => e !== null), + ...match.commentator_ids.filter((e) => e !== null), ]); - console.log("Contributors in match:", addedPlayerIds); - if(matchContributorIds.length === 0) { - return; + console.log("MatchService: Contributors in match:", matchContributorIds); + if (matchContributorIds.length === 0) { + return; } - const matchContributors = await this.userRepository.getUsers(matchContributorIds); + const matchContributors = await this.userRepository.getUsers([...matchContributorIds]); const usersToNotify = matchContributors.filter((m) => user_models.userNotificationEnabled(m)).map((m)=> m.id); - console.log("Users to notify:", usersToNotify); + console.log("MatchService: Users to notify:", usersToNotify); -const teamAName = teamA.name; -const teamBName = teamB.name; -const matchId = match.id; - if (usersToNotify.length > 0 && typeof matchId === 'string' && typeof teamAName === 'string' && typeof teamBName === 'string') { + const teamAName = teamA.name; + const teamBName = teamB.name; + const matchId = match.id; + if (usersToNotify.length > 0 && typeof matchId === "string" && typeof teamAName === "string" && typeof teamBName === "string") { const title = "Match Alert!"; const body = `${teamAName} vs. ${teamBName} is starting in 30 minutes!`; await this.notificationService.sendNotification(usersToNotify, title, body, {matchId: matchId, type: "match_start"}); diff --git a/khelo/functions/src/notification/notification_service.js b/khelo/functions/src/notification/notification_service.js index 3ac38434..c4a64aad 100644 --- a/khelo/functions/src/notification/notification_service.js +++ b/khelo/functions/src/notification/notification_service.js @@ -19,7 +19,7 @@ class NotificationService { } } if (tokens.size == 0) { - console.debug("No tokens found for user"); + console.debug("NotificationService: No tokens found for user"); return; } const payload = { diff --git a/khelo/functions/src/team/team_repository.js b/khelo/functions/src/team/team_repository.js index 01323260..8759cee3 100644 --- a/khelo/functions/src/team/team_repository.js +++ b/khelo/functions/src/team/team_repository.js @@ -10,7 +10,7 @@ class TeamRepository { return this.db.collection("teams"); } async getTeams(teamIds) { - const teamRef = this.teamRef.where("id", "in", teamIds); + const teamRef = this.teamRef().where("id", "in", teamIds); try { const teamDoc = await teamRef.get(); return teamDoc.docs.map((doc) => doc.data()); diff --git a/khelo/functions/src/team/team_service.js b/khelo/functions/src/team/team_service.js index fe5e5c58..01c0553c 100644 --- a/khelo/functions/src/team/team_service.js +++ b/khelo/functions/src/team/team_service.js @@ -12,17 +12,17 @@ class TeamService { const oldPlayers = oldTeam.team_players || []; const newPlayers = newTeam.team_players || []; const addedPlayerIds = newPlayers.filter((player) => !oldPlayers.some((oldPlayer) => oldPlayer.id === player.id)).map((m) => m.id); - console.log("Newly added players:", addedPlayerIds); - if(addedPlayerIds.length === 0) { - return; + console.log("TeamService: Newly added players:", addedPlayerIds); + if (addedPlayerIds.length === 0) { + return; } const addedPlayers = await this.userRepository.getUsers(addedPlayerIds); const playersToNotify = addedPlayers.filter((m) => user_models.userNotificationEnabled(m)).map((m)=> m.id); - console.log("Players to notify:", playersToNotify); + console.log("TeamService: Players to notify:", playersToNotify); const teamName=newTeam.name; const teamId= newTeam.id; - if (playersToNotify.length > 0 && typeof teamId === 'string' && typeof teamName === 'string') { + if (playersToNotify.length > 0 && typeof teamId === "string" && typeof teamName === "string") { const title =`Welcome to ${teamName}`; const body = `You have been added to ${teamName}. Get ready to join the action and play with your new teammates!`; await this.notificationService.sendNotification(playersToNotify, title, body, {team_id: teamId, type: "added_to_team"}); diff --git a/khelo/functions/src/user/user_repository.js b/khelo/functions/src/user/user_repository.js index 28eff495..1498a758 100644 --- a/khelo/functions/src/user/user_repository.js +++ b/khelo/functions/src/user/user_repository.js @@ -1,7 +1,8 @@ "use strict"; Object.defineProperty(exports, "__esModule", {value: true}); -exports.UserRepository = void 0; - +exports.firebaseAuthMiddleware = exports.UserRepository = void 0; +const firestore_1 = require("firebase-admin/firestore"); +const admin = require("firebase-admin"); class UserRepository { constructor(db) { this.db = db; @@ -9,6 +10,25 @@ class UserRepository { userRef() { return this.db.collection("users"); } + async createUser(userId, userName, phoneNumber) { + const userRef = this.userRef().doc(userId); + try { + const user = { + id: userId, + name: userName, + name_lowercase: userName.toLowerCase(), + phone: phoneNumber, + created_at: new Date().toISOString(), + create_time: firestore_1.Timestamp.now(), + }; + await userRef.set(user); + + return user; + } catch (e) { + console.error("UserRepository: Error creating user:", e); + return null; + } + } async getUser(userId) { const userRef = this.userRef().doc(userId); try { @@ -43,4 +63,29 @@ class UserRepository { } } } +const firebaseAuthMiddleware = (userRepository) => { + return async (req, res, next) => { + let _a; + const idToken = (_a = req.get("Authorization")) === null || _a === void 0 ? void 0 : _a.split("Bearer ")[1]; + if (!idToken) { + res.status(403).send("Unauthorized"); + return; + } + + const auth = await admin.auth().verifyIdToken(idToken); + if (!auth) { + res.status(403).send("Unauthorized"); + return; + } + const user = await userRepository.getUser(auth.uid); + if (!user) { + res.status(403).send("Unauthorized"); + return; + } + console.log("Authenticated user: ", auth.uid); + req.headers["auth-uid"] = auth.uid; + next(); + }; +}; +exports.firebaseAuthMiddleware = firebaseAuthMiddleware; exports.UserRepository = UserRepository; diff --git a/khelo/ios/Podfile.lock b/khelo/ios/Podfile.lock index e4be8246..4a2b5755 100644 --- a/khelo/ios/Podfile.lock +++ b/khelo/ios/Podfile.lock @@ -1155,6 +1155,12 @@ PODS: - Firebase/Firestore (= 11.0.0) - firebase_core - Flutter + - cloud_functions (5.1.1): + - Firebase/Functions (= 11.0.0) + - firebase_core + - Flutter + - contacts_service (0.2.2): + - Flutter - device_info_plus (0.0.1): - Flutter - Firebase/Auth (11.0.0): @@ -1168,6 +1174,9 @@ PODS: - Firebase/Firestore (11.0.0): - Firebase/CoreOnly - FirebaseFirestore (~> 11.0.0) + - Firebase/Functions (11.0.0): + - Firebase/CoreOnly + - FirebaseFunctions (~> 11.0.0) - Firebase/Messaging (11.0.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.0.0) @@ -1178,7 +1187,7 @@ PODS: - Firebase/Auth (= 11.0.0) - firebase_core - Flutter - - firebase_core (3.4.0): + - firebase_core (3.4.1): - Firebase/CoreOnly (= 11.0.0) - Flutter - firebase_crashlytics (4.1.0): @@ -1241,6 +1250,14 @@ PODS: - gRPC-Core (~> 1.65.0) - leveldb-library (~> 1.22) - nanopb (~> 3.30910.0) + - FirebaseFunctions (11.0.0): + - FirebaseAppCheckInterop (~> 11.0) + - FirebaseAuthInterop (~> 11.0) + - FirebaseCore (~> 11.0) + - FirebaseCoreExtension (~> 11.0) + - FirebaseMessagingInterop (~> 11.0) + - FirebaseSharedSwift (~> 11.0) + - GTMSessionFetcher/Core (~> 3.4) - FirebaseInstallations (11.1.0): - FirebaseCore (~> 11.0) - GoogleUtilities/Environment (~> 8.0) @@ -1255,6 +1272,7 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) + - FirebaseMessagingInterop (11.1.0) - FirebaseRemoteConfigInterop (11.1.0) - FirebaseSessions (11.1.0): - FirebaseCore (~> 11.0) @@ -1434,6 +1452,8 @@ PODS: DEPENDENCIES: - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - cloud_functions (from `.symlinks/plugins/cloud_functions/ios`) + - contacts_service (from `.symlinks/plugins/contacts_service/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -1467,8 +1487,10 @@ SPEC REPOS: - FirebaseCrashlytics - FirebaseFirestore - FirebaseFirestoreInternal + - FirebaseFunctions - FirebaseInstallations - FirebaseMessaging + - FirebaseMessagingInterop - FirebaseRemoteConfigInterop - FirebaseSessions - FirebaseSharedSwift @@ -1489,6 +1511,10 @@ SPEC REPOS: EXTERNAL SOURCES: cloud_firestore: :path: ".symlinks/plugins/cloud_firestore/ios" + cloud_functions: + :path: ".symlinks/plugins/cloud_functions/ios" + contacts_service: + :path: ".symlinks/plugins/contacts_service/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_auth: @@ -1530,10 +1556,12 @@ SPEC CHECKSUMS: abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3 BoringSSL-GRPC: ca6a8e5d04812fce8ffd6437810c2d46f925eaeb cloud_firestore: 15d5767445fbd525df58295240753e87393c854f + cloud_functions: 8503b9219ebace7211e7289a59e2b9b0c53461c2 + contacts_service: 849e1f84281804c8bfbec1b4c3eedcb23c5d3eca device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 firebase_auth: 16ac5db3d064db837ecd845080d7e18e4be7c66d - firebase_core: ceec591a66629daaee82d3321551692c4a871493 + firebase_core: ba84e940cf5cbbc601095f86556560937419195c firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0 firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425 firebase_storage: 3b166e39a19e95b6a2d1c33b4ebff41e12a7615b @@ -1546,8 +1574,10 @@ SPEC CHECKSUMS: FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b FirebaseFirestore: a1758850668dbb503537b7780a2a1fdc5e37c6ce FirebaseFirestoreInternal: 9fcc0ccb987ab73163f2249444e4bfd9eac63748 + FirebaseFunctions: 49653511d8c966ad16c5c02acc2bfd642ef1cec1 FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 + FirebaseMessagingInterop: 02e4dc0c64fa21c97b7c8d514cd2aa6672c252b6 FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5 diff --git a/khelo/ios/Runner.xcodeproj/project.pbxproj b/khelo/ios/Runner.xcodeproj/project.pbxproj index a6199193..80fea780 100644 --- a/khelo/ios/Runner.xcodeproj/project.pbxproj +++ b/khelo/ios/Runner.xcodeproj/project.pbxproj @@ -205,8 +205,8 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, B701D8A68AF0CB72EE4EF76E /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, - 75EEB2C25DCD12D9EE1B3BA1 /* [CP] Embed Pods Frameworks */, - 9C8DD79CF293961A8F4177C2 /* [CP] Copy Pods Resources */, + 9C8DD79CF293961A8F4177C2 /* [CP] Embed Pods Frameworks */, + 9EE7146958D62CE1CD764FEE /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -317,39 +317,39 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; - 75EEB2C25DCD12D9EE1B3BA1 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + name = "Run Script"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 9C8DD79CF293961A8F4177C2 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Run Script"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 9C8DD79CF293961A8F4177C2 /* [CP] Copy Pods Resources */ = { + 9EE7146958D62CE1CD764FEE /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -386,7 +386,8 @@ }; D62BB21752E1710DBAD3A9D1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + alwaysOutOfDate = 1; + buildActionMask = 12; files = ( ); inputFileListPaths = ( diff --git a/khelo/ios/Runner/Info.plist b/khelo/ios/Runner/Info.plist index 765c2c07..64f3ae22 100644 --- a/khelo/ios/Runner/Info.plist +++ b/khelo/ios/Runner/Info.plist @@ -44,6 +44,8 @@ Capture a new profile image using your device's camera for a personalized and up-to-date appearance. NSPhotoLibraryUsageDescription Select a profile image from your device's gallery to showcase your preferred photo in moments of your choice. + NSContactsUsageDescription + Sync and select contacts from your device to easily add team members and enhance collaboration. UIApplicationSupportsIndirectInputEvents UIBackgroundModes diff --git a/khelo/lib/components/country_code_view.dart b/khelo/lib/components/country_code_view.dart new file mode 100644 index 00000000..da5f29eb --- /dev/null +++ b/khelo/lib/components/country_code_view.dart @@ -0,0 +1,113 @@ +import 'package:canopas_country_picker/canopas_country_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; +import 'package:twemoji_v2/twemoji_v2.dart'; + +import '../gen/assets.gen.dart'; + + +class CountryCodeView extends StatelessWidget { + final CountryCode countryCode; + final Function(CountryCode) onCodeChange; + + const CountryCodeView({super.key, required this.countryCode, required this.onCodeChange}); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: () => _showCountryPickerSheet(context), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + decoration: BoxDecoration( + color: context.colorScheme.containerLow, + borderRadius: BorderRadius.circular(30)), + child: Row( + children: [ + ClipPath( + clipper: const _FlagClipper(12), + child: Twemoji( + emoji: countryCode.flag, + height: 35, + twemojiFormat: TwemojiFormat.svg, + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + Assets.images.icArrowDown, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, BlendMode.srcATop), + height: 18, + width: 18, + ) + ], + ), + ), + ); + } + + void _showCountryPickerSheet(BuildContext context) async { + final countryCode = await showCountryCodePickerSheet( + customizationBuilders: _getCustomizationBuilder(context), + context: context, + ); + if (countryCode != null) { + onCodeChange(countryCode); + } + } + + CustomizationBuilders _getCustomizationBuilder(BuildContext context) { + return CustomizationBuilders( + backgroundColor: () => context.colorScheme.surface, + textFieldBuilder: (filter) => DefaultCountryCodeFilterTextField( + filter: filter, + fillColor: context.colorScheme.containerLow, + prefixIcon: Icon( + CupertinoIcons.search, + color: context.colorScheme.textSecondary, + size: 22, + ), + style: AppTextStyle.body1.copyWith( + color: context.colorScheme.textPrimary, + ), + hintStyle: AppTextStyle.body1.copyWith( + color: context.colorScheme.textSecondary, + ), + ), + codeBuilder: (code) => GestureDetector( + onTap: () => context.pop(code), + child: DefaultCountryCodeListItemView( + code: code, + dialCodeStyle: AppTextStyle.body1.copyWith( + color: context.colorScheme.textPrimary, + ), + nameStyle: AppTextStyle.body1.copyWith( + color: context.colorScheme.textPrimary, + ), + ), + ), + ); + } +} + +class _FlagClipper extends CustomClipper { + const _FlagClipper(this.radius); + + final double radius; + + @override + Path getClip(Size size) { + final path = Path(); + final center = Offset(size.width / 2, size.height / 2); + + path.addOval(Rect.fromCircle(center: center, radius: radius)); + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} diff --git a/khelo/lib/main.dart b/khelo/lib/main.dart index 460c3b9a..2c097d95 100644 --- a/khelo/lib/main.dart +++ b/khelo/lib/main.dart @@ -1,4 +1,5 @@ import 'package:data/storage/provider/preferences_provider.dart'; +import 'package:data/utils/constant/firestore_constant.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; @@ -9,6 +10,8 @@ import 'package:khelo/ui/app.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'firebase_options.dart'; +const _baseUrl = 'https://apiv1-g7mqemn2ga-el.a.run.app/'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); final container = await _initContainer(); @@ -21,6 +24,12 @@ Future _initContainer() async { options: DefaultFirebaseOptions.currentPlatform, ); + DataConfig.init( + DataConfig( + apiBaseUrl: _baseUrl, + ), + ); + if (!kDebugMode) { await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); } diff --git a/khelo/lib/ui/app_route.dart b/khelo/lib/ui/app_route.dart index 0839b32b..6ebc233d 100644 --- a/khelo/lib/ui/app_route.dart +++ b/khelo/lib/ui/app_route.dart @@ -19,6 +19,7 @@ import 'package:khelo/ui/flow/settings/edit_profile/edit_profile_screen.dart'; import 'package:khelo/ui/flow/sign_in/phone_verification/phone_verification_screen.dart'; import 'package:khelo/ui/flow/team/add_team/add_team_screen.dart'; import 'package:khelo/ui/flow/team/add_team_member/add_team_member_screen.dart'; +import 'package:khelo/ui/flow/team/add_team_member/contact_selection/contact_selection_screen.dart'; import 'package:khelo/ui/flow/team/detail/make_admin/make_team_admin_screen.dart'; import 'package:khelo/ui/flow/team/detail/team_detail_screen.dart'; import 'package:khelo/ui/flow/team/search_team/search_team_screen.dart'; @@ -47,6 +48,7 @@ class AppRoute { static const pathMatchDetailTab = '/match-detail-tab'; static const pathSearchHome = "/search-home"; static const pathViewAll = "/view-all"; + static const pathContactSelection = "/contact-selection"; final String path; final String? name; @@ -251,6 +253,10 @@ class AppRoute { AppRoute(pathUserDetail, builder: (_) => UserDetailScreen(userId: userId)); + static AppRoute contactSelection({required List memberIds}) => + AppRoute(pathContactSelection, + builder: (_) => ContactSelectionScreen(memberIds: memberIds)); + static final routes = [ GoRoute( path: main.path, @@ -331,6 +337,10 @@ class AppRoute { path: pathUserDetail, builder: (context, state) => state.widget(context), ), + GoRoute( + path: pathContactSelection, + builder: (context, state) => state.widget(context), + ), GoRoute( path: pathMakeTeamAdmin, builder: (context, state) => state.widget(context), diff --git a/khelo/lib/ui/flow/sign_in/sign_in_with_phone/sign_in_with_phone_screen.dart b/khelo/lib/ui/flow/sign_in/sign_in_with_phone/sign_in_with_phone_screen.dart index 885975b5..ff06ce92 100644 --- a/khelo/lib/ui/flow/sign_in/sign_in_with_phone/sign_in_with_phone_screen.dart +++ b/khelo/lib/ui/flow/sign_in/sign_in_with_phone/sign_in_with_phone_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:khelo/components/app_page.dart'; +import 'package:khelo/components/country_code_view.dart'; import 'package:khelo/components/error_snackbar.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/ui/app_route.dart'; @@ -13,8 +14,6 @@ import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_field.dart'; import 'package:style/text/app_text_style.dart'; -import 'components/sign_in_with_phone_country_picker.dart'; - class SignInWithPhoneScreen extends ConsumerWidget { const SignInWithPhoneScreen({super.key}); @@ -76,7 +75,10 @@ class SignInWithPhoneScreen extends ConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SignInWithPhoneCountryPicker(), + CountryCodeView( + countryCode: state.code, + onCodeChange: notifier.changeCountryCode, + ), const SizedBox(width: 8), Expanded( child: AppTextField( diff --git a/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart b/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart index b1962515..9b5c6da9 100644 --- a/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart +++ b/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart @@ -1,5 +1,6 @@ import 'package:data/api/team/team_model.dart'; import 'package:data/api/user/user_models.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -12,6 +13,7 @@ import 'package:khelo/components/image_avatar.dart'; import 'package:khelo/components/user_detail_cell.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/widget_extension.dart'; +import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/matches/add_match/select_squad/components/user_detail_sheet.dart'; import 'package:khelo/ui/flow/team/add_team_member/add_team_member_view_model.dart'; import 'package:khelo/ui/flow/team/add_team_member/components/verify_team_member_sheet.dart'; @@ -51,6 +53,7 @@ class _AddTeamMemberScreenState extends ConsumerState { _observeActionError(); _observeIsAdded(state); + return AppPage( title: context.l10n.add_team_member_screen_title, actions: [ @@ -88,14 +91,20 @@ class _AddTeamMemberScreenState extends ConsumerState { return Padding( padding: context.mediaQueryPadding + const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Stack( + alignment: Alignment.bottomRight, children: [ - _searchField(context, state), - if (state.selectedUsers.isNotEmpty) ...[ - _selectedPlayerList(context, state), - ], - _searchedPlayerList(context, state), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _searchField(context, state), + if (state.selectedUsers.isNotEmpty) ...[ + _selectedPlayerList(context, state), + ], + _searchedPlayerList(context, state), + ], + ), + _addViaContactButton(context), ], ), ); @@ -255,6 +264,48 @@ class _AddTeamMemberScreenState extends ConsumerState { ); } + Widget _addViaContactButton( + BuildContext context + ) { + return OnTapScale( + onTap: () async { + final user = await AppRoute.contactSelection(memberIds: notifier.getMemberIds()).push(context); + if(context.mounted && user != null) { + notifier.selectUser(user); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: context.colorScheme.containerLowOnSurface, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + CupertinoIcons.add, + size: 18, + weight: 24, + color: context.colorScheme.textPrimary, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + context.l10n.add_team_player_add_via_contact_title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: AppTextStyle.button + .copyWith(color: context.colorScheme.textPrimary), + ), + ) + ], + ), + ), + ); + } + Widget _crossIcon(BuildContext context) { return Container( height: 24, diff --git a/khelo/lib/ui/flow/team/add_team_member/add_team_member_view_model.dart b/khelo/lib/ui/flow/team/add_team_member/add_team_member_view_model.dart index 3dc2f09c..6ba18518 100644 --- a/khelo/lib/ui/flow/team/add_team_member/add_team_member_view_model.dart +++ b/khelo/lib/ui/flow/team/add_team_member/add_team_member_view_model.dart @@ -22,14 +22,13 @@ class AddTeamMemberViewNotifier extends StateNotifier { final UserService _userService; final TeamService _teamService; Timer? _debounce; + TeamModel? _team; AddTeamMemberViewNotifier( this._userService, this._teamService, ) : super(AddTeamMemberState(searchController: TextEditingController())); - late TeamModel _team; - void setData(TeamModel team) { _team = team; } @@ -59,7 +58,7 @@ class AddTeamMemberViewNotifier extends StateNotifier { } void selectUser(UserModel user) { - final role = (_team.created_by == user.id) + final role = (_team?.created_by == user.id) ? TeamPlayerRole.admin : TeamPlayerRole.player; final player = TeamPlayer(id: user.id, role: role, user: user); @@ -75,7 +74,7 @@ class AddTeamMemberViewNotifier extends StateNotifier { Future addPlayersToTeam() async { state = state.copyWith(isAddInProgress: true, actionError: null); try { - await _teamService.addPlayersToTeam(_team.id, state.selectedUsers); + await _teamService.addPlayersToTeam(_team?.id ?? '', state.selectedUsers); state = state.copyWith(isAddInProgress: false, isAdded: true); } catch (e) { state = state.copyWith(isAddInProgress: false, actionError: e); @@ -84,6 +83,12 @@ class AddTeamMemberViewNotifier extends StateNotifier { } } + List getMemberIds() { + var memberIds = _team?.players.map((e) => e.id).toList() ?? []; + memberIds.addAll(state.selectedUsers.map((e) => e.id)); + return memberIds; + } + @override void dispose() { _debounce?.cancel(); diff --git a/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_sheet.dart b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_sheet.dart new file mode 100644 index 00000000..520316fc --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_sheet.dart @@ -0,0 +1,181 @@ +import 'package:canopas_country_picker/canopas_country_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:khelo/components/country_code_view.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/extensions/widget_extension.dart'; +import 'package:style/button/primary_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_field.dart'; +import 'package:style/text/app_text_style.dart'; + +import 'confirm_number_view_model.dart'; + +class ConfirmNumberSheet extends ConsumerStatefulWidget { + final CountryCode? code; + final String? defaultNumber; + + static Future show( + BuildContext context, { + CountryCode? code, + String? defaultNumber, + }) { + HapticFeedback.mediumImpact(); + return showModalBottomSheet( + context: context, + isScrollControlled: true, + enableDrag: false, + showDragHandle: true, + useRootNavigator: true, + backgroundColor: context.colorScheme.surface, + builder: (context) { + return Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: ConfirmNumberSheet( + code: code, + defaultNumber: defaultNumber, + ), + ); + }, + ); + } + + const ConfirmNumberSheet({ + super.key, + this.code, + this.defaultNumber, + }); + + @override + ConsumerState createState() => _ConfirmNumberSheetState(); +} + +class _ConfirmNumberSheetState extends ConsumerState { + late ConfirmNumberViewNotifier notifier; + + @override + void initState() { + notifier = ref.read(confirmNumberStateProvider.notifier); + runPostFrame(() => notifier.setDate(widget.code, widget.defaultNumber)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(confirmNumberStateProvider); + + _observeIsPop(state); + + return Container( + padding: context.mediaQueryPadding + + const EdgeInsets.only(bottom: 24, left: 16, right: 16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.confirm_number_confirm_phone_title, + style: AppTextStyle.header3 + .copyWith(color: context.colorScheme.textPrimary), + ), + _phoneInputField(context, state), + PrimaryButton( + context.l10n.confirm_number_confirm_title, + enabled: state.isButtonEnable, + onPressed: notifier.onConfirmTap, + ), + ], + ), + ), + ); + } + + Widget _phoneInputField( + BuildContext context, + ConfirmNumberViewState state, + ) { + return MediaQuery.withNoTextScaling( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 40), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CountryCodeView( + countryCode: state.code, + onCodeChange: notifier.onCodeChange, + ), + const SizedBox(width: 8), + Expanded( + child: AppTextField( + controller: state.phoneController, + autoFocus: true, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + style: AppTextStyle.header2.copyWith( + color: context.colorScheme.textSecondary, + ), + hintStyle: AppTextStyle.header2.copyWith( + color: context.colorScheme.outline, + ), + hintText: context.l10n.sign_in_phone_number_placeholder, + backgroundColor: context.colorScheme.containerLowOnSurface, + borderRadius: BorderRadius.circular(40), + borderType: AppTextFieldBorderType.outline, + borderColor: BorderColor( + focusColor: Colors.transparent, + unFocusColor: Colors.transparent), + prefixIcon: _inputFieldPrefix(context, state), + prefixIconConstraints: const BoxConstraints.tightFor(), + onChanged: (_) => notifier.onTextChange(), + onSubmitted: (_) => notifier.onConfirmTap(), + onTapOutside: (event) => + FocusManager.instance.primaryFocus?.unfocus(), + ), + ), + ], + ), + ), + ); + } + + Widget _inputFieldPrefix( + BuildContext context, + ConfirmNumberViewState state, + ) { + return Padding( + padding: const EdgeInsets.only(left: 12.0), + child: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + state.code.dialCode, + style: AppTextStyle.header2 + .copyWith(color: context.colorScheme.textPrimary), + ), + VerticalDivider( + width: 24, + color: context.colorScheme.outline, + ), + ], + ), + ), + ); + } + + void _observeIsPop(ConfirmNumberViewState state) { + ref.listen(confirmNumberStateProvider.select((value) => value.isPop), + (previous, next) { + if (next == true && context.mounted) { + String number = state.phoneController.text; + context.pop((state.code, number)); + } + }); + } +} diff --git a/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.dart b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.dart new file mode 100644 index 00000000..04e992d6 --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.dart @@ -0,0 +1,54 @@ +import 'package:canopas_country_picker/canopas_country_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'confirm_number_view_model.freezed.dart'; + +final confirmNumberStateProvider = StateNotifierProvider.autoDispose< + ConfirmNumberViewNotifier, ConfirmNumberViewState>((ref) { + return ConfirmNumberViewNotifier(); +}); + +class ConfirmNumberViewNotifier extends StateNotifier { + ConfirmNumberViewNotifier() + : super(ConfirmNumberViewState( + phoneController: TextEditingController(), + code: CountryCode.getCountryCodeByAlpha2( + countryAlpha2Code: + WidgetsBinding.instance.platformDispatcher.locale.countryCode, + ), + )); + + void setDate(CountryCode? code, String? defaultNumber) { + state.phoneController.text = defaultNumber ?? ''; + if (code != null) { + state = state.copyWith(code: code); + } + onTextChange(); + } + + void onCodeChange(CountryCode code) { + state = state.copyWith(code: code); + } + + void onTextChange() { + state = state.copyWith( + isButtonEnable: state.phoneController.text.length > 3); + } + + void onConfirmTap() { + state = state.copyWith(isPop: true); + } +} + +@freezed +class ConfirmNumberViewState with _$ConfirmNumberViewState { + const factory ConfirmNumberViewState({ + Object? error, + required TextEditingController phoneController, + required CountryCode code, + @Default(false) bool isButtonEnable, + @Default(false) bool isPop, + }) = _ConfirmNumberViewState; +} diff --git a/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.freezed.dart b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.freezed.dart new file mode 100644 index 00000000..e82d3fd9 --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_view_model.freezed.dart @@ -0,0 +1,237 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'confirm_number_view_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ConfirmNumberViewState { + Object? get error => throw _privateConstructorUsedError; + TextEditingController get phoneController => + throw _privateConstructorUsedError; + CountryCode get code => throw _privateConstructorUsedError; + bool get isButtonEnable => throw _privateConstructorUsedError; + bool get isPop => throw _privateConstructorUsedError; + + /// Create a copy of ConfirmNumberViewState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConfirmNumberViewStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConfirmNumberViewStateCopyWith<$Res> { + factory $ConfirmNumberViewStateCopyWith(ConfirmNumberViewState value, + $Res Function(ConfirmNumberViewState) then) = + _$ConfirmNumberViewStateCopyWithImpl<$Res, ConfirmNumberViewState>; + @useResult + $Res call( + {Object? error, + TextEditingController phoneController, + CountryCode code, + bool isButtonEnable, + bool isPop}); +} + +/// @nodoc +class _$ConfirmNumberViewStateCopyWithImpl<$Res, + $Val extends ConfirmNumberViewState> + implements $ConfirmNumberViewStateCopyWith<$Res> { + _$ConfirmNumberViewStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConfirmNumberViewState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? phoneController = null, + Object? code = null, + Object? isButtonEnable = null, + Object? isPop = null, + }) { + return _then(_value.copyWith( + error: freezed == error ? _value.error : error, + phoneController: null == phoneController + ? _value.phoneController + : phoneController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as CountryCode, + isButtonEnable: null == isButtonEnable + ? _value.isButtonEnable + : isButtonEnable // ignore: cast_nullable_to_non_nullable + as bool, + isPop: null == isPop + ? _value.isPop + : isPop // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ConfirmNumberViewStateImplCopyWith<$Res> + implements $ConfirmNumberViewStateCopyWith<$Res> { + factory _$$ConfirmNumberViewStateImplCopyWith( + _$ConfirmNumberViewStateImpl value, + $Res Function(_$ConfirmNumberViewStateImpl) then) = + __$$ConfirmNumberViewStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Object? error, + TextEditingController phoneController, + CountryCode code, + bool isButtonEnable, + bool isPop}); +} + +/// @nodoc +class __$$ConfirmNumberViewStateImplCopyWithImpl<$Res> + extends _$ConfirmNumberViewStateCopyWithImpl<$Res, + _$ConfirmNumberViewStateImpl> + implements _$$ConfirmNumberViewStateImplCopyWith<$Res> { + __$$ConfirmNumberViewStateImplCopyWithImpl( + _$ConfirmNumberViewStateImpl _value, + $Res Function(_$ConfirmNumberViewStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ConfirmNumberViewState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? phoneController = null, + Object? code = null, + Object? isButtonEnable = null, + Object? isPop = null, + }) { + return _then(_$ConfirmNumberViewStateImpl( + error: freezed == error ? _value.error : error, + phoneController: null == phoneController + ? _value.phoneController + : phoneController // ignore: cast_nullable_to_non_nullable + as TextEditingController, + code: null == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as CountryCode, + isButtonEnable: null == isButtonEnable + ? _value.isButtonEnable + : isButtonEnable // ignore: cast_nullable_to_non_nullable + as bool, + isPop: null == isPop + ? _value.isPop + : isPop // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$ConfirmNumberViewStateImpl implements _ConfirmNumberViewState { + const _$ConfirmNumberViewStateImpl( + {this.error, + required this.phoneController, + required this.code, + this.isButtonEnable = false, + this.isPop = false}); + + @override + final Object? error; + @override + final TextEditingController phoneController; + @override + final CountryCode code; + @override + @JsonKey() + final bool isButtonEnable; + @override + @JsonKey() + final bool isPop; + + @override + String toString() { + return 'ConfirmNumberViewState(error: $error, phoneController: $phoneController, code: $code, isButtonEnable: $isButtonEnable, isPop: $isPop)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConfirmNumberViewStateImpl && + const DeepCollectionEquality().equals(other.error, error) && + (identical(other.phoneController, phoneController) || + other.phoneController == phoneController) && + (identical(other.code, code) || other.code == code) && + (identical(other.isButtonEnable, isButtonEnable) || + other.isButtonEnable == isButtonEnable) && + (identical(other.isPop, isPop) || other.isPop == isPop)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(error), + phoneController, + code, + isButtonEnable, + isPop); + + /// Create a copy of ConfirmNumberViewState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConfirmNumberViewStateImplCopyWith<_$ConfirmNumberViewStateImpl> + get copyWith => __$$ConfirmNumberViewStateImplCopyWithImpl< + _$ConfirmNumberViewStateImpl>(this, _$identity); +} + +abstract class _ConfirmNumberViewState implements ConfirmNumberViewState { + const factory _ConfirmNumberViewState( + {final Object? error, + required final TextEditingController phoneController, + required final CountryCode code, + final bool isButtonEnable, + final bool isPop}) = _$ConfirmNumberViewStateImpl; + + @override + Object? get error; + @override + TextEditingController get phoneController; + @override + CountryCode get code; + @override + bool get isButtonEnable; + @override + bool get isPop; + + /// Create a copy of ConfirmNumberViewState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConfirmNumberViewStateImplCopyWith<_$ConfirmNumberViewStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_screen.dart b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_screen.dart new file mode 100644 index 00000000..29500ce9 --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_screen.dart @@ -0,0 +1,304 @@ +import 'dart:typed_data'; + +import 'package:canopas_country_picker/canopas_country_picker.dart'; +import 'package:contacts_service/contacts_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:khelo/components/app_page.dart'; +import 'package:khelo/components/empty_screen.dart'; +import 'package:khelo/components/error_screen.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/extensions/widget_extension.dart'; +import 'package:khelo/ui/flow/team/add_team_member/confirm_number_sheet/confirm_number_sheet.dart'; +import 'package:khelo/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/button/secondary_button.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/text/app_text_style.dart'; + +import '../../../../../components/error_snackbar.dart'; + +class ContactSelectionScreen extends ConsumerStatefulWidget { + final List memberIds; + + const ContactSelectionScreen({super.key, required this.memberIds}); + + @override + ConsumerState createState() => _ContactSelectionScreenState(); +} + +class _ContactSelectionScreenState + extends ConsumerState { + late ContactSelectionViewNotifier notifier; + + @override + void initState() { + notifier = ref.read(contactSelectionStateProvider.notifier); + runPostFrame(() => notifier.setDate(widget.memberIds)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(contactSelectionStateProvider); + + _observeActionError(); + _observeAlreadyAdded(); + _observeSelectedUser(); + + return AppPage( + title: context.l10n.contact_selection_contact_title, + actions: [ + if (state.isActionInProgress) + const Padding( + padding: EdgeInsets.only(right: 16.0), + child: AppProgressIndicator( + size: AppProgressIndicatorSize.small, + ), + ), + ], + body: Builder( + builder: (context) => Padding( + padding: context.mediaQueryPadding, + child: _body(context, state), + )), + ); + } + + Widget _body(BuildContext context, ContactSelectionState state) { + if (state.loading) { + return const Center( + child: AppProgressIndicator(), + ); + } + + if (state.error != null) { + return ErrorScreen( + error: state.error, + onRetryTap: notifier.fetchContacts, + ); + } + if (state.contacts.isEmpty && state.hasContactPermission) { + return EmptyScreen( + title: context.l10n.contact_selection_empty_contact, + description: context.l10n.contact_selection_empty_contact_description, + isShowButton: false, + ); + } + + if (!state.hasContactPermission) { + return EmptyScreen( + title: context.l10n.contact_selection_access_permission, + description: + context.l10n.contact_selection_access_permission_description, + buttonTitle: context.l10n.contact_selection_allow_title, + onTap: () => _requestContactPermission(context), + ); + } + + return ListView.separated( + itemCount: state.contacts.length, + separatorBuilder: (context, index) => Divider( + color: context.colorScheme.outline, + height: 32, + ), + itemBuilder: (context, index) { + final contact = state.contacts.elementAt(index); + return _contactCellView(context, state.isActionInProgress, contact); + }, + ); + } + + Widget _contactCellView( + BuildContext context, + bool isActionInProgress, + Contact contact, + ) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + _profileImageView(context, contact.avatar, + contact.displayName?.characters.firstOrNull), + const SizedBox(width: 16), + Expanded( + child: Text( + contact.displayName ?? '', + style: AppTextStyle.subtitle2 + .copyWith(color: context.colorScheme.textPrimary), + ), + ), + const SizedBox(width: 4), + SecondaryButton( + context.l10n.common_add_title, + enabled: contact.phones != null && + contact.phones!.isNotEmpty && + !isActionInProgress, + onPressed: () async { + if (contact.phones == null || contact.phones!.isEmpty) { + return; + } + if ((contact.phones?.length ?? 0) > 1) { + _showSelectNumberDialog(context, contact); + } else { + final firstNumber = contact.phones?.first.value; + if (firstNumber != null && firstNumber.isNotEmpty) { + showConfirmNumberSheet( + context, contact.displayName, firstNumber); + } + } + }, + ), + ], + ), + ); + } + + Future _showSelectNumberDialog( + BuildContext context, + Contact contact, + ) async { + final number = await showAdaptiveDialog( + context: context, + barrierDismissible: true, + builder: (context) => AlertDialog.adaptive( + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: contact.phones?.map((e) { + final showDivider = contact.phones?.lastOrNull != e; + if (e.value == null || e.value!.isEmpty) { + return const SizedBox(); + } + return OnTapScale( + onTap: () => context.pop(e.value), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + e.value ?? '', + style: AppTextStyle.subtitle3 + .copyWith(color: context.colorScheme.textPrimary), + ), + if (showDivider) + Divider( + height: 36, + color: context.colorScheme.outline, + ), + ], + ), + ); + }).toList() ?? + [], + ), + ), + ), + ); + + if (context.mounted && number != null) { + showConfirmNumberSheet(context, contact.displayName, number); + } + } + + Future showConfirmNumberSheet( + BuildContext context, + String? displayName, + String phoneNumber, + ) async { + final (code, number) = notifier.getNormalisedNumber(phoneNumber); + final confirmedNumber = + await ConfirmNumberSheet.show<(CountryCode, String)>( + context, + code: code, + defaultNumber: number, + ); + if (context.mounted && confirmedNumber != null) { + notifier.getUserByPhoneNumber( + displayName, + "${confirmedNumber.$1.dialCode} ${confirmedNumber.$2}", + ); + } + } + + Widget _profileImageView( + BuildContext context, + Uint8List? image, + String? initial, + ) { + return Container( + height: 40, + width: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.containerHigh, + image: image != null && image.isNotEmpty + ? DecorationImage( + image: MemoryImage(image), + ) + : null), + child: image != null && image.isNotEmpty + ? null + : Text( + initial ?? '?', + textScaler: TextScaler.noScaling, + style: AppTextStyle.subtitle1 + .copyWith(color: context.colorScheme.textPrimary), + ), + ); + } + + Future _requestContactPermission( + BuildContext context, + ) async { + final PermissionStatus status = await Permission.contacts.status; + + if (status == PermissionStatus.permanentlyDenied) { + openAppSettings(); + return; + } + + if (status == PermissionStatus.denied) { + await Permission.contacts.request(); + if (!context.mounted) return; + } + + notifier.checkContactPermission(requestIfNotGranted: false); + } + + void _observeActionError() { + ref.listen( + contactSelectionStateProvider.select((value) => value.actionError), + (previous, next) { + if (next != null) { + showErrorSnackBar(context: context, error: next); + } + }); + } + + void _observeAlreadyAdded() { + ref.listen( + contactSelectionStateProvider.select((value) => value.alreadyAdded), + (previous, next) { + if (next == true && context.mounted) { + showErrorSnackBar( + context: context, + error: context.l10n.contact_selection_already_added, + ); + } + }); + } + + void _observeSelectedUser() { + ref.listen( + contactSelectionStateProvider.select((value) => value.selectedUser), + (previous, next) { + if (next != null && context.mounted) { + context.pop(next); + } + }); + } +} diff --git a/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.dart b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.dart new file mode 100644 index 00000000..722d9c99 --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.dart @@ -0,0 +1,172 @@ +import 'package:canopas_country_picker/canopas_country_picker.dart'; +import 'package:data/api/user/user_models.dart'; +import 'package:data/service/device/device_service.dart'; +import 'package:data/service/user/user_service.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:contacts_service/contacts_service.dart'; +import 'package:permission_handler/permission_handler.dart'; + +part 'contact_selection_view_model.freezed.dart'; + +final contactSelectionStateProvider = StateNotifierProvider.autoDispose< + ContactSelectionViewNotifier, ContactSelectionState>((ref) { + return ContactSelectionViewNotifier( + ref.read(userServiceProvider), + ref.read(deviceServiceProvider), + ); +}); + +class ContactSelectionViewNotifier + extends StateNotifier { + final UserService _userService; + final DeviceService _deviceService; + List fetchedContacts = []; + List memberIds = []; + String? deviceCountryCode = + WidgetsBinding.instance.platformDispatcher.locale.countryCode; + + ContactSelectionViewNotifier(this._userService, this._deviceService) + : super(const ContactSelectionState()) { + fetchCountryCode(); + } + + void setDate(List memberIds) { + this.memberIds = memberIds; + checkContactPermission(); + } + + Future fetchContacts() async { + try { + state = state.copyWith(error: null, loading: true); + fetchedContacts = await ContactsService.getContacts(); + state = state.copyWith(contacts: fetchedContacts, loading: false); + } catch (e) { + state = state.copyWith(loading: false, error: e); + debugPrint("ContactSelectionViewNotifier: Error getting contacts"); + } + } + + void fetchCountryCode() async { + try { + deviceCountryCode = await _deviceService.countryCode; + } catch (e) { + debugPrint( + "ContactSelectionViewNotifier: Error in fetchCountryCode -> $e"); + } + } + + void checkContactPermission({requestIfNotGranted = true}) async { + var status = await Permission.contacts.status; + if (requestIfNotGranted && + !status.isGranted && + status != PermissionStatus.permanentlyDenied) { + // Show initial permission prompt after 2 seconds + await Future.delayed(const Duration(seconds: 2)); + if (!mounted) return; + status = await Permission.contacts.request(); + } + + state = state.copyWith( + hasContactPermission: status.isGranted, + loading: false, + ); + + if (status.isGranted) { + fetchContacts(); + } + } + + Future getUserByPhoneNumber(String? name, String number) async { + state = state.copyWith(alreadyAdded: false, isActionInProgress: true); + try { + final user = await _userService.getUserByPhoneNumber(number); + if (user == null) { + createUser(name, number); + } else if (memberIds.contains(user.id)) { + state = state.copyWith(alreadyAdded: true, isActionInProgress: false); + } else { + state = state.copyWith(selectedUser: user, isActionInProgress: false); + } + } catch (e) { + state = state.copyWith(actionError: e, isActionInProgress: false); + debugPrint( + "ContactSelectionViewNotifier: Error getting user by phone number $e"); + } + } + + Future createUser(String? name, String number) async { + if (name == null) { + state = state.copyWith(isActionInProgress: false); + return; + } + + try { + final user = await _userService.createNewUser(number, name); + state = state.copyWith(selectedUser: user, isActionInProgress: false); + } catch (e) { + state = state.copyWith(actionError: e, isActionInProgress: false); + debugPrint("ContactSelectionViewNotifier: Error creating new user $e"); + } + } + + (CountryCode, String) getNormalisedNumber(String phoneNumber) { + String? code; + if (phoneNumber.startsWith('+')) { + phoneNumber = keepDigitAndPlusAtStart(phoneNumber); + + final matchedCountryCode = CountryCode.allCodes + .where((element) => phoneNumber.startsWith(element.dialCode)) + .firstOrNull + ?.dialCode; + code = matchedCountryCode ?? deviceCountryCode; + final trimFrom = matchedCountryCode != null ? code?.length : 1; + phoneNumber = phoneNumber.substring(trimFrom ?? 1); + } else { + phoneNumber = phoneNumber.replaceAll(RegExp(r'[^0-9]'), ''); + } + + CountryCode? countryCode; + if (code == null) { + countryCode = CountryCode.getCountryCodeByAlpha2( + countryAlpha2Code: deviceCountryCode, + ); + } else { + countryCode = CountryCode.getCountryCodeByDialCode(dialCode: code); + // handle default returned US code in case not found + if (countryCode.dialCode == "+1" && code != "+1") { + countryCode = CountryCode.getCountryCodeByAlpha2( + countryAlpha2Code: deviceCountryCode, + ); + } + } + return (countryCode, phoneNumber); + } +} + +String keepDigitAndPlusAtStart(String input) { + String formatted = input.replaceAll(RegExp(r'[^+\d]'), ''); + + if (formatted.startsWith('+')) { + formatted = '+${formatted.replaceAll(RegExp(r'[^\d]'), '')}'; + } else { + formatted = formatted.replaceAll(RegExp(r'[^\d]'), ''); + } + + return formatted; +} + +@freezed +class ContactSelectionState with _$ContactSelectionState { + const factory ContactSelectionState({ + Object? error, + Object? actionError, + UserModel? selectedUser, + @Default(false) bool loading, + @Default(false) bool isActionInProgress, + @Default(false) bool alreadyAdded, + @Default(false) bool hasContactPermission, + @Default([]) List contacts, + }) = _ContactSelectionState; +} diff --git a/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.freezed.dart b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.freezed.dart new file mode 100644 index 00000000..6403f311 --- /dev/null +++ b/khelo/lib/ui/flow/team/add_team_member/contact_selection/contact_selection_view_model.freezed.dart @@ -0,0 +1,323 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'contact_selection_view_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ContactSelectionState { + Object? get error => throw _privateConstructorUsedError; + Object? get actionError => throw _privateConstructorUsedError; + UserModel? get selectedUser => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + bool get isActionInProgress => throw _privateConstructorUsedError; + bool get alreadyAdded => throw _privateConstructorUsedError; + bool get hasContactPermission => throw _privateConstructorUsedError; + List get contacts => throw _privateConstructorUsedError; + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ContactSelectionStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ContactSelectionStateCopyWith<$Res> { + factory $ContactSelectionStateCopyWith(ContactSelectionState value, + $Res Function(ContactSelectionState) then) = + _$ContactSelectionStateCopyWithImpl<$Res, ContactSelectionState>; + @useResult + $Res call( + {Object? error, + Object? actionError, + UserModel? selectedUser, + bool loading, + bool isActionInProgress, + bool alreadyAdded, + bool hasContactPermission, + List contacts}); + + $UserModelCopyWith<$Res>? get selectedUser; +} + +/// @nodoc +class _$ContactSelectionStateCopyWithImpl<$Res, + $Val extends ContactSelectionState> + implements $ContactSelectionStateCopyWith<$Res> { + _$ContactSelectionStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? actionError = freezed, + Object? selectedUser = freezed, + Object? loading = null, + Object? isActionInProgress = null, + Object? alreadyAdded = null, + Object? hasContactPermission = null, + Object? contacts = null, + }) { + return _then(_value.copyWith( + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + selectedUser: freezed == selectedUser + ? _value.selectedUser + : selectedUser // ignore: cast_nullable_to_non_nullable + as UserModel?, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + isActionInProgress: null == isActionInProgress + ? _value.isActionInProgress + : isActionInProgress // ignore: cast_nullable_to_non_nullable + as bool, + alreadyAdded: null == alreadyAdded + ? _value.alreadyAdded + : alreadyAdded // ignore: cast_nullable_to_non_nullable + as bool, + hasContactPermission: null == hasContactPermission + ? _value.hasContactPermission + : hasContactPermission // ignore: cast_nullable_to_non_nullable + as bool, + contacts: null == contacts + ? _value.contacts + : contacts // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserModelCopyWith<$Res>? get selectedUser { + if (_value.selectedUser == null) { + return null; + } + + return $UserModelCopyWith<$Res>(_value.selectedUser!, (value) { + return _then(_value.copyWith(selectedUser: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ContactSelectionStateImplCopyWith<$Res> + implements $ContactSelectionStateCopyWith<$Res> { + factory _$$ContactSelectionStateImplCopyWith( + _$ContactSelectionStateImpl value, + $Res Function(_$ContactSelectionStateImpl) then) = + __$$ContactSelectionStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {Object? error, + Object? actionError, + UserModel? selectedUser, + bool loading, + bool isActionInProgress, + bool alreadyAdded, + bool hasContactPermission, + List contacts}); + + @override + $UserModelCopyWith<$Res>? get selectedUser; +} + +/// @nodoc +class __$$ContactSelectionStateImplCopyWithImpl<$Res> + extends _$ContactSelectionStateCopyWithImpl<$Res, + _$ContactSelectionStateImpl> + implements _$$ContactSelectionStateImplCopyWith<$Res> { + __$$ContactSelectionStateImplCopyWithImpl(_$ContactSelectionStateImpl _value, + $Res Function(_$ContactSelectionStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = freezed, + Object? actionError = freezed, + Object? selectedUser = freezed, + Object? loading = null, + Object? isActionInProgress = null, + Object? alreadyAdded = null, + Object? hasContactPermission = null, + Object? contacts = null, + }) { + return _then(_$ContactSelectionStateImpl( + error: freezed == error ? _value.error : error, + actionError: freezed == actionError ? _value.actionError : actionError, + selectedUser: freezed == selectedUser + ? _value.selectedUser + : selectedUser // ignore: cast_nullable_to_non_nullable + as UserModel?, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + isActionInProgress: null == isActionInProgress + ? _value.isActionInProgress + : isActionInProgress // ignore: cast_nullable_to_non_nullable + as bool, + alreadyAdded: null == alreadyAdded + ? _value.alreadyAdded + : alreadyAdded // ignore: cast_nullable_to_non_nullable + as bool, + hasContactPermission: null == hasContactPermission + ? _value.hasContactPermission + : hasContactPermission // ignore: cast_nullable_to_non_nullable + as bool, + contacts: null == contacts + ? _value._contacts + : contacts // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$ContactSelectionStateImpl implements _ContactSelectionState { + const _$ContactSelectionStateImpl( + {this.error, + this.actionError, + this.selectedUser, + this.loading = false, + this.isActionInProgress = false, + this.alreadyAdded = false, + this.hasContactPermission = false, + final List contacts = const []}) + : _contacts = contacts; + + @override + final Object? error; + @override + final Object? actionError; + @override + final UserModel? selectedUser; + @override + @JsonKey() + final bool loading; + @override + @JsonKey() + final bool isActionInProgress; + @override + @JsonKey() + final bool alreadyAdded; + @override + @JsonKey() + final bool hasContactPermission; + final List _contacts; + @override + @JsonKey() + List get contacts { + if (_contacts is EqualUnmodifiableListView) return _contacts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_contacts); + } + + @override + String toString() { + return 'ContactSelectionState(error: $error, actionError: $actionError, selectedUser: $selectedUser, loading: $loading, isActionInProgress: $isActionInProgress, alreadyAdded: $alreadyAdded, hasContactPermission: $hasContactPermission, contacts: $contacts)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ContactSelectionStateImpl && + const DeepCollectionEquality().equals(other.error, error) && + const DeepCollectionEquality() + .equals(other.actionError, actionError) && + (identical(other.selectedUser, selectedUser) || + other.selectedUser == selectedUser) && + (identical(other.loading, loading) || other.loading == loading) && + (identical(other.isActionInProgress, isActionInProgress) || + other.isActionInProgress == isActionInProgress) && + (identical(other.alreadyAdded, alreadyAdded) || + other.alreadyAdded == alreadyAdded) && + (identical(other.hasContactPermission, hasContactPermission) || + other.hasContactPermission == hasContactPermission) && + const DeepCollectionEquality().equals(other._contacts, _contacts)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(error), + const DeepCollectionEquality().hash(actionError), + selectedUser, + loading, + isActionInProgress, + alreadyAdded, + hasContactPermission, + const DeepCollectionEquality().hash(_contacts)); + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ContactSelectionStateImplCopyWith<_$ContactSelectionStateImpl> + get copyWith => __$$ContactSelectionStateImplCopyWithImpl< + _$ContactSelectionStateImpl>(this, _$identity); +} + +abstract class _ContactSelectionState implements ContactSelectionState { + const factory _ContactSelectionState( + {final Object? error, + final Object? actionError, + final UserModel? selectedUser, + final bool loading, + final bool isActionInProgress, + final bool alreadyAdded, + final bool hasContactPermission, + final List contacts}) = _$ContactSelectionStateImpl; + + @override + Object? get error; + @override + Object? get actionError; + @override + UserModel? get selectedUser; + @override + bool get loading; + @override + bool get isActionInProgress; + @override + bool get alreadyAdded; + @override + bool get hasContactPermission; + @override + List get contacts; + + /// Create a copy of ContactSelectionState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ContactSelectionStateImplCopyWith<_$ContactSelectionStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/khelo/macos/Flutter/GeneratedPluginRegistrant.swift b/khelo/macos/Flutter/GeneratedPluginRegistrant.swift index e2c13406..9019b189 100644 --- a/khelo/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/khelo/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import cloud_firestore +import cloud_functions import device_info_plus import file_selector_macos import firebase_auth @@ -23,6 +24,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) diff --git a/khelo/pubspec.lock b/khelo/pubspec.lock index 272b2f11..ab5a7a7c 100644 --- a/khelo/pubspec.lock +++ b/khelo/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" + sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692 url: "https://pub.dev" source: hosted - version: "1.3.41" + version: "1.3.42" _macros: dependency: transitive description: dart @@ -230,6 +230,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + cloud_functions: + dependency: transitive + description: + name: cloud_functions + sha256: aa107ef8f548ea2d08c08691e7476a1f6f7faba539b24965c413ae3c7188b38a + url: "https://pub.dev" + source: hosted + version: "5.1.1" + cloud_functions_platform_interface: + dependency: transitive + description: + name: cloud_functions_platform_interface + sha256: "80ecf925d30026f134311deded508cefda9b0a4cc4fae176658a8d571997c07a" + url: "https://pub.dev" + source: hosted + version: "5.5.35" + cloud_functions_web: + dependency: transitive + description: + name: cloud_functions_web + sha256: ef28b0386cd375335d3f481297900d4e35c7f15ef7ddbc34e2f02e8b7970ddec + url: "https://pub.dev" + source: hosted + version: "4.10.0" code_builder: dependency: transitive description: @@ -254,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + contacts_service: + dependency: "direct main" + description: + name: contacts_service + sha256: f6d5ea33b31dfcdcd2e65d8abdc836502e04ddb0f66a96aa726fa9891ea9671e + url: "https://pub.dev" + source: hosted + version: "0.6.3" convert: dependency: transitive description: @@ -449,26 +481,26 @@ packages: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "80237bb8a92bb0a5e3b40de1c8dbc80254e49ac9e3907b4b47b8e95ac3dd3fad" + sha256: "48ed1841dbe617082d3b3b1db5a86dbce41503c4021d43982cfdcec598bb403e" url: "https://pub.dev" source: hosted - version: "7.4.4" + version: "7.4.5" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "9d315491a6be65ea83511cb0e078544a309c39dd54c0ee355c51dbd6d8c03cc8" + sha256: "7d4a0f8a9234eda0622aaf8344c74d57adf9eb36bf714f37df4114492d0e34bc" url: "https://pub.dev" source: hosted - version: "5.12.6" + version: "5.13.0" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" + sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" firebase_core_platform_interface: dependency: transitive description: @@ -481,10 +513,10 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" + sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25 url: "https://pub.dev" source: hosted - version: "2.17.5" + version: "2.18.0" firebase_crashlytics: dependency: "direct main" description: @@ -513,18 +545,18 @@ packages: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72" + sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459 url: "https://pub.dev" source: hosted - version: "4.5.43" + version: "4.5.44" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4" + sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678" url: "https://pub.dev" source: hosted - version: "3.8.13" + version: "3.9.0" firebase_storage: dependency: "direct main" description: @@ -537,18 +569,18 @@ packages: dependency: transitive description: name: firebase_storage_platform_interface - sha256: "3da511301b77514dee5370281923fbbc6d5725c2a0b96004c5c45415e067f234" + sha256: "65d29507abc78a179449bc41fe386fec4056afbcc0e4cc9812756e39818f0a91" url: "https://pub.dev" source: hosted - version: "5.1.28" + version: "5.1.29" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: "7ad67b1c1c46c995a6bd4f225d240fc9a5fb277fade583631ae38750ffd9be17" + sha256: c5affb63909a1809afd1376239a517c60f577c953dfa214ce8e87742d9da9805 url: "https://pub.dev" source: hosted - version: "3.9.13" + version: "3.10.0" fixnum: dependency: transitive description: @@ -789,26 +821,26 @@ packages: dependency: "direct main" description: name: image_cropper - sha256: d31be025c744ac1bf52d1f49cfdd92fd421e7e45ddadaaac0b39901f67c2a7e3 + sha256: fe37d9a129411486e0d93089b61bd326d05b89e78ad4981de54b560725bf5bd5 url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "8.0.2" image_cropper_for_web: dependency: transitive description: name: image_cropper_for_web - sha256: "6386e64908ce5d5df404e01c750a99b633dfcea88da69b3efcd3b3811d639760" + sha256: "34256c8fb7fcb233251787c876bb37271744459b593a948a2db73caa323034d0" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.0.2" image_cropper_platform_interface: dependency: transitive description: name: image_cropper_platform_interface - sha256: "39c6539571bda7ce666e0a2f450246a5d42187406eef8f486a3d64f1d9381637" + sha256: e8e9d2ca36360387aee39295ce49029362ae4df3071f23e8e71f2b81e40b7531 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.0" image_picker: dependency: "direct main" description: @@ -1217,6 +1249,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" riverpod: dependency: transitive description: @@ -1665,10 +1705,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.0" web_socket: dependency: transitive description: diff --git a/khelo/pubspec.yaml b/khelo/pubspec.yaml index 3f159d8a..e1ecee22 100644 --- a/khelo/pubspec.yaml +++ b/khelo/pubspec.yaml @@ -44,20 +44,23 @@ dependencies: # UI cupertino_icons: ^1.0.2 - image_cropper: ^7.0.3 + image_cropper: ^8.0.2 cached_network_image: ^3.3.1 fluttertoast: ^8.2.4 smooth_page_indicator: ^1.1.0 flutter_svg: ^2.0.10+1 url_launcher: ^6.3.0 qr_flutter: ^4.1.0 + permission_handler: ^11.3.1 flutter_local_notifications: ^17.1.0 + # contacts + contacts_service: ^0.6.3 + # picker canopas_country_picker: ^0.0.4 image_picker: ^1.0.7 twemoji_v2: ^0.5.3 - permission_handler: ^11.3.1 # io freezed_annotation: ^2.4.1