Skip to content

Commit

Permalink
Merge pull request #36 from baharudin-yusup/hotfix/submit_phone_numbe…
Browse files Browse the repository at this point in the history
…r_failed

Hotfix/submit phone number failed.
  • Loading branch information
baharudin-yusup authored Feb 4, 2024
2 parents ceca81b + 8c906db commit 10251b3
Show file tree
Hide file tree
Showing 25 changed files with 1,024 additions and 248 deletions.
2 changes: 1 addition & 1 deletion lib/core/errors/failures.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:equatable/equatable.dart';

import '../../data/constants/firebase_exception_code.dart';
import '../../data/constants/exception_code.dart';
import '../../domain/entities/app_permission.dart';

abstract class Failure extends Equatable {
Expand Down
11 changes: 6 additions & 5 deletions lib/core/utils/logger.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import 'dart:core' as core;
import 'dart:developer';

class Logger {
Logger._();

static void print(
Object message, {
String name = '',
core.Object message, {
core.String name = '',
}) {
return log('$message', name: name);
}

static void error(
Object error, {
required String event,
String name = '',
core.Object? error, {
required core.String event,
core.String name = '',
}) {
return log('An error occurred when $event', name: name, error: error);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ enum AppFailureCode {
autoSignInFailed,
unknown,
permissionFailure,
submitPhoneNumberNoResponse,
}
48 changes: 48 additions & 0 deletions lib/data/models/submit_phone_number_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:firebase_auth/firebase_auth.dart';

abstract class SubmitPhoneNumberStatus {
final String phoneNumber;

SubmitPhoneNumberStatus({required this.phoneNumber});
}

class SubmitPhoneNumberStatusNoResponse extends SubmitPhoneNumberStatus {
SubmitPhoneNumberStatusNoResponse(String phoneNumber)
: super(phoneNumber: phoneNumber);

@override
String toString() {
return 'Submit phone number $phoneNumber no response!';
}
}

class CodeSent extends SubmitPhoneNumberStatus {
CodeSent(String phoneNumber) : super(phoneNumber: phoneNumber);

@override
String toString() {
return 'Code sent for phone number $phoneNumber';
}
}

class SubmitPhoneNumberError extends SubmitPhoneNumberStatus {
final FirebaseAuthException? exception;

SubmitPhoneNumberError(this.exception, {required super.phoneNumber});

@override
String toString() {
return 'Submit phone number $phoneNumber error: ${exception?.message}';
}
}

class AutoSignIn extends SubmitPhoneNumberStatus {
final User user;

AutoSignIn(this.user, {required super.phoneNumber});

@override
String toString() {
return 'Auto sign in for phone number $phoneNumber success!';
}
}
93 changes: 83 additions & 10 deletions lib/data/repositories/authentication_repository_impl.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:dartz/dartz.dart';
import 'package:firebase_auth/firebase_auth.dart' show FirebaseAuthException;
import 'package:rxdart/rxdart.dart';
Expand All @@ -8,7 +10,8 @@ import '../../core/utils/logger.dart';
import '../../domain/entities/auth_status.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/authentication_repository.dart';
import '../constants/firebase_exception_code.dart';
import '../constants/exception_code.dart';
import '../models/submit_phone_number_status.dart';
import '../models/user_model.dart';
import '../sources/authentication_local_data_source.dart';
import '../sources/authentication_remote_data_source.dart';
Expand All @@ -28,7 +31,8 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
late final UserModel? currentUser;
try {
currentUser = await _remoteDatSource.currentUser();
Logger.print('(repository) getCurrentUser() value: $currentUser');
Logger.print(
'(repository) getCurrentUser() value phone number: ${currentUser?.phoneNumber}');

if (currentUser == null) {
_authorizationStatusStreamController.sink
Expand All @@ -49,21 +53,88 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
}

@override
Future<Either<Failure, User>> verifyPhoneNumber(
{required String phoneNumber}) async {
Future<Either<Failure, User>> verifyPhoneNumber({
required String phoneNumber,
}) async {
Logger.print('verifying phone number $phoneNumber started...');
try {
final model =
await _remoteDatSource.verifyPhoneNumber(phoneNumber: phoneNumber);
_authorizationStatusStreamController.sink
.add(const Right(AuthStatus.authorized));
return Right(model.toEntity());
_remoteDatSource.verifyPhoneNumber(phoneNumber: phoneNumber);

final latestValue = await _getLatestSubmitPhoneNumberStatus();

// TODO: Remove this delay
// This delay is to make sure the stream controller is closed
await Future.delayed(const Duration(milliseconds: 100));

Logger.print('latest verify phone number status: $latestValue');

if (latestValue is SubmitPhoneNumberStatusNoResponse) {
return Left(
ServerFailure(
code: AppFailureCode.submitPhoneNumberNoResponse,
createdAt: DateTime.now(),
),
);
}

if (latestValue is CodeSent) {
return Left(
ServerFailure(
code: AppFailureCode.autoSignInFailed,
createdAt: DateTime.now(),
),
);
}

if (latestValue is SubmitPhoneNumberError) {
Logger.error(latestValue.exception?.message,
event: 'verifying phone number');
return Left(
ServerFailure(
code: AppFailureCode.phoneNumberBlocked,
createdAt: DateTime.now(),
),
);
}

if (latestValue is AutoSignIn) {
return Right(latestValue.user.toEntity());
}

return Left(
ServerFailure(
createdAt: DateTime.now(),
),
);
} on AppFailureCode catch (code) {
return Left(ServerFailure(code: code, createdAt: DateTime.now()));
} on Exception catch (_) {
return Left(ServerFailure(createdAt: DateTime.now()));
}
}

Future<SubmitPhoneNumberStatus> _getLatestSubmitPhoneNumberStatus() async {
Completer<SubmitPhoneNumberStatus> completer =
Completer<SubmitPhoneNumberStatus>();

// Listener for the stream
late StreamSubscription<SubmitPhoneNumberStatus> subscription;
subscription = _remoteDatSource.streamSubmitPhoneNumberStatus.listen(
(status) {
completer.complete(status); // Fill the completer with the latest value
subscription
.cancel(); // Cancel the subscription after getting the latest value
},
onError: (error) {
completer.completeError(
error); // Fill the completer with an error if one occurs
},
cancelOnError: true, // Cancel the subscription if an error occurs
);

return completer.future;
}

@override
Future<Either<Failure, Unit>> signOut() async {
try {
Expand All @@ -76,7 +147,9 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
}

@override
Future<Either<Failure, User>> verifyOtp({required String otp}) async {
Future<Either<Failure, User>> verifyOtp({
required String otp,
}) async {
try {
final model = await _remoteDatSource.verifyOtp(otp: otp);
_authorizationStatusStreamController.sink
Expand Down
2 changes: 1 addition & 1 deletion lib/data/repositories/user_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class UserRepositoryImpl extends UserRepository {
}
try {
Logger.print(
'(repository) current user number: ${model.phoneNumber}');
'(repository) current user phone number: ${model.phoneNumber}');
return Right(model.toEntity());
} catch (error) {
Logger.error(error,
Expand Down
102 changes: 70 additions & 32 deletions lib/data/sources/authentication_remote_data_source.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:rxdart/rxdart.dart';

import '../../core/errors/exceptions.dart';
import '../../core/utils/logger.dart';
import '../constants/firebase_exception_code.dart';
import '../constants/exception_code.dart';
import '../models/submit_phone_number_status.dart';
import '../models/user_model.dart';
import '../plugins/network_plugin.dart';

abstract class AuthenticationRemoteDatSource {
Future<UserModel?> currentUser();

Future<UserModel> verifyPhoneNumber({required String phoneNumber});
Future<void> verifyPhoneNumber({required String phoneNumber});

Future<UserModel> verifyOtp({required String otp});

Future<void> resendOtp();

Future<void> signOut();

Stream<SubmitPhoneNumberStatus> get streamSubmitPhoneNumberStatus;
}

class AuthenticationRemoteDatSourceImpl
Expand All @@ -26,6 +32,9 @@ class AuthenticationRemoteDatSourceImpl
final NetworkPlugin _networkPlugin;
String _verificationId;

Timer? _verificationPhoneNumberTimer;
StreamController<SubmitPhoneNumberStatus>? _submitPhoneNumberStatusController;

AuthenticationRemoteDatSourceImpl(
this._auth, this._firestore, this._networkPlugin)
: _verificationId = '' {
Expand All @@ -50,59 +59,79 @@ class AuthenticationRemoteDatSourceImpl
}

@override
Future<UserModel> verifyPhoneNumber({required String phoneNumber}) async {
PhoneAuthCredential? autoSignInCredential;
FirebaseAuthException? firebaseAuthException;

Future<void> verifyPhoneNumber({
required String phoneNumber,
}) async {
Logger.print('Verifying phone number $phoneNumber started...');

// Remove previous verification id
_verificationId = '';
_submitPhoneNumberStatusController = BehaviorSubject();
_verificationPhoneNumberTimer =
Timer(const Duration(seconds: 10), () async {
_submitPhoneNumberStatusController?.sink
.add(SubmitPhoneNumberStatusNoResponse(phoneNumber));
await _submitPhoneNumberStatusController?.close();
});

// TODO: Remove this delay
await Future.delayed(const Duration(milliseconds: 200));

try {
await _auth.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (credential) {
autoSignInCredential = credential;
verificationCompleted: (credential) async {
_verificationPhoneNumberTimer?.cancel();
final user = (await _auth.signInWithCredential(credential)).user;
if (user == null) {
_submitPhoneNumberStatusController?.sink.add(
_verificationId.isEmpty
? SubmitPhoneNumberError(null, phoneNumber: phoneNumber)
: CodeSent(phoneNumber),
);
await _submitPhoneNumberStatusController?.close();
return;
}
final verifiedUser = await _updateInitialData(user);
if (!(_submitPhoneNumberStatusController?.isClosed ?? true)) {
_submitPhoneNumberStatusController?.sink
.add(AutoSignIn(verifiedUser, phoneNumber: phoneNumber));
await _submitPhoneNumberStatusController?.close();
}
},
verificationFailed: (error) {
firebaseAuthException = error;
verificationFailed: (error) async {
_verificationPhoneNumberTimer?.cancel();
if (!(_submitPhoneNumberStatusController?.isClosed ?? true)) {
_submitPhoneNumberStatusController?.sink
.add(SubmitPhoneNumberError(error, phoneNumber: phoneNumber));
await _submitPhoneNumberStatusController?.close();
}
},
codeSent: (verificationId, _) {
codeSent: (verificationId, _) async {
_verificationPhoneNumberTimer?.cancel();
Logger.print(
'code sent verification id for phone number $phoneNumber is $verificationId');
_verificationId = verificationId;
if (!(_submitPhoneNumberStatusController?.isClosed ?? true)) {
_submitPhoneNumberStatusController?.sink.add(CodeSent(phoneNumber));
await _submitPhoneNumberStatusController?.close();
}
},
codeAutoRetrievalTimeout: (verificationId) {
codeAutoRetrievalTimeout: (verificationId) async {
_verificationPhoneNumberTimer?.cancel();
Logger.print(
'code auto retrieval timeout verification id for phone number $phoneNumber is $verificationId');
_verificationId = verificationId;
if (!(_submitPhoneNumberStatusController?.isClosed ?? true)) {
_submitPhoneNumberStatusController?.sink.add(CodeSent(phoneNumber));
await _submitPhoneNumberStatusController?.close();
}
},
);
} catch (error) {
Logger.error(error, event: 'verifying phone number');
throw ServerException();
}

if (firebaseAuthException != null) {
Logger.error(firebaseAuthException!, event: 'verifying phone number');
throw ServerException();
}

if (autoSignInCredential == null) {
Logger.error('autoSignInCredential is null',
event: 'verifying phone number');
throw AppFailureCode.autoSignInFailed;
}

final user = (await _auth.signInWithCredential(autoSignInCredential!)).user;

if (user == null) {
Logger.error('user is null!', event: 'verifying phone number');
throw AppFailureCode.autoSignInFailed;
}

return await _updateInitialData(user);
}

@override
Expand Down Expand Up @@ -170,4 +199,13 @@ class AuthenticationRemoteDatSourceImpl
Future<void> resendOtp() async {
throw FeatureException();
}

@override
Stream<SubmitPhoneNumberStatus> get streamSubmitPhoneNumberStatus {
if (_submitPhoneNumberStatusController == null) {
// TODO: Handle this
throw ServerException();
}
return _submitPhoneNumberStatusController!.stream;
}
}
2 changes: 1 addition & 1 deletion lib/domain/entities/auth_status.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
enum AuthStatus { unauthorized, lastAuthorized, authorized }
enum AuthStatus { unknown, unauthorized, authorized }
Loading

0 comments on commit 10251b3

Please sign in to comment.