From 337eef15519fb24670d3b3dbca08429e404fc13c Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Thu, 5 Sep 2024 23:35:43 +0200 Subject: [PATCH 1/8] Add demo survey and payment data sources --- .../demo/payment_demo_data_source.dart | 99 +++++++++++++++++++ .../demo/survey_demo_data_source.dart | 47 +++++++++ .../data/datasource/payment_data_source.dart | 19 ++++ .../remote/payment_remote_data_source.dart | 77 +++++++++++++++ .../remote/survey_remote_data_source.dart | 39 ++++++++ .../data/datasource/survey_data_source.dart | 7 ++ .../data/repositories/payment_repository.dart | 66 ++++--------- .../data/repositories/survey_repository.dart | 35 +++---- 8 files changed, 319 insertions(+), 70 deletions(-) create mode 100644 recipients_app/lib/data/datasource/demo/payment_demo_data_source.dart create mode 100644 recipients_app/lib/data/datasource/demo/survey_demo_data_source.dart create mode 100644 recipients_app/lib/data/datasource/payment_data_source.dart create mode 100644 recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart create mode 100644 recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart create mode 100644 recipients_app/lib/data/datasource/survey_data_source.dart diff --git a/recipients_app/lib/data/datasource/demo/payment_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/payment_demo_data_source.dart new file mode 100644 index 000000000..853c7dca6 --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/payment_demo_data_source.dart @@ -0,0 +1,99 @@ +import "dart:math"; + +import "package:app/data/datasource/payment_data_source.dart"; +import "package:app/data/models/models.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +const String paymentCollection = "payments"; + +class PaymentDemoDataSource implements PaymentDataSource { + List payments = initData(); + + static List initData() { + final List payments = []; + + final nowDate = DateTime.now(); + final random = Random(); + + final confirmedPaymentsCount = random.nextInt(12) + 1; + final notConfirmedPaymentsCount = random.nextInt(2) + 1; + + for (int i = 0; i < confirmedPaymentsCount; i++) { + final currentDateTime = DateTime( + nowDate.year, + nowDate.month - confirmedPaymentsCount - notConfirmedPaymentsCount + i, + 15, + ); + payments.add( + SocialIncomePayment( + id: "${currentDateTime.year}-${currentDateTime.month}", + paymentAt: Timestamp.fromDate(currentDateTime), + currency: "SLE", + amount: 700, + status: PaymentStatus.confirmed, + ), + ); + } + + for (int i = 0; i < notConfirmedPaymentsCount; i++) { + final currentDateTime = DateTime( + nowDate.year, + nowDate.month - notConfirmedPaymentsCount + i, + 15, + ); + payments.add( + SocialIncomePayment( + id: "${currentDateTime.year}-${currentDateTime.month}", + paymentAt: Timestamp.fromDate(currentDateTime), + currency: "SLE", + amount: 700, + status: PaymentStatus.paid, + ), + ); + } + + payments.sort((a, b) => a.id.compareTo(b.id)); + + return payments; + } + + @override + Future> fetchPayments({ + required String recipientId, + }) async { + return payments; + } + + /// This updates the payment status to confirmed + /// and also sets lastUpdatedAt and lastUpdatedBy to the + /// current time and recipient + @override + Future confirmPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + }) async { + final updatedPayment = payment.copyWith( + status: PaymentStatus.confirmed, + updatedBy: recipient.userId, + ); + + final indexWhere = payments.indexWhere((element) => element.id == updatedPayment.id); + payments[indexWhere] = updatedPayment; + } + + @override + Future contestPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + required String contestReason, + }) async { + final updatedPayment = payment.copyWith( + status: PaymentStatus.contested, + comments: contestReason, + updatedBy: recipient.userId, + ); + + final indexWhere = payments.indexWhere((element) => element.id == updatedPayment.id); + payments[indexWhere] = updatedPayment; + } +} diff --git a/recipients_app/lib/data/datasource/demo/survey_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/survey_demo_data_source.dart new file mode 100644 index 000000000..0bbefc443 --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/survey_demo_data_source.dart @@ -0,0 +1,47 @@ +import "package:app/data/datasource/survey_data_source.dart"; +import "package:app/data/models/models.dart"; +import "package:app/data/models/survey/survey.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +class SurveyDemoDataSource implements SurveyDataSource { + final List _surveys = _generateDemoSurveys(); + + static List _generateDemoSurveys() { + final now = DateTime.now(); + final surveys = [ + Survey( + id: "onboarding", + status: SurveyServerStatus.scheduled, + dueDateAt: Timestamp.fromDate(now.subtract(const Duration(days: 10))), + ), + Survey( + id: "checkin", + status: SurveyServerStatus.scheduled, + dueDateAt: Timestamp.fromDate(now), + ), + Survey( + id: "offboarding", + status: SurveyServerStatus.scheduled, + dueDateAt: Timestamp.fromDate( + now.add(const Duration(days: 11)), + ), + ), + Survey( + id: "followup", + status: SurveyServerStatus.scheduled, + dueDateAt: Timestamp.fromDate( + now.add(const Duration(days: 16)), + ), + ), + ]; + + surveys.sort((a, b) => a.id.compareTo(b.id)); + + return surveys; + } + + @override + Future> fetchSurveys({required String recipientId}) async { + return _surveys; + } +} diff --git a/recipients_app/lib/data/datasource/payment_data_source.dart b/recipients_app/lib/data/datasource/payment_data_source.dart new file mode 100644 index 000000000..04c75cf1e --- /dev/null +++ b/recipients_app/lib/data/datasource/payment_data_source.dart @@ -0,0 +1,19 @@ +import "package:app/data/models/payment/social_income_payment.dart"; +import "package:app/data/models/recipient.dart"; + +abstract class PaymentDataSource { + Future> fetchPayments({ + required String recipientId, + }); + + Future confirmPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + }); + + Future contestPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + required String contestReason, + }); +} diff --git a/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart new file mode 100644 index 000000000..7bcfffa74 --- /dev/null +++ b/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart @@ -0,0 +1,77 @@ +import "package:app/data/datasource/payment_data_source.dart"; +import "package:app/data/models/models.dart"; +import "package:app/data/repositories/repositories.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +const String paymentCollection = "payments"; + +class PaymentRemoteDataSource implements PaymentDataSource { + final FirebaseFirestore firestore; + + const PaymentRemoteDataSource({ + required this.firestore, + }); + + @override + Future> fetchPayments({ + required String recipientId, + }) async { + final List payments = []; + + final paymentsDocs = + await firestore.collection(recipientCollection).doc(recipientId).collection(paymentCollection).get(); + + for (final paymentDoc in paymentsDocs.docs) { + final payment = SocialIncomePayment.fromJson( + paymentDoc.data(), + ); + + payments.add(payment.copyWith(id: paymentDoc.id)); + } + + payments.sort((a, b) => a.id.compareTo(b.id)); + + return payments; + } + + /// This updates the payment status to confirmed + /// and also sets lastUpdatedAt and lastUpdatedBy to the + /// current time and recipient + @override + Future confirmPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + }) async { + final updatedPayment = payment.copyWith( + status: PaymentStatus.confirmed, + updatedBy: recipient.userId, + ); + + await firestore + .collection(recipientCollection) + .doc(recipient.userId) + .collection(paymentCollection) + .doc(payment.id) + .update(updatedPayment.toJson()); + } + + @override + Future contestPayment({ + required Recipient recipient, + required SocialIncomePayment payment, + required String contestReason, + }) async { + final updatedPayment = payment.copyWith( + status: PaymentStatus.contested, + comments: contestReason, + updatedBy: recipient.userId, + ); + + await firestore + .collection(recipientCollection) + .doc(recipient.userId) + .collection(paymentCollection) + .doc(payment.id) + .update(updatedPayment.toJson()); + } +} diff --git a/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart new file mode 100644 index 000000000..8becc501b --- /dev/null +++ b/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart @@ -0,0 +1,39 @@ +import "package:app/data/datasource/survey_data_source.dart"; +import "package:app/data/models/survey/survey.dart"; +import "package:app/data/repositories/user_repository.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +const String surveyCollection = "surveys"; + +class SurveyRemoteDataSource implements SurveyDataSource { + final FirebaseFirestore firestore; + + const SurveyRemoteDataSource({ + required this.firestore, + }); + + @override + Future> fetchSurveys({ + required String recipientId, + }) async { + final surveys = []; + + final surveysDocs = await firestore + .collection(recipientCollection) + .doc(recipientId) + .collection(surveyCollection) + .get(); + + for (final surveyDoc in surveysDocs.docs) { + final survey = Survey.fromJson(surveyDoc.data()); + + surveys.add( + survey.copyWith(id: surveyDoc.id), + ); + } + + surveys.sort((a, b) => a.id.compareTo(b.id)); + + return surveys; + } +} diff --git a/recipients_app/lib/data/datasource/survey_data_source.dart b/recipients_app/lib/data/datasource/survey_data_source.dart new file mode 100644 index 000000000..ab10bbe2e --- /dev/null +++ b/recipients_app/lib/data/datasource/survey_data_source.dart @@ -0,0 +1,7 @@ +import "package:app/data/models/survey/survey.dart"; + +abstract class SurveyDataSource { + Future> fetchSurveys({ + required String recipientId, + }); +} diff --git a/recipients_app/lib/data/repositories/payment_repository.dart b/recipients_app/lib/data/repositories/payment_repository.dart index bf5b67f28..e36fea661 100644 --- a/recipients_app/lib/data/repositories/payment_repository.dart +++ b/recipients_app/lib/data/repositories/payment_repository.dart @@ -1,58 +1,37 @@ +import "package:app/data/datasource/demo/payment_demo_data_source.dart"; +import "package:app/data/datasource/payment_data_source.dart"; +import "package:app/data/datasource/remote/payment_remote_data_source.dart"; import "package:app/data/models/models.dart"; -import "package:app/data/repositories/repositories.dart"; import "package:cloud_firestore/cloud_firestore.dart"; -const String paymentCollection = "payments"; - class PaymentRepository { + late PaymentDataSource remoteDataSource = PaymentRemoteDataSource(firestore: firestore); + late PaymentDataSource demoDataSource = PaymentDemoDataSource(); + + final bool useRemoteDataSource; final FirebaseFirestore firestore; - const PaymentRepository({ + PaymentRepository({ required this.firestore, + this.useRemoteDataSource = false, }); + PaymentDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; + Future> fetchPayments({ required String recipientId, }) async { - final List payments = []; - - final paymentsDocs = await firestore - .collection(recipientCollection) - .doc(recipientId) - .collection(paymentCollection) - .get(); - - for (final paymentDoc in paymentsDocs.docs) { - final payment = SocialIncomePayment.fromJson( - paymentDoc.data(), - ); - - payments.add(payment.copyWith(id: paymentDoc.id)); - } - - payments.sort((a, b) => a.id.compareTo(b.id)); - - return payments; + return _activeDataSource.fetchPayments(recipientId: recipientId); } - /// This updates the payment status to confirmed - /// and also sets lastUpdatedAt and lastUpdatedBy to the - /// current time and recipient Future confirmPayment({ required Recipient recipient, required SocialIncomePayment payment, }) async { - final updatedPayment = payment.copyWith( - status: PaymentStatus.confirmed, - updatedBy: recipient.userId, + await _activeDataSource.confirmPayment( + recipient: recipient, + payment: payment, ); - - await firestore - .collection(recipientCollection) - .doc(recipient.userId) - .collection(paymentCollection) - .doc(payment.id) - .update(updatedPayment.toJson()); } Future contestPayment({ @@ -60,17 +39,10 @@ class PaymentRepository { required SocialIncomePayment payment, required String contestReason, }) async { - final updatedPayment = payment.copyWith( - status: PaymentStatus.contested, - comments: contestReason, - updatedBy: recipient.userId, + await _activeDataSource.contestPayment( + recipient: recipient, + payment: payment, + contestReason: contestReason, ); - - await firestore - .collection(recipientCollection) - .doc(recipient.userId) - .collection(paymentCollection) - .doc(payment.id) - .update(updatedPayment.toJson()); } } diff --git a/recipients_app/lib/data/repositories/survey_repository.dart b/recipients_app/lib/data/repositories/survey_repository.dart index 5031038f7..4ff3004dc 100644 --- a/recipients_app/lib/data/repositories/survey_repository.dart +++ b/recipients_app/lib/data/repositories/survey_repository.dart @@ -1,38 +1,27 @@ +import "package:app/data/datasource/demo/survey_demo_data_source.dart"; +import "package:app/data/datasource/remote/survey_remote_data_source.dart"; +import "package:app/data/datasource/survey_data_source.dart"; import "package:app/data/models/models.dart"; import "package:app/data/models/survey/survey.dart"; -import "package:app/data/repositories/repositories.dart"; import "package:cloud_firestore/cloud_firestore.dart"; -const String surveyCollection = "surveys"; - class SurveyRepository { + late SurveyDataSource remoteDataSource = SurveyRemoteDataSource(firestore: firestore); + late SurveyDataSource demoDataSource = SurveyDemoDataSource(); // Assuming you have a demo source + + final bool useRemoteDataSource; final FirebaseFirestore firestore; - const SurveyRepository({ + SurveyRepository({ required this.firestore, + this.useRemoteDataSource = false, }); + SurveyDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; + Future> fetchSurveys({ required String recipientId, }) async { - final surveys = []; - - final surveysDocs = await firestore - .collection(recipientCollection) - .doc(recipientId) - .collection(surveyCollection) - .get(); - - for (final surveyDoc in surveysDocs.docs) { - final survey = Survey.fromJson(surveyDoc.data()); - - surveys.add( - survey.copyWith(id: surveyDoc.id), - ); - } - - surveys.sort((a, b) => a.id.compareTo(b.id)); - - return surveys; + return _activeDataSource.fetchSurveys(recipientId: recipientId); } } From 58e608e6756fcb2e0a29cebce49d845687271c63 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Thu, 5 Sep 2024 23:41:44 +0200 Subject: [PATCH 2/8] Remove payments from user profile as not needed anymore --- recipients_app/lib/data/models/recipient.dart | 8 -------- recipients_app/lib/data/repositories/user_repository.dart | 6 ------ 2 files changed, 14 deletions(-) diff --git a/recipients_app/lib/data/models/recipient.dart b/recipients_app/lib/data/models/recipient.dart index 96bbe3641..ed054cba2 100644 --- a/recipients_app/lib/data/models/recipient.dart +++ b/recipients_app/lib/data/models/recipient.dart @@ -78,10 +78,6 @@ class Recipient extends Equatable { @JsonKey(name: "successor") final String? successorName; - // this should be got from `/recipients//payments` collection - @JsonKey(includeFromJson: false, includeToJson: false) - final List? payments; - const Recipient({ required this.userId, required this.communicationMobilePhone, @@ -102,7 +98,6 @@ class Recipient extends Equatable { this.imLinkRegular, this.nextSurvey, this.organizationRef, - this.payments = const [], this.updatedBy, this.successorName, }); @@ -129,7 +124,6 @@ class Recipient extends Equatable { imLinkRegular, nextSurvey, organizationRef, - payments, updatedBy, successorName, ]; @@ -155,7 +149,6 @@ class Recipient extends Equatable { String? imLinkRegular, Timestamp? nextSurvey, DocumentReference? organizationRef, - List? payments, String? updatedBy, String? successorName, }) { @@ -180,7 +173,6 @@ class Recipient extends Equatable { imLinkRegular: imLinkRegular ?? this.imLinkRegular, nextSurvey: nextSurvey ?? this.nextSurvey, organizationRef: organizationRef ?? this.organizationRef, - payments: payments ?? this.payments, updatedBy: updatedBy ?? this.updatedBy, successorName: successorName ?? this.successorName, ); diff --git a/recipients_app/lib/data/repositories/user_repository.dart b/recipients_app/lib/data/repositories/user_repository.dart index 102bd9cc1..36f0c44f1 100644 --- a/recipients_app/lib/data/repositories/user_repository.dart +++ b/recipients_app/lib/data/repositories/user_repository.dart @@ -1,7 +1,6 @@ import "dart:developer"; import "package:app/data/models/models.dart"; -import "package:app/data/repositories/repositories.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_auth/firebase_auth.dart"; @@ -44,12 +43,7 @@ class UserRepository { // await firestore.collection("/recipients").doc(firebaseUser.uid).get(); if (userSnapshot != null && userSnapshot.exists) { - // TODO: decide if we should keep it in user object in the app at all - final payments = - await PaymentRepository(firestore: firestore).fetchPayments(recipientId: userSnapshot.id); - return Recipient.fromMap(userSnapshot.data()).copyWith( - payments: payments, userId: userSnapshot.id, ); } else { From b429b77f808643dee4a1dddfc620284dad717239 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Fri, 6 Sep 2024 00:27:25 +0200 Subject: [PATCH 3/8] Add user demo source --- .../lib/data/datasource/demo/fake_user.dart | 149 ++++++++++++++++++ .../demo/user_demo_data_source.dart | 59 +++++++ .../remote/payment_remote_data_source.dart | 2 +- .../remote/survey_remote_data_source.dart | 2 +- .../remote/user_remote_data_source.dart | 92 +++++++++++ .../lib/data/datasource/user_data_source.dart | 18 +++ .../data/repositories/user_repository.dart | 86 +++------- 7 files changed, 344 insertions(+), 64 deletions(-) create mode 100644 recipients_app/lib/data/datasource/demo/fake_user.dart create mode 100644 recipients_app/lib/data/datasource/demo/user_demo_data_source.dart create mode 100644 recipients_app/lib/data/datasource/remote/user_remote_data_source.dart create mode 100644 recipients_app/lib/data/datasource/user_data_source.dart diff --git a/recipients_app/lib/data/datasource/demo/fake_user.dart b/recipients_app/lib/data/datasource/demo/fake_user.dart new file mode 100644 index 000000000..5f4230fe8 --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/fake_user.dart @@ -0,0 +1,149 @@ +import "package:firebase_auth/firebase_auth.dart"; + +class FakeUser implements User { + @override + String? get displayName => "demo user"; + + @override + String? get email => null; + + @override + bool get emailVerified => true; + + @override + Future getIdToken([bool forceRefresh = false]) { + throw UnimplementedError(); + } + + @override + Future delete() { + throw UnimplementedError(); + } + + @override + Future getIdTokenResult([bool forceRefresh = false]) { + throw UnimplementedError(); + } + + @override + bool get isAnonymous => throw UnimplementedError(); + + @override + Future linkWithCredential(AuthCredential credential) { + throw UnimplementedError(); + } + + @override + Future linkWithPhoneNumber(String phoneNumber, [RecaptchaVerifier? verifier]) { + throw UnimplementedError(); + } + + @override + Future linkWithPopup(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + Future linkWithProvider(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + Future linkWithRedirect(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + UserMetadata get metadata => throw UnimplementedError(); + + @override + MultiFactor get multiFactor => throw UnimplementedError(); + + @override + String? get phoneNumber => throw UnimplementedError(); + + @override + String? get photoURL => throw UnimplementedError(); + + @override + List get providerData => throw UnimplementedError(); + + @override + Future reauthenticateWithCredential(AuthCredential credential) { + throw UnimplementedError(); + } + + @override + Future reauthenticateWithPopup(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + Future reauthenticateWithProvider(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + Future reauthenticateWithRedirect(AuthProvider provider) { + throw UnimplementedError(); + } + + @override + String? get refreshToken => throw UnimplementedError(); + + @override + Future reload() { + throw UnimplementedError(); + } + + @override + Future sendEmailVerification([ActionCodeSettings? actionCodeSettings]) { + throw UnimplementedError(); + } + + @override + String? get tenantId => throw UnimplementedError(); + + @override + String get uid => throw UnimplementedError(); + + @override + Future unlink(String providerId) { + throw UnimplementedError(); + } + + @override + Future updateDisplayName(String? displayName) { + throw UnimplementedError(); + } + + @override + Future updateEmail(String newEmail) { + throw UnimplementedError(); + } + + @override + Future updatePassword(String newPassword) { + throw UnimplementedError(); + } + + @override + Future updatePhoneNumber(PhoneAuthCredential phoneCredential) { + throw UnimplementedError(); + } + + @override + Future updatePhotoURL(String? photoURL) { + throw UnimplementedError(); + } + + @override + Future updateProfile({String? displayName, String? photoURL}) { + throw UnimplementedError(); + } + + @override + Future verifyBeforeUpdateEmail(String newEmail, [ActionCodeSettings? actionCodeSettings]) { + throw UnimplementedError(); + } +} diff --git a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart new file mode 100644 index 000000000..2b84796fc --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart @@ -0,0 +1,59 @@ +import "dart:async"; + +import "package:app/data/datasource/demo/fake_user.dart"; +import "package:app/data/datasource/user_data_source.dart"; +import "package:app/data/models/models.dart"; +import "package:firebase_auth/firebase_auth.dart"; + +class UserDemoDataSource implements UserDataSource { + Recipient? _recipient = const Recipient( + userId: "demo", + mobileMoneyPhone: Phone(23271118897), + communicationMobilePhone: Phone(23271118897), + ); + final _userStreamController = StreamController(); + final _user = FakeUser(); + + @override + Stream authStateChanges() { + _userStreamController.add(_user); + return _userStreamController.stream; + } + + @override + User? get currentUser { + return _user; + } + + @override + Future fetchRecipient(User firebaseUser) async { + return _recipient; + } + + @override + Future verifyPhoneNumber({ + required String phoneNumber, + required Function(String, int?) onCodeSend, + required Function(FirebaseAuthException) onVerificationFailed, + required Function(PhoneAuthCredential) onVerificationCompleted, + required int? forceResendingToken, + }) async { + // TODO do the auth flow demo as well + } + + @override + Future signOut() async { + _userStreamController.add(null); + _userStreamController.done; + } + + @override + Future signInWithCredential(PhoneAuthCredential credentials) async { + // TODO do the auth flow demo as well + } + + @override + Future updateRecipient(Recipient recipient) async { + _recipient = recipient; + } +} diff --git a/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart index 7bcfffa74..95eb607be 100644 --- a/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart +++ b/recipients_app/lib/data/datasource/remote/payment_remote_data_source.dart @@ -1,6 +1,6 @@ import "package:app/data/datasource/payment_data_source.dart"; +import "package:app/data/datasource/remote/user_remote_data_source.dart"; import "package:app/data/models/models.dart"; -import "package:app/data/repositories/repositories.dart"; import "package:cloud_firestore/cloud_firestore.dart"; const String paymentCollection = "payments"; diff --git a/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart index 8becc501b..7b835fa7c 100644 --- a/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart +++ b/recipients_app/lib/data/datasource/remote/survey_remote_data_source.dart @@ -1,6 +1,6 @@ +import "package:app/data/datasource/remote/user_remote_data_source.dart"; import "package:app/data/datasource/survey_data_source.dart"; import "package:app/data/models/survey/survey.dart"; -import "package:app/data/repositories/user_repository.dart"; import "package:cloud_firestore/cloud_firestore.dart"; const String surveyCollection = "surveys"; diff --git a/recipients_app/lib/data/datasource/remote/user_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/user_remote_data_source.dart new file mode 100644 index 000000000..97fca0694 --- /dev/null +++ b/recipients_app/lib/data/datasource/remote/user_remote_data_source.dart @@ -0,0 +1,92 @@ +import "dart:developer"; +import "package:app/data/datasource/user_data_source.dart"; +import "package:app/data/models/recipient.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; +import "package:firebase_auth/firebase_auth.dart"; + +const String recipientCollection = "/recipients"; + +class UserRemoteDataSource implements UserDataSource { + final FirebaseFirestore firestore; + final FirebaseAuth firebaseAuth; + + const UserRemoteDataSource({ + required this.firestore, + required this.firebaseAuth, + }); + + @override + Stream authStateChanges() => firebaseAuth.authStateChanges(); + + @override + User? get currentUser => firebaseAuth.currentUser; + + + /// Fetches the user data by userId from firestore and maps it to a recipient object + /// Returns null if the user does not exist. + @override + Future fetchRecipient(User firebaseUser) async { + final phoneNumber = firebaseUser.phoneNumber ?? ""; + + final matchingUsers = await firestore + .collection(recipientCollection) + .where( + "mobile_money_phone.phone", + isEqualTo: int.parse(phoneNumber.substring(1)), + ) + .get(); + + if (matchingUsers.docs.isEmpty) { + return null; + } + + final userSnapshot = matchingUsers.docs.firstOrNull; + + // This doesnt work because user id from firebaseAuth is not related to user id from firestore + // Needs to be discussed if changes should be made or not + // final userSnapshot = + // await firestore.collection("/recipients").doc(firebaseUser.uid).get(); + + if (userSnapshot != null && userSnapshot.exists) { + return Recipient.fromMap(userSnapshot.data()).copyWith( + userId: userSnapshot.id, + ); + } else { + return null; + } + } + + @override + Future verifyPhoneNumber({ + required String phoneNumber, + required Function(String, int?) onCodeSend, + required Function(FirebaseAuthException) onVerificationFailed, + required Function(PhoneAuthCredential) onVerificationCompleted, + required int? forceResendingToken, + }) async { + await firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + forceResendingToken: forceResendingToken, + timeout: const Duration(seconds: 60), + verificationCompleted: (credential) => onVerificationCompleted(credential), + verificationFailed: (ex) => onVerificationFailed(ex), + codeSent: (verificationId, forceResendingToken) => onCodeSend(verificationId, forceResendingToken), + codeAutoRetrievalTimeout: (e) { + log("auto-retrieval timeout"); + }, + ); + } + + @override + Future signOut() => firebaseAuth.signOut(); + + @override + Future signInWithCredential(PhoneAuthCredential credentials) => firebaseAuth.signInWithCredential(credentials); + + @override + Future updateRecipient(Recipient recipient) async { + final updatedRecipient = recipient.copyWith(updatedBy: recipient.userId); + + return firestore.collection(recipientCollection).doc(recipient.userId).update(updatedRecipient.toJson()); + } +} diff --git a/recipients_app/lib/data/datasource/user_data_source.dart b/recipients_app/lib/data/datasource/user_data_source.dart new file mode 100644 index 000000000..647d8168e --- /dev/null +++ b/recipients_app/lib/data/datasource/user_data_source.dart @@ -0,0 +1,18 @@ +import "package:app/data/models/recipient.dart"; +import "package:firebase_auth/firebase_auth.dart"; + +abstract class UserDataSource { + Stream authStateChanges(); + User? get currentUser; + Future fetchRecipient(User firebaseUser); + Future verifyPhoneNumber({ + required String phoneNumber, + required Function(String, int?) onCodeSend, + required Function(FirebaseAuthException) onVerificationFailed, + required Function(PhoneAuthCredential) onVerificationCompleted, + required int? forceResendingToken, + }); + Future signOut(); + Future signInWithCredential(PhoneAuthCredential credentials); + Future updateRecipient(Recipient recipient); +} diff --git a/recipients_app/lib/data/repositories/user_repository.dart b/recipients_app/lib/data/repositories/user_repository.dart index 36f0c44f1..a93ae776e 100644 --- a/recipients_app/lib/data/repositories/user_repository.dart +++ b/recipients_app/lib/data/repositories/user_repository.dart @@ -1,55 +1,30 @@ -import "dart:developer"; - +import "package:app/data/datasource/demo/user_demo_data_source.dart"; +import "package:app/data/datasource/remote/user_remote_data_source.dart"; +import "package:app/data/datasource/user_data_source.dart"; import "package:app/data/models/models.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_auth/firebase_auth.dart"; -const String recipientCollection = "/recipients"; - class UserRepository { + late UserDataSource remoteDataSource = UserRemoteDataSource(firestore: firestore, firebaseAuth: firebaseAuth); + late UserDataSource demoDataSource = UserDemoDataSource(); + + final bool useRemoteDataSource; final FirebaseFirestore firestore; final FirebaseAuth firebaseAuth; - const UserRepository({ + UserRepository({ required this.firestore, required this.firebaseAuth, + this.useRemoteDataSource = false, }); - Stream authStateChanges() => firebaseAuth.authStateChanges(); - User? get currentUser => firebaseAuth.currentUser; - - /// Fetches the user data by userId from firestore and maps it to a recipient object - /// Returns null if the user does not exist. - Future fetchRecipient(User firebaseUser) async { - final phoneNumber = firebaseUser.phoneNumber ?? ""; - - final matchingUsers = await firestore - .collection(recipientCollection) - .where( - "mobile_money_phone.phone", - isEqualTo: int.parse(phoneNumber.substring(1)), - ) - .get(); + UserDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; - if (matchingUsers.docs.isEmpty) { - return null; - } + Stream authStateChanges() => _activeDataSource.authStateChanges(); + User? get currentUser => _activeDataSource.currentUser; - final userSnapshot = matchingUsers.docs.firstOrNull; - - // This doesnt work because user id from firebaseAuth is not related to user id from firestore - // Needs to be discussed if changes should be made or not - // final userSnapshot = - // await firestore.collection("/recipients").doc(firebaseUser.uid).get(); - - if (userSnapshot != null && userSnapshot.exists) { - return Recipient.fromMap(userSnapshot.data()).copyWith( - userId: userSnapshot.id, - ); - } else { - return null; - } - } + Future fetchRecipient(User firebaseUser) => _activeDataSource.fetchRecipient(firebaseUser); Future verifyPhoneNumber({ required String phoneNumber, @@ -57,32 +32,19 @@ class UserRepository { required Function(FirebaseAuthException) onVerificationFailed, required Function(PhoneAuthCredential) onVerificationCompleted, required int? forceResendingToken, - }) async { - await firebaseAuth.verifyPhoneNumber( - phoneNumber: phoneNumber, - forceResendingToken: forceResendingToken, - timeout: const Duration(seconds: 60), - verificationCompleted: (credential) => onVerificationCompleted(credential), - verificationFailed: (ex) => onVerificationFailed(ex), - codeSent: (verificationId, forceResendingToken) => - onCodeSend(verificationId, forceResendingToken), - codeAutoRetrievalTimeout: (e) { - log("auto-retrieval timeout"); - }, - ); - } + }) => + _activeDataSource.verifyPhoneNumber( + phoneNumber: phoneNumber, + onCodeSend: onCodeSend, + onVerificationFailed: onVerificationFailed, + onVerificationCompleted: onVerificationCompleted, + forceResendingToken: forceResendingToken, + ); - Future signOut() => firebaseAuth.signOut(); + Future signOut() => _activeDataSource.signOut(); Future signInWithCredential(PhoneAuthCredential credentials) => - firebaseAuth.signInWithCredential(credentials); - - Future updateRecipient(Recipient recipient) async { - final updatedRecipient = recipient.copyWith(updatedBy: recipient.userId); + _activeDataSource.signInWithCredential(credentials); - return firestore - .collection(recipientCollection) - .doc(recipient.userId) - .update(updatedRecipient.toJson()); - } + Future updateRecipient(Recipient recipient) => _activeDataSource.updateRecipient(recipient); } From 59768c8f8e0601db246eb4814cc01c7859602827 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Fri, 11 Oct 2024 17:27:42 +0200 Subject: [PATCH 4/8] Improve demo user source --- .../data/datasource/demo/{fake_user.dart => demo_user.dart} | 2 +- .../lib/data/datasource/demo/user_demo_data_source.dart | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename recipients_app/lib/data/datasource/demo/{fake_user.dart => demo_user.dart} (99%) diff --git a/recipients_app/lib/data/datasource/demo/fake_user.dart b/recipients_app/lib/data/datasource/demo/demo_user.dart similarity index 99% rename from recipients_app/lib/data/datasource/demo/fake_user.dart rename to recipients_app/lib/data/datasource/demo/demo_user.dart index 5f4230fe8..825f56a94 100644 --- a/recipients_app/lib/data/datasource/demo/fake_user.dart +++ b/recipients_app/lib/data/datasource/demo/demo_user.dart @@ -1,6 +1,6 @@ import "package:firebase_auth/firebase_auth.dart"; -class FakeUser implements User { +class DemoUser implements User { @override String? get displayName => "demo user"; diff --git a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart index 2b84796fc..305f0988c 100644 --- a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart +++ b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart @@ -1,6 +1,6 @@ import "dart:async"; -import "package:app/data/datasource/demo/fake_user.dart"; +import "package:app/data/datasource/demo/demo_user.dart"; import "package:app/data/datasource/user_data_source.dart"; import "package:app/data/models/models.dart"; import "package:firebase_auth/firebase_auth.dart"; @@ -8,11 +8,13 @@ import "package:firebase_auth/firebase_auth.dart"; class UserDemoDataSource implements UserDataSource { Recipient? _recipient = const Recipient( userId: "demo", + firstName: "Demo", + lastName: "SocialIncome", mobileMoneyPhone: Phone(23271118897), communicationMobilePhone: Phone(23271118897), ); final _userStreamController = StreamController(); - final _user = FakeUser(); + final _user = DemoUser(); @override Stream authStateChanges() { From 2884044d9c2ace56e64eb6bf33a1637af0b2d414 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Sat, 30 Nov 2024 21:12:13 +0100 Subject: [PATCH 5/8] Add demo manager and demo button on start screen --- .../demo/user_demo_data_source.dart | 8 +++- .../data/repositories/payment_repository.dart | 7 ++-- .../data/repositories/survey_repository.dart | 7 ++-- .../data/repositories/user_repository.dart | 39 ++++++++++++++++--- recipients_app/lib/demo_manager.dart | 26 +++++++++++++ recipients_app/lib/l10n/app_en.arb | 3 +- recipients_app/lib/l10n/app_kri.arb | 3 +- recipients_app/lib/main.dart | 3 ++ recipients_app/lib/my_app.dart | 6 +++ .../widgets/welcome/phone_input_page.dart | 21 ++++++++-- 10 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 recipients_app/lib/demo_manager.dart diff --git a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart index 305f0988c..d0377a60f 100644 --- a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart +++ b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart @@ -14,12 +14,17 @@ class UserDemoDataSource implements UserDataSource { communicationMobilePhone: Phone(23271118897), ); final _userStreamController = StreamController(); + late final _userBroadcastStreamController = _getBroadcastStream(); final _user = DemoUser(); @override Stream authStateChanges() { _userStreamController.add(_user); - return _userStreamController.stream; + return _userBroadcastStreamController; + } + + Stream _getBroadcastStream() { + return _userStreamController.stream.asBroadcastStream(); } @override @@ -46,7 +51,6 @@ class UserDemoDataSource implements UserDataSource { @override Future signOut() async { _userStreamController.add(null); - _userStreamController.done; } @override diff --git a/recipients_app/lib/data/repositories/payment_repository.dart b/recipients_app/lib/data/repositories/payment_repository.dart index e36fea661..78893d503 100644 --- a/recipients_app/lib/data/repositories/payment_repository.dart +++ b/recipients_app/lib/data/repositories/payment_repository.dart @@ -2,21 +2,22 @@ import "package:app/data/datasource/demo/payment_demo_data_source.dart"; import "package:app/data/datasource/payment_data_source.dart"; import "package:app/data/datasource/remote/payment_remote_data_source.dart"; import "package:app/data/models/models.dart"; +import "package:app/demo_manager.dart"; import "package:cloud_firestore/cloud_firestore.dart"; class PaymentRepository { late PaymentDataSource remoteDataSource = PaymentRemoteDataSource(firestore: firestore); late PaymentDataSource demoDataSource = PaymentDemoDataSource(); - final bool useRemoteDataSource; + final DemoManager demoManager; final FirebaseFirestore firestore; PaymentRepository({ required this.firestore, - this.useRemoteDataSource = false, + required this.demoManager, }); - PaymentDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; + PaymentDataSource get _activeDataSource => demoManager.isDemoEnabled ? demoDataSource : remoteDataSource; Future> fetchPayments({ required String recipientId, diff --git a/recipients_app/lib/data/repositories/survey_repository.dart b/recipients_app/lib/data/repositories/survey_repository.dart index 4ff3004dc..be76d780b 100644 --- a/recipients_app/lib/data/repositories/survey_repository.dart +++ b/recipients_app/lib/data/repositories/survey_repository.dart @@ -3,21 +3,22 @@ import "package:app/data/datasource/remote/survey_remote_data_source.dart"; import "package:app/data/datasource/survey_data_source.dart"; import "package:app/data/models/models.dart"; import "package:app/data/models/survey/survey.dart"; +import "package:app/demo_manager.dart"; import "package:cloud_firestore/cloud_firestore.dart"; class SurveyRepository { late SurveyDataSource remoteDataSource = SurveyRemoteDataSource(firestore: firestore); late SurveyDataSource demoDataSource = SurveyDemoDataSource(); // Assuming you have a demo source - final bool useRemoteDataSource; + final DemoManager demoManager; final FirebaseFirestore firestore; SurveyRepository({ required this.firestore, - this.useRemoteDataSource = false, + required this.demoManager, }); - SurveyDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; + SurveyDataSource get _activeDataSource => demoManager.isDemoEnabled ? demoDataSource : remoteDataSource; Future> fetchSurveys({ required String recipientId, diff --git a/recipients_app/lib/data/repositories/user_repository.dart b/recipients_app/lib/data/repositories/user_repository.dart index a93ae776e..ba76f58a4 100644 --- a/recipients_app/lib/data/repositories/user_repository.dart +++ b/recipients_app/lib/data/repositories/user_repository.dart @@ -1,7 +1,10 @@ +import "dart:async"; + import "package:app/data/datasource/demo/user_demo_data_source.dart"; import "package:app/data/datasource/remote/user_remote_data_source.dart"; import "package:app/data/datasource/user_data_source.dart"; import "package:app/data/models/models.dart"; +import "package:app/demo_manager.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_auth/firebase_auth.dart"; @@ -9,19 +12,41 @@ class UserRepository { late UserDataSource remoteDataSource = UserRemoteDataSource(firestore: firestore, firebaseAuth: firebaseAuth); late UserDataSource demoDataSource = UserDemoDataSource(); - final bool useRemoteDataSource; + final DemoManager demoManager; final FirebaseFirestore firestore; final FirebaseAuth firebaseAuth; UserRepository({ required this.firestore, required this.firebaseAuth, - this.useRemoteDataSource = false, + required this.demoManager, }); - UserDataSource get _activeDataSource => useRemoteDataSource ? remoteDataSource : demoDataSource; + UserDataSource get _activeDataSource => demoManager.isDemoEnabled ? demoDataSource : remoteDataSource; + + Stream authStateChanges() { + final StreamController authStateController = StreamController(); + StreamSubscription? authStateSubscription; + + authStateSubscription = _activeDataSource.authStateChanges().listen((authState) { + authStateController.add(authState); + }); + + demoManager.isDemoEnabledStream.listen((isDemoMode) { + authStateSubscription?.cancel(); + + authStateSubscription = _activeDataSource.authStateChanges().listen((authState) { + authStateController.add(authState); + }); + + authStateController.onCancel = () { + authStateSubscription?.cancel(); + }; + }); + + return authStateController.stream; + } - Stream authStateChanges() => _activeDataSource.authStateChanges(); User? get currentUser => _activeDataSource.currentUser; Future fetchRecipient(User firebaseUser) => _activeDataSource.fetchRecipient(firebaseUser); @@ -41,7 +66,11 @@ class UserRepository { forceResendingToken: forceResendingToken, ); - Future signOut() => _activeDataSource.signOut(); + Future signOut() { + return _activeDataSource.signOut().whenComplete(() { + demoManager.isDemoEnabled = false; + }); + } Future signInWithCredential(PhoneAuthCredential credentials) => _activeDataSource.signInWithCredential(credentials); diff --git a/recipients_app/lib/demo_manager.dart b/recipients_app/lib/demo_manager.dart new file mode 100644 index 000000000..24a6870ed --- /dev/null +++ b/recipients_app/lib/demo_manager.dart @@ -0,0 +1,26 @@ +import "dart:async"; + +class DemoManager { + factory DemoManager() { + return _instance; + } + + DemoManager._privateConstructor() { + _isDemoEnabled = false; + _controller.add(false); + } + + static final DemoManager _instance = DemoManager._privateConstructor(); + + bool _isDemoEnabled = false; + + final _controller = StreamController.broadcast(); + Stream get isDemoEnabledStream => _controller.stream; + + set isDemoEnabled(bool value) { + _isDemoEnabled = value; + _controller.add(_isDemoEnabled); + } + + bool get isDemoEnabled => _isDemoEnabled; +} diff --git a/recipients_app/lib/l10n/app_en.arb b/recipients_app/lib/l10n/app_en.arb index e5da358be..b04b2e276 100644 --- a/recipients_app/lib/l10n/app_en.arb +++ b/recipients_app/lib/l10n/app_en.arb @@ -276,5 +276,6 @@ "missingRecaptchaVersion": "Missing reCAPTCHA version", "invalidRecaptchaVersion": "Invalid reCAPTCHA version", "invalidReqType": "Invalid request type", - "errorEmailInvalid": "Invalid email address" + "errorEmailInvalid": "Invalid email address", + "demoCta": "Demo" } diff --git a/recipients_app/lib/l10n/app_kri.arb b/recipients_app/lib/l10n/app_kri.arb index bedc1eb70..55bf03707 100644 --- a/recipients_app/lib/l10n/app_kri.arb +++ b/recipients_app/lib/l10n/app_kri.arb @@ -176,5 +176,6 @@ "invalidVerificationCodeError": "Di spɛshal kod we wi sɛn yu nɔ kɔrɛkt. Duya chɛk di SMS kod ɛn tray bak.", "userDisabledError": "Wi dɔn lɔk yu akawnt. Rich awt to wi if yu want fɔ no mɔ.", "invalidCredentialError": " Di fon nɔmba ɔ di spɛshal kod we wi sɛn to yu in tɛm dɔn pas fɔ yuz. Duya tray bak.", - "errorEmailInvalid": "Imel nɔ kɔrɛkt" + "errorEmailInvalid": "Imel nɔ kɔrɛkt", + "demoCta": "Demo" } diff --git a/recipients_app/lib/main.dart b/recipients_app/lib/main.dart index 9447f5890..6fa31ab22 100644 --- a/recipients_app/lib/main.dart +++ b/recipients_app/lib/main.dart @@ -1,4 +1,5 @@ import "package:app/core/helpers/custom_bloc_observer.dart"; +import "package:app/demo_manager.dart"; import "package:app/my_app.dart"; import "package:cloud_firestore/cloud_firestore.dart"; import "package:firebase_app_check/firebase_app_check.dart"; @@ -31,6 +32,7 @@ Future main() async { final firestore = FirebaseFirestore.instance; final firebaseAuth = FirebaseAuth.instance; final messaging = FirebaseMessaging.instance; + final demoManager = DemoManager(); if (appFlavor == "dev") { firestore.useFirestoreEmulator("localhost", 8080); @@ -52,6 +54,7 @@ Future main() async { firebaseAuth: firebaseAuth, firestore: firestore, messaging: messaging, + demoManager: demoManager, ), ), ); diff --git a/recipients_app/lib/my_app.dart b/recipients_app/lib/my_app.dart index 278ad5ad9..11d4b17f5 100644 --- a/recipients_app/lib/my_app.dart +++ b/recipients_app/lib/my_app.dart @@ -1,6 +1,7 @@ import "package:app/core/cubits/auth/auth_cubit.dart"; import "package:app/core/cubits/settings/settings_cubit.dart"; import "package:app/data/repositories/repositories.dart"; +import "package:app/demo_manager.dart"; import "package:app/kri_intl.dart"; import "package:app/ui/configs/configs.dart"; import "package:app/view/pages/main_app_page.dart"; @@ -19,12 +20,14 @@ class MyApp extends StatelessWidget { final FirebaseAuth firebaseAuth; final FirebaseFirestore firestore; final FirebaseMessaging messaging; + final DemoManager demoManager; const MyApp({ super.key, required this.firebaseAuth, required this.firestore, required this.messaging, + required this.demoManager, }); // This widget is the root of your application. @@ -41,6 +44,7 @@ class MyApp extends StatelessWidget { create: (context) => UserRepository( firebaseAuth: firebaseAuth, firestore: firestore, + demoManager: demoManager, ), ), RepositoryProvider( @@ -49,11 +53,13 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => PaymentRepository( firestore: firestore, + demoManager: demoManager, ), ), RepositoryProvider( create: (context) => SurveyRepository( firestore: firestore, + demoManager: demoManager, ), ), RepositoryProvider( diff --git a/recipients_app/lib/view/widgets/welcome/phone_input_page.dart b/recipients_app/lib/view/widgets/welcome/phone_input_page.dart index e70c834b1..e68eee59b 100644 --- a/recipients_app/lib/view/widgets/welcome/phone_input_page.dart +++ b/recipients_app/lib/view/widgets/welcome/phone_input_page.dart @@ -1,4 +1,5 @@ import "package:app/core/cubits/signup/signup_cubit.dart"; +import "package:app/demo_manager.dart"; import "package:app/ui/buttons/buttons.dart"; import "package:app/ui/configs/app_colors.dart"; import "package:flutter/material.dart"; @@ -18,6 +19,7 @@ class _PhoneInputPageState extends State { late final RoundedLoadingButtonController btnController; late final TextEditingController phoneNumberController; late PhoneNumber number; + late DemoManager demoManager; @override void initState() { @@ -25,6 +27,7 @@ class _PhoneInputPageState extends State { btnController = RoundedLoadingButtonController(); phoneNumberController = TextEditingController(); number = PhoneNumber(isoCode: "SL"); + demoManager = DemoManager(); } @override @@ -45,6 +48,18 @@ class _PhoneInputPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Align( + alignment: Alignment.topRight, + child: SafeArea( + child: ButtonSmall( + onPressed: () { + demoManager.isDemoEnabled = true; + }, + label: localizations.demoCta, + buttonType: ButtonSmallType.outlined, + ), + ), + ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -97,10 +112,8 @@ class _PhoneInputPageState extends State { ), inputDecoration: InputDecoration( labelText: localizations.phoneNumber, - labelStyle: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: AppColors.primaryColor), + labelStyle: + Theme.of(context).textTheme.headlineMedium!.copyWith(color: AppColors.primaryColor), enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: AppColors.primaryColor), borderRadius: BorderRadius.all(Radius.circular(10)), From 8388659c1ec74492e6e5f4150b2c221d38274182 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Sun, 1 Dec 2024 00:19:01 +0100 Subject: [PATCH 6/8] Add organization demo data source --- .../lib/core/cubits/auth/auth_cubit.dart | 9 ++- .../lib/core/cubits/auth/auth_state.dart | 4 +- .../demo/no_op_document_reference.dart | 66 +++++++++++++++++++ .../demo/organization_demo_data_source.dart | 16 +++++ .../demo/user_demo_data_source.dart | 2 + .../datasource/organization_data_source.dart | 8 +++ .../organization_remote_data_source.dart | 28 ++++++++ .../repositories/organization_repository.dart | 23 ++++--- .../data/repositories/survey_repository.dart | 2 +- recipients_app/lib/my_app.dart | 1 + 10 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 recipients_app/lib/data/datasource/demo/no_op_document_reference.dart create mode 100644 recipients_app/lib/data/datasource/demo/organization_demo_data_source.dart create mode 100644 recipients_app/lib/data/datasource/organization_data_source.dart create mode 100644 recipients_app/lib/data/datasource/remote/organization_remote_data_source.dart diff --git a/recipients_app/lib/core/cubits/auth/auth_cubit.dart b/recipients_app/lib/core/cubits/auth/auth_cubit.dart index 2894c2a65..553f90bf4 100644 --- a/recipients_app/lib/core/cubits/auth/auth_cubit.dart +++ b/recipients_app/lib/core/cubits/auth/auth_cubit.dart @@ -23,12 +23,18 @@ class AuthCubit extends Cubit { if (user != null) { try { final recipient = await userRepository.fetchRecipient(user); + Organization? organization; + + if (recipient?.organizationRef != null) { + organization = await organizationRepository.fetchOrganization(recipient!.organizationRef!); + } emit( AuthState( status: AuthStatus.authenticated, firebaseUser: user, recipient: recipient, + organization: organization, ), ); } on Exception catch (ex, stackTrace) { @@ -59,8 +65,7 @@ class AuthCubit extends Cubit { Organization? organization; if (recipient?.organizationRef != null) { - organization = await organizationRepository - .fetchOrganization(recipient!.organizationRef!); + organization = await organizationRepository.fetchOrganization(recipient!.organizationRef!); } emit( diff --git a/recipients_app/lib/core/cubits/auth/auth_state.dart b/recipients_app/lib/core/cubits/auth/auth_state.dart index ae3db5a60..04e1e570f 100644 --- a/recipients_app/lib/core/cubits/auth/auth_state.dart +++ b/recipients_app/lib/core/cubits/auth/auth_state.dart @@ -26,19 +26,21 @@ class AuthState extends Equatable { }); @override - List get props => [status, firebaseUser, recipient, exception]; + List get props => [status, firebaseUser, recipient, exception, organization]; AuthState copyWith({ AuthStatus? status, User? firebaseUser, Recipient? recipient, Exception? exception, + Organization? organization, }) { return AuthState( status: status ?? this.status, firebaseUser: firebaseUser ?? this.firebaseUser, recipient: recipient ?? this.recipient, exception: exception ?? this.exception, + organization: organization ?? this.organization, ); } } diff --git a/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart b/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart new file mode 100644 index 000000000..9e3548e92 --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart @@ -0,0 +1,66 @@ +import "package:cloud_firestore/cloud_firestore.dart"; + +// ignore: subtype_of_sealed_class +class NoOpDocumentReference implements DocumentReference> { + const NoOpDocumentReference(); + + @override + CollectionReference> collection(String collectionPath) { + // TODO: implement collection + throw UnimplementedError(); + } + + @override + Future delete() { + // TODO: implement delete + throw UnimplementedError(); + } + + @override + // TODO: implement firestore + FirebaseFirestore get firestore => throw UnimplementedError(); + + @override + Future>> get([GetOptions? options]) { + // TODO: implement get + throw UnimplementedError(); + } + + @override + // TODO: implement id + String get id => throw UnimplementedError(); + + @override + // TODO: implement parent + CollectionReference> get parent => throw UnimplementedError(); + + @override + // TODO: implement path + String get path => throw UnimplementedError(); + + @override + Future set(Map data, [SetOptions? options]) { + // TODO: implement set + throw UnimplementedError(); + } + + @override + Stream>> snapshots( + {bool includeMetadataChanges = false, ListenSource source = ListenSource.defaultSource,}) { + // TODO: implement snapshots + throw UnimplementedError(); + } + + @override + Future update(Map data) { + // TODO: implement update + throw UnimplementedError(); + } + + @override + DocumentReference withConverter( + {required FromFirestore fromFirestore, required ToFirestore toFirestore,}) { + // TODO: implement withConverter + throw UnimplementedError(); + } +} diff --git a/recipients_app/lib/data/datasource/demo/organization_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/organization_demo_data_source.dart new file mode 100644 index 000000000..0ba0d6b0a --- /dev/null +++ b/recipients_app/lib/data/datasource/demo/organization_demo_data_source.dart @@ -0,0 +1,16 @@ +import "package:app/data/datasource/organization_data_source.dart"; +import "package:app/data/models/models.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +class OrganizationDemoDataSource implements OrganizationDataSource { + final Organization _organization = _generateOrganization(); + + static Organization _generateOrganization() { + return const Organization(name: "Demo organization", contactName: "Demo manager", contactNumber: "+232 123456789"); + } + + @override + Future fetchOrganization(DocumentReference organizationRef) async { + return _organization; + } +} diff --git a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart index d0377a60f..e4468a4f7 100644 --- a/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart +++ b/recipients_app/lib/data/datasource/demo/user_demo_data_source.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:app/data/datasource/demo/demo_user.dart"; +import "package:app/data/datasource/demo/no_op_document_reference.dart"; import "package:app/data/datasource/user_data_source.dart"; import "package:app/data/models/models.dart"; import "package:firebase_auth/firebase_auth.dart"; @@ -12,6 +13,7 @@ class UserDemoDataSource implements UserDataSource { lastName: "SocialIncome", mobileMoneyPhone: Phone(23271118897), communicationMobilePhone: Phone(23271118897), + organizationRef: NoOpDocumentReference(), ); final _userStreamController = StreamController(); late final _userBroadcastStreamController = _getBroadcastStream(); diff --git a/recipients_app/lib/data/datasource/organization_data_source.dart b/recipients_app/lib/data/datasource/organization_data_source.dart new file mode 100644 index 000000000..cfbb715ae --- /dev/null +++ b/recipients_app/lib/data/datasource/organization_data_source.dart @@ -0,0 +1,8 @@ +import "package:app/data/models/organization.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +abstract class OrganizationDataSource { + Future fetchOrganization( + DocumentReference organizationRef, + ); +} diff --git a/recipients_app/lib/data/datasource/remote/organization_remote_data_source.dart b/recipients_app/lib/data/datasource/remote/organization_remote_data_source.dart new file mode 100644 index 000000000..fd2d0c5b6 --- /dev/null +++ b/recipients_app/lib/data/datasource/remote/organization_remote_data_source.dart @@ -0,0 +1,28 @@ +import "package:app/data/datasource/organization_data_source.dart"; +import "package:app/data/models/organization.dart"; +import "package:cloud_firestore/cloud_firestore.dart"; + +const String surveyCollection = "surveys"; + +class OrganizationRemoteDataSource implements OrganizationDataSource { + final FirebaseFirestore firestore; + + const OrganizationRemoteDataSource({ + required this.firestore, + }); + + @override + Future fetchOrganization( + DocumentReference organizationRef, + ) async { + final organization = organizationRef.withConverter( + fromFirestore: (snapshot, _) { + final data = snapshot.data()!; + return Organization.fromJson(data); + }, + toFirestore: (organization, _) => organization.toJson(), + ); + + return (await organization.get()).data(); + } +} diff --git a/recipients_app/lib/data/repositories/organization_repository.dart b/recipients_app/lib/data/repositories/organization_repository.dart index d1aad7833..b95b94655 100644 --- a/recipients_app/lib/data/repositories/organization_repository.dart +++ b/recipients_app/lib/data/repositories/organization_repository.dart @@ -1,24 +1,27 @@ +import "package:app/data/datasource/demo/organization_demo_data_source.dart"; +import "package:app/data/datasource/organization_data_source.dart"; +import "package:app/data/datasource/remote/organization_remote_data_source.dart"; import "package:app/data/models/organization.dart"; +import "package:app/demo_manager.dart"; import "package:cloud_firestore/cloud_firestore.dart"; class OrganizationRepository { + late OrganizationDataSource remoteDataSource = OrganizationRemoteDataSource(firestore: firestore); + late OrganizationDataSource demoDataSource = OrganizationDemoDataSource(); + + final DemoManager demoManager; final FirebaseFirestore firestore; - const OrganizationRepository({ + OrganizationRepository({ required this.firestore, + required this.demoManager, }); + OrganizationDataSource get _activeDataSource => demoManager.isDemoEnabled ? demoDataSource : remoteDataSource; + Future fetchOrganization( DocumentReference organizationRef, ) async { - final organization = organizationRef.withConverter( - fromFirestore: (snapshot, _) { - final data = snapshot.data()!; - return Organization.fromJson(data); - }, - toFirestore: (organization, _) => organization.toJson(), - ); - - return (await organization.get()).data(); + return _activeDataSource.fetchOrganization(organizationRef); } } diff --git a/recipients_app/lib/data/repositories/survey_repository.dart b/recipients_app/lib/data/repositories/survey_repository.dart index be76d780b..5c6e73fbd 100644 --- a/recipients_app/lib/data/repositories/survey_repository.dart +++ b/recipients_app/lib/data/repositories/survey_repository.dart @@ -8,7 +8,7 @@ import "package:cloud_firestore/cloud_firestore.dart"; class SurveyRepository { late SurveyDataSource remoteDataSource = SurveyRemoteDataSource(firestore: firestore); - late SurveyDataSource demoDataSource = SurveyDemoDataSource(); // Assuming you have a demo source + late SurveyDataSource demoDataSource = SurveyDemoDataSource(); final DemoManager demoManager; final FirebaseFirestore firestore; diff --git a/recipients_app/lib/my_app.dart b/recipients_app/lib/my_app.dart index 11d4b17f5..b40a5c3f9 100644 --- a/recipients_app/lib/my_app.dart +++ b/recipients_app/lib/my_app.dart @@ -65,6 +65,7 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => OrganizationRepository( firestore: firestore, + demoManager: demoManager, ), ), ], From 99ebeeb0b3de0e262ac6264389a1b44db83830aa Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Thu, 2 Jan 2025 14:35:33 +0100 Subject: [PATCH 7/8] Add separate button text for create account page in demo mode --- recipients_app/lib/l10n/app_en.arb | 3 ++- recipients_app/lib/l10n/app_kri.arb | 3 ++- recipients_app/lib/view/pages/terms_and_conditions_page.dart | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/recipients_app/lib/l10n/app_en.arb b/recipients_app/lib/l10n/app_en.arb index b04b2e276..a6d415fd4 100644 --- a/recipients_app/lib/l10n/app_en.arb +++ b/recipients_app/lib/l10n/app_en.arb @@ -277,5 +277,6 @@ "invalidRecaptchaVersion": "Invalid reCAPTCHA version", "invalidReqType": "Invalid request type", "errorEmailInvalid": "Invalid email address", - "demoCta": "Demo" + "demoCta": "Demo", + "createAccountDemo": "Enter demo" } diff --git a/recipients_app/lib/l10n/app_kri.arb b/recipients_app/lib/l10n/app_kri.arb index 55bf03707..dfd9fb81c 100644 --- a/recipients_app/lib/l10n/app_kri.arb +++ b/recipients_app/lib/l10n/app_kri.arb @@ -177,5 +177,6 @@ "userDisabledError": "Wi dɔn lɔk yu akawnt. Rich awt to wi if yu want fɔ no mɔ.", "invalidCredentialError": " Di fon nɔmba ɔ di spɛshal kod we wi sɛn to yu in tɛm dɔn pas fɔ yuz. Duya tray bak.", "errorEmailInvalid": "Imel nɔ kɔrɛkt", - "demoCta": "Demo" + "demoCta": "Demo", + "createAccountDemo": "Enta di demo" } diff --git a/recipients_app/lib/view/pages/terms_and_conditions_page.dart b/recipients_app/lib/view/pages/terms_and_conditions_page.dart index d901c4a4e..f7cce8c77 100644 --- a/recipients_app/lib/view/pages/terms_and_conditions_page.dart +++ b/recipients_app/lib/view/pages/terms_and_conditions_page.dart @@ -1,5 +1,6 @@ import "package:app/core/cubits/auth/auth_cubit.dart"; import "package:app/core/helpers/flushbar_helper.dart"; +import "package:app/demo_manager.dart"; import "package:app/ui/buttons/buttons.dart"; import "package:app/ui/configs/configs.dart"; import "package:flutter/gestures.dart"; @@ -92,7 +93,7 @@ class TermsAndConditionsPage extends StatelessWidget { context.read().updateRecipient(updated); } }, - label: localizations.createAccount, + label: DemoManager().isDemoEnabled ? localizations.createAccountDemo : localizations.createAccount, ), ], ), From eb83043b504f7050f439027b78c3401ddfe3a8e4 Mon Sep 17 00:00:00 2001 From: Mikolaj Date: Mon, 6 Jan 2025 20:40:15 +0100 Subject: [PATCH 8/8] Fix review changes --- .../lib/core/cubits/auth/auth_cubit.dart | 19 +++++++++---------- .../demo/no_op_document_reference.dart | 14 +++----------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/recipients_app/lib/core/cubits/auth/auth_cubit.dart b/recipients_app/lib/core/cubits/auth/auth_cubit.dart index 553f90bf4..7fe1d6acb 100644 --- a/recipients_app/lib/core/cubits/auth/auth_cubit.dart +++ b/recipients_app/lib/core/cubits/auth/auth_cubit.dart @@ -23,11 +23,7 @@ class AuthCubit extends Cubit { if (user != null) { try { final recipient = await userRepository.fetchRecipient(user); - Organization? organization; - - if (recipient?.organizationRef != null) { - organization = await organizationRepository.fetchOrganization(recipient!.organizationRef!); - } + final Organization? organization = await _fetchOrganization(recipient); emit( AuthState( @@ -62,11 +58,7 @@ class AuthCubit extends Cubit { if (user != null) { final recipient = await userRepository.fetchRecipient(user); - Organization? organization; - - if (recipient?.organizationRef != null) { - organization = await organizationRepository.fetchOrganization(recipient!.organizationRef!); - } + final Organization? organization = await _fetchOrganization(recipient); emit( AuthState( @@ -109,4 +101,11 @@ class AuthCubit extends Cubit { await userRepository.signOut(); emit(const AuthState()); } + + Future _fetchOrganization(Recipient? recipient) async { + if (recipient?.organizationRef != null) { + return await organizationRepository.fetchOrganization(recipient!.organizationRef!); + } + return null; + } } diff --git a/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart b/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart index 9e3548e92..8006c327c 100644 --- a/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart +++ b/recipients_app/lib/data/datasource/demo/no_op_document_reference.dart @@ -1,66 +1,58 @@ import "package:cloud_firestore/cloud_firestore.dart"; +// We are using DocumentReference in repository / data source. That's why we need to get +// no-op implementation for it for demo data source. + // ignore: subtype_of_sealed_class class NoOpDocumentReference implements DocumentReference> { const NoOpDocumentReference(); @override CollectionReference> collection(String collectionPath) { - // TODO: implement collection throw UnimplementedError(); } @override Future delete() { - // TODO: implement delete throw UnimplementedError(); } @override - // TODO: implement firestore FirebaseFirestore get firestore => throw UnimplementedError(); @override Future>> get([GetOptions? options]) { - // TODO: implement get throw UnimplementedError(); } @override - // TODO: implement id String get id => throw UnimplementedError(); @override - // TODO: implement parent CollectionReference> get parent => throw UnimplementedError(); @override - // TODO: implement path String get path => throw UnimplementedError(); @override Future set(Map data, [SetOptions? options]) { - // TODO: implement set throw UnimplementedError(); } @override Stream>> snapshots( {bool includeMetadataChanges = false, ListenSource source = ListenSource.defaultSource,}) { - // TODO: implement snapshots throw UnimplementedError(); } @override Future update(Map data) { - // TODO: implement update throw UnimplementedError(); } @override DocumentReference withConverter( {required FromFirestore fromFirestore, required ToFirestore toFirestore,}) { - // TODO: implement withConverter throw UnimplementedError(); } }