Skip to content

Commit

Permalink
feat(*): Migrate to explicit exceptions: part 1
Browse files Browse the repository at this point in the history
Explicit exceptions in authentication repository.
  • Loading branch information
realth000 committed Aug 20, 2024
1 parent 742be50 commit b73cb23
Show file tree
Hide file tree
Showing 12 changed files with 384 additions and 380 deletions.
136 changes: 118 additions & 18 deletions lib/exceptions/exceptions.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,63 @@
import 'dart:async';

import 'package:dart_mappable/dart_mappable.dart';
import 'package:fpdart/fpdart.dart';

part 'exceptions.mapper.dart';

/// App wide wrapped exception
typedef SyncEither<T> = Either<AppException, T>;

/// App wide wrapped exception for future.
typedef AsyncEither<T> = TaskEither<AppException, T>;

/// App wide wrapped exception with void as value.
typedef SyncVoidEither = Either<AppException, void>;

/// App wide wrapped exception for future with void as value.
typedef AsyncVoidEither = TaskEither<AppException, void>;

/// Convenient `right(null)`
Either<L, void> rightVoid<L>() => Right<L, void>(null);

/// Functional programming operators on [AsyncEither] with generic type [T].
extension FPFutureExt<T> on AsyncEither<T> {
/// Await and handle result.
Future<void> handle(
FutureOr<void> Function(AppException e) onLeft,
FutureOr<void> Function(T v) onRight,
) async {
switch (await run()) {
case Left(:final value):
onLeft(value);
case Right(:final value):
onRight(value);
}
}
}

/// Base class for all exceptions.
@MappableClass()
sealed class AppException with AppExceptionMappable implements Exception {
AppException({
this.message,
}) : stackTrace = StackTrace.current;

/// Message to print
final String? message;

/// Collected stack trace.
late final StackTrace stackTrace;
}

/// Exception represents an error occurred in http request.
///
/// Usually the response status code is not 200.
class HttpRequestFailedException implements Exception {
@MappableClass()
final class HttpRequestFailedException extends AppException
with HttpRequestFailedExceptionMappable {
/// Constructor.
const HttpRequestFailedException(this.statusCode);
HttpRequestFailedException(this.statusCode);

/// Returned status code.
final int? statusCode;
Expand All @@ -21,25 +71,75 @@ class HttpRequestFailedException implements Exception {
/// Till now we manually set the status code to 999 to indicate this error, but
/// it should be refactored to a more proper way.
@MappableClass()
class HttpHandshakeFailedException
with HttpHandshakeFailedExceptionMappable
implements Exception {
final class HttpHandshakeFailedException extends AppException
with HttpHandshakeFailedExceptionMappable {
/// Constructor.
const HttpHandshakeFailedException(this.message);

/// Error message.
final String message;
HttpHandshakeFailedException(String message) : super(message: message);
}

/// Invalid setting's key (aka name) when accessing settings values:
/// setting values or getting values.
/// The form hash used in login progress is not found.
@MappableClass()
class InvalidSettingsKeyException
with InvalidSettingsKeyExceptionMappable
implements Exception {
/// Constructor.
const InvalidSettingsKeyException(this.message);
final class LoginFormHashNotFoundException extends AppException
with LoginFormHashNotFoundExceptionMappable {}

/// Found form hash, but it's not in the expect format.
@MappableClass()
final class LoginInvalidFormHashException extends AppException
with LoginInvalidFormHashExceptionMappable {}

/// The login result message of login progress is not found.
///
/// Indicating that we do not know whether we logged in successful or not.
@MappableClass()
final class LoginMessageNotFoundException extends AppException
with LoginMessageNotFoundExceptionMappable {}

/// The captcha user texted is incorrect.
@MappableClass()
final class LoginIncorrectCaptchaException extends AppException
with LoginIncorrectCaptchaExceptionMappable {}

/// Incorrect password or account.
@MappableClass()
final class LoginInvalidCredentialException extends AppException
with LoginInvalidCredentialExceptionMappable {}

/// Security question or its answer is incorrect.
@MappableClass()
final class LoginIncorrectSecurityQuestionException extends AppException
with LoginIncorrectSecurityQuestionExceptionMappable {}

/// Reached the limit of login attempt.
///
/// Maybe locked in 20 minutes.
@MappableClass()
final class LoginAttemptLimitException extends AppException
with LoginAttemptLimitExceptionMappable {}

/// Error message.
final String message;
/// User info not found when try to login after login seems success.
///
/// Now we should update the logged user info but this exception means we can
/// not found the logged user info.
@MappableClass()
final class LoginUserInfoNotFoundException extends AppException
with LoginUserInfoNotFoundExceptionMappable {}

/// Some other exception that not recognized.
@MappableClass()
final class LoginOtherErrorException extends AppException
with LoginOtherErrorExceptionMappable {
/// Constructor.
LoginOtherErrorException(String message) : super(message: message);
}

/// The form hash used to logout is not found.
@MappableClass()
final class LogoutFormHashNotFoundException extends AppException
with LogoutFormHashNotFoundExceptionMappable {}

/// Failed to logout.
///
/// Nearly impossible to happen.
@MappableClass()
final class LogoutFailedException extends AppException
with LogoutFailedExceptionMappable {}
72 changes: 28 additions & 44 deletions lib/features/authentication/bloc/authentication_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tsdm_client/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/repository/authentication_repository.dart';
import 'package:tsdm_client/features/authentication/repository/exceptions/exceptions.dart';
import 'package:tsdm_client/features/authentication/repository/models/models.dart';
import 'package:tsdm_client/utils/logger.dart';

Expand All @@ -11,7 +10,7 @@ part 'authentication_event.dart';
part 'authentication_state.dart';

/// Emitter
typedef AuthenticationEmitter = Emitter<AuthenticationState>;
typedef _Emitter = Emitter<AuthenticationState>;

/// Bloc the authentication, including login and logout.
///
Expand All @@ -23,60 +22,45 @@ class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState>
required AuthenticationRepository authenticationRepository,
}) : _authenticationRepository = authenticationRepository,
super(const AuthenticationState()) {
on<AuthenticationFetchLoginHashRequested>(
_onAuthenticationFetchLoginHashRequested,
on<AuthenticationEvent>(
(event, emitter) => switch (event) {
AuthenticationFetchLoginHashRequested() =>
_onFetchLoginHashRequested(emitter),
AuthenticationLoginRequested(:final userCredential) =>
_onLoginRequested(emitter, userCredential),
},
);
on<AuthenticationLoginRequested>(_onAuthenticationLoginRequested);
}

final AuthenticationRepository _authenticationRepository;

Future<void> _onAuthenticationFetchLoginHashRequested(
AuthenticationFetchLoginHashRequested event,
AuthenticationEmitter emit,
) async {
Future<void> _onFetchLoginHashRequested(_Emitter emit) async {
emit(state.copyWith(status: AuthenticationStatus.fetchingHash));
try {
final loginHash = await _authenticationRepository.fetchHash();
emit(
await _authenticationRepository.fetchHash().match(
(e) {
handle(e);
emit(state.copyWith(status: AuthenticationStatus.failure));
},
(v) => emit(
state.copyWith(
status: AuthenticationStatus.gotHash,
loginHash: loginHash,
),
);
} on HttpRequestFailedException catch (e) {
debug('failed to fetch login hash: $e');
emit(state.copyWith(status: AuthenticationStatus.failure));
} on LoginException catch (e) {
debug('failed to fetch login hash: $e');
emit(
state.copyWith(
status: AuthenticationStatus.failure,
loginException: e,
loginHash: v,
),
);
}
),
).run();
}

Future<void> _onAuthenticationLoginRequested(
AuthenticationLoginRequested event,
AuthenticationEmitter emit,
Future<void> _onLoginRequested(
_Emitter emit,
UserCredential userCredential,
) async {
emit(state.copyWith(status: AuthenticationStatus.loggingIn));
try {
await _authenticationRepository.loginWithPassword(event.userCredential);
emit(state.copyWith(status: AuthenticationStatus.success));
} on HttpRequestFailedException catch (e) {
debug('failed to login: $e');
emit(state.copyWith(status: AuthenticationStatus.failure));
} on LoginException catch (e) {
debug('failed to login: $e');
emit(
state.copyWith(
status: AuthenticationStatus.failure,
loginException: e,
),
);
}
await _authenticationRepository.loginWithPassword(userCredential).match(
(e) {
handle(e);
emit(state.copyWith(status: AuthenticationStatus.failure));
},
(_) => emit(state.copyWith(status: AuthenticationStatus.success)),
).run();
}
}
2 changes: 1 addition & 1 deletion lib/features/authentication/bloc/authentication_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ final class AuthenticationState with AuthenticationStateMappable {
final LoginHash? loginHash;

/// Exception happened in login.
final LoginException? loginException;
final AppException? loginException;
}
Loading

0 comments on commit b73cb23

Please sign in to comment.