From 255b10d5a9b276e518217324965577dee66c4bbf Mon Sep 17 00:00:00 2001 From: efrilm Date: Sat, 16 Aug 2025 17:27:57 +0700 Subject: [PATCH] feat: login api --- devtools_options.yaml | 4 + .../auth/login_form/login_form_bloc.dart | 54 + .../login_form/login_form_bloc.freezed.dart | 734 ++++++++++++ .../auth/login_form/login_form_event.dart | 9 + .../auth/login_form/login_form_state.dart | 15 + lib/common/api/api_client.dart | 1 + lib/common/constant/local_storage_key.dart | 2 + lib/common/url/api_path.dart | 3 + lib/domain/auth/auth.dart | 11 + lib/domain/auth/auth.freezed.dart | 1039 +++++++++++++++++ lib/domain/auth/entities/auth_entity.dart | 18 + lib/domain/auth/entities/user_entity.dart | 32 + lib/domain/auth/failures/auth_failure.dart | 9 + .../auth/repositories/i_auth_repository.dart | 8 + lib/env.dart | 4 +- lib/infrastructure/auth/auth_dtos.dart | 9 + .../auth/auth_dtos.freezed.dart | 642 ++++++++++ lib/infrastructure/auth/auth_dtos.g.dart | 51 + .../auth/datasources/local_data_provider.dart | 54 + .../datasources/remote_data_provider.dart | 44 + lib/infrastructure/auth/dto/auth_dto.dart | 23 + lib/infrastructure/auth/dto/user_dto.dart | 39 + .../auth/repositories/auth_repository.dart | 45 + lib/injection.config.dart | 24 + .../field/password_text_form_field.dart | 3 + .../components/field/text_form_field.dart | 3 + .../components/toast/flushbar.dart | 11 + .../pages/auth/login/login_page.dart | 166 +-- .../pages/auth/login/widgets/email_field.dart | 11 +- .../auth/login/widgets/password_field.dart | 13 +- lib/presentation/router/app_router.gr.dart | 2 +- 31 files changed, 2997 insertions(+), 86 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/application/auth/login_form/login_form_bloc.dart create mode 100644 lib/application/auth/login_form/login_form_bloc.freezed.dart create mode 100644 lib/application/auth/login_form/login_form_event.dart create mode 100644 lib/application/auth/login_form/login_form_state.dart create mode 100644 lib/common/url/api_path.dart create mode 100644 lib/domain/auth/auth.dart create mode 100644 lib/domain/auth/auth.freezed.dart create mode 100644 lib/domain/auth/entities/auth_entity.dart create mode 100644 lib/domain/auth/entities/user_entity.dart create mode 100644 lib/domain/auth/failures/auth_failure.dart create mode 100644 lib/domain/auth/repositories/i_auth_repository.dart create mode 100644 lib/infrastructure/auth/auth_dtos.dart create mode 100644 lib/infrastructure/auth/auth_dtos.freezed.dart create mode 100644 lib/infrastructure/auth/auth_dtos.g.dart create mode 100644 lib/infrastructure/auth/datasources/local_data_provider.dart create mode 100644 lib/infrastructure/auth/datasources/remote_data_provider.dart create mode 100644 lib/infrastructure/auth/dto/auth_dto.dart create mode 100644 lib/infrastructure/auth/dto/user_dto.dart create mode 100644 lib/infrastructure/auth/repositories/auth_repository.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..4dcfde9 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - shared_preferences: true \ No newline at end of file diff --git a/lib/application/auth/login_form/login_form_bloc.dart b/lib/application/auth/login_form/login_form_bloc.dart new file mode 100644 index 0000000..2c51083 --- /dev/null +++ b/lib/application/auth/login_form/login_form_bloc.dart @@ -0,0 +1,54 @@ +// ignore: depend_on_referenced_packages; +import 'package:dartz/dartz.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/auth/auth.dart'; + +part 'login_form_event.dart'; +part 'login_form_state.dart'; +part 'login_form_bloc.freezed.dart'; + +@injectable +class LoginFormBloc extends Bloc { + final IAuthRepository _repository; + LoginFormBloc(this._repository) : super(LoginFormState.initial()) { + on(_onLoginFormEvent); + } + + Future _onLoginFormEvent( + LoginFormEvent event, + Emitter emit, + ) { + return event.map( + emailChanged: (e) async { + emit(state.copyWith(email: e.email)); + }, + passwordChanged: (e) async { + emit(state.copyWith(password: e.password)); + }, + submitted: (e) async { + Either? failureOrAuth; + emit(state.copyWith(isSubmitting: true, failureOrAuthOption: none())); + + final emailValid = state.email.isNotEmpty; + final passwordValid = state.password.isNotEmpty; + + if (emailValid && passwordValid) { + failureOrAuth = await _repository.login( + email: state.email, + password: state.password, + ); + emit( + state.copyWith( + isSubmitting: false, + failureOrAuthOption: optionOf(failureOrAuth), + ), + ); + } + emit(state.copyWith(showErrorMessages: true, isSubmitting: false)); + }, + ); + } +} diff --git a/lib/application/auth/login_form/login_form_bloc.freezed.dart b/lib/application/auth/login_form/login_form_bloc.freezed.dart new file mode 100644 index 0000000..8bdc3cc --- /dev/null +++ b/lib/application/auth/login_form/login_form_bloc.freezed.dart @@ -0,0 +1,734 @@ +// 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 'login_form_bloc.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 _$LoginFormEvent { + @optionalTypeArgs + TResult when({ + required TResult Function(String email) emailChanged, + required TResult Function(String password) passwordChanged, + required TResult Function() submitted, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String email)? emailChanged, + TResult? Function(String password)? passwordChanged, + TResult? Function()? submitted, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String email)? emailChanged, + TResult Function(String password)? passwordChanged, + TResult Function()? submitted, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_EmailChanged value) emailChanged, + required TResult Function(_PasswordChanged value) passwordChanged, + required TResult Function(_Submitted value) submitted, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_EmailChanged value)? emailChanged, + TResult? Function(_PasswordChanged value)? passwordChanged, + TResult? Function(_Submitted value)? submitted, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_EmailChanged value)? emailChanged, + TResult Function(_PasswordChanged value)? passwordChanged, + TResult Function(_Submitted value)? submitted, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginFormEventCopyWith<$Res> { + factory $LoginFormEventCopyWith( + LoginFormEvent value, + $Res Function(LoginFormEvent) then, + ) = _$LoginFormEventCopyWithImpl<$Res, LoginFormEvent>; +} + +/// @nodoc +class _$LoginFormEventCopyWithImpl<$Res, $Val extends LoginFormEvent> + implements $LoginFormEventCopyWith<$Res> { + _$LoginFormEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$EmailChangedImplCopyWith<$Res> { + factory _$$EmailChangedImplCopyWith( + _$EmailChangedImpl value, + $Res Function(_$EmailChangedImpl) then, + ) = __$$EmailChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({String email}); +} + +/// @nodoc +class __$$EmailChangedImplCopyWithImpl<$Res> + extends _$LoginFormEventCopyWithImpl<$Res, _$EmailChangedImpl> + implements _$$EmailChangedImplCopyWith<$Res> { + __$$EmailChangedImplCopyWithImpl( + _$EmailChangedImpl _value, + $Res Function(_$EmailChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? email = null}) { + return _then( + _$EmailChangedImpl( + null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$EmailChangedImpl implements _EmailChanged { + const _$EmailChangedImpl(this.email); + + @override + final String email; + + @override + String toString() { + return 'LoginFormEvent.emailChanged(email: $email)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$EmailChangedImpl && + (identical(other.email, email) || other.email == email)); + } + + @override + int get hashCode => Object.hash(runtimeType, email); + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$EmailChangedImplCopyWith<_$EmailChangedImpl> get copyWith => + __$$EmailChangedImplCopyWithImpl<_$EmailChangedImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String email) emailChanged, + required TResult Function(String password) passwordChanged, + required TResult Function() submitted, + }) { + return emailChanged(email); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String email)? emailChanged, + TResult? Function(String password)? passwordChanged, + TResult? Function()? submitted, + }) { + return emailChanged?.call(email); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String email)? emailChanged, + TResult Function(String password)? passwordChanged, + TResult Function()? submitted, + required TResult orElse(), + }) { + if (emailChanged != null) { + return emailChanged(email); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_EmailChanged value) emailChanged, + required TResult Function(_PasswordChanged value) passwordChanged, + required TResult Function(_Submitted value) submitted, + }) { + return emailChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_EmailChanged value)? emailChanged, + TResult? Function(_PasswordChanged value)? passwordChanged, + TResult? Function(_Submitted value)? submitted, + }) { + return emailChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_EmailChanged value)? emailChanged, + TResult Function(_PasswordChanged value)? passwordChanged, + TResult Function(_Submitted value)? submitted, + required TResult orElse(), + }) { + if (emailChanged != null) { + return emailChanged(this); + } + return orElse(); + } +} + +abstract class _EmailChanged implements LoginFormEvent { + const factory _EmailChanged(final String email) = _$EmailChangedImpl; + + String get email; + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$EmailChangedImplCopyWith<_$EmailChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$PasswordChangedImplCopyWith<$Res> { + factory _$$PasswordChangedImplCopyWith( + _$PasswordChangedImpl value, + $Res Function(_$PasswordChangedImpl) then, + ) = __$$PasswordChangedImplCopyWithImpl<$Res>; + @useResult + $Res call({String password}); +} + +/// @nodoc +class __$$PasswordChangedImplCopyWithImpl<$Res> + extends _$LoginFormEventCopyWithImpl<$Res, _$PasswordChangedImpl> + implements _$$PasswordChangedImplCopyWith<$Res> { + __$$PasswordChangedImplCopyWithImpl( + _$PasswordChangedImpl _value, + $Res Function(_$PasswordChangedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? password = null}) { + return _then( + _$PasswordChangedImpl( + null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$PasswordChangedImpl implements _PasswordChanged { + const _$PasswordChangedImpl(this.password); + + @override + final String password; + + @override + String toString() { + return 'LoginFormEvent.passwordChanged(password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PasswordChangedImpl && + (identical(other.password, password) || + other.password == password)); + } + + @override + int get hashCode => Object.hash(runtimeType, password); + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PasswordChangedImplCopyWith<_$PasswordChangedImpl> get copyWith => + __$$PasswordChangedImplCopyWithImpl<_$PasswordChangedImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String email) emailChanged, + required TResult Function(String password) passwordChanged, + required TResult Function() submitted, + }) { + return passwordChanged(password); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String email)? emailChanged, + TResult? Function(String password)? passwordChanged, + TResult? Function()? submitted, + }) { + return passwordChanged?.call(password); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String email)? emailChanged, + TResult Function(String password)? passwordChanged, + TResult Function()? submitted, + required TResult orElse(), + }) { + if (passwordChanged != null) { + return passwordChanged(password); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_EmailChanged value) emailChanged, + required TResult Function(_PasswordChanged value) passwordChanged, + required TResult Function(_Submitted value) submitted, + }) { + return passwordChanged(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_EmailChanged value)? emailChanged, + TResult? Function(_PasswordChanged value)? passwordChanged, + TResult? Function(_Submitted value)? submitted, + }) { + return passwordChanged?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_EmailChanged value)? emailChanged, + TResult Function(_PasswordChanged value)? passwordChanged, + TResult Function(_Submitted value)? submitted, + required TResult orElse(), + }) { + if (passwordChanged != null) { + return passwordChanged(this); + } + return orElse(); + } +} + +abstract class _PasswordChanged implements LoginFormEvent { + const factory _PasswordChanged(final String password) = _$PasswordChangedImpl; + + String get password; + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PasswordChangedImplCopyWith<_$PasswordChangedImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SubmittedImplCopyWith<$Res> { + factory _$$SubmittedImplCopyWith( + _$SubmittedImpl value, + $Res Function(_$SubmittedImpl) then, + ) = __$$SubmittedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SubmittedImplCopyWithImpl<$Res> + extends _$LoginFormEventCopyWithImpl<$Res, _$SubmittedImpl> + implements _$$SubmittedImplCopyWith<$Res> { + __$$SubmittedImplCopyWithImpl( + _$SubmittedImpl _value, + $Res Function(_$SubmittedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginFormEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$SubmittedImpl implements _Submitted { + const _$SubmittedImpl(); + + @override + String toString() { + return 'LoginFormEvent.submitted()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$SubmittedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String email) emailChanged, + required TResult Function(String password) passwordChanged, + required TResult Function() submitted, + }) { + return submitted(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String email)? emailChanged, + TResult? Function(String password)? passwordChanged, + TResult? Function()? submitted, + }) { + return submitted?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String email)? emailChanged, + TResult Function(String password)? passwordChanged, + TResult Function()? submitted, + required TResult orElse(), + }) { + if (submitted != null) { + return submitted(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_EmailChanged value) emailChanged, + required TResult Function(_PasswordChanged value) passwordChanged, + required TResult Function(_Submitted value) submitted, + }) { + return submitted(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_EmailChanged value)? emailChanged, + TResult? Function(_PasswordChanged value)? passwordChanged, + TResult? Function(_Submitted value)? submitted, + }) { + return submitted?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_EmailChanged value)? emailChanged, + TResult Function(_PasswordChanged value)? passwordChanged, + TResult Function(_Submitted value)? submitted, + required TResult orElse(), + }) { + if (submitted != null) { + return submitted(this); + } + return orElse(); + } +} + +abstract class _Submitted implements LoginFormEvent { + const factory _Submitted() = _$SubmittedImpl; +} + +/// @nodoc +mixin _$LoginFormState { + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + Option> get failureOrAuthOption => + throw _privateConstructorUsedError; + bool get isSubmitting => throw _privateConstructorUsedError; + bool get showErrorMessages => throw _privateConstructorUsedError; + + /// Create a copy of LoginFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LoginFormStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginFormStateCopyWith<$Res> { + factory $LoginFormStateCopyWith( + LoginFormState value, + $Res Function(LoginFormState) then, + ) = _$LoginFormStateCopyWithImpl<$Res, LoginFormState>; + @useResult + $Res call({ + String email, + String password, + Option> failureOrAuthOption, + bool isSubmitting, + bool showErrorMessages, + }); +} + +/// @nodoc +class _$LoginFormStateCopyWithImpl<$Res, $Val extends LoginFormState> + implements $LoginFormStateCopyWith<$Res> { + _$LoginFormStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginFormState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + Object? failureOrAuthOption = null, + Object? isSubmitting = null, + Object? showErrorMessages = null, + }) { + return _then( + _value.copyWith( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + failureOrAuthOption: null == failureOrAuthOption + ? _value.failureOrAuthOption + : failureOrAuthOption // ignore: cast_nullable_to_non_nullable + as Option>, + isSubmitting: null == isSubmitting + ? _value.isSubmitting + : isSubmitting // ignore: cast_nullable_to_non_nullable + as bool, + showErrorMessages: null == showErrorMessages + ? _value.showErrorMessages + : showErrorMessages // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$LoginFormStateImplCopyWith<$Res> + implements $LoginFormStateCopyWith<$Res> { + factory _$$LoginFormStateImplCopyWith( + _$LoginFormStateImpl value, + $Res Function(_$LoginFormStateImpl) then, + ) = __$$LoginFormStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String email, + String password, + Option> failureOrAuthOption, + bool isSubmitting, + bool showErrorMessages, + }); +} + +/// @nodoc +class __$$LoginFormStateImplCopyWithImpl<$Res> + extends _$LoginFormStateCopyWithImpl<$Res, _$LoginFormStateImpl> + implements _$$LoginFormStateImplCopyWith<$Res> { + __$$LoginFormStateImplCopyWithImpl( + _$LoginFormStateImpl _value, + $Res Function(_$LoginFormStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginFormState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + Object? failureOrAuthOption = null, + Object? isSubmitting = null, + Object? showErrorMessages = null, + }) { + return _then( + _$LoginFormStateImpl( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + failureOrAuthOption: null == failureOrAuthOption + ? _value.failureOrAuthOption + : failureOrAuthOption // ignore: cast_nullable_to_non_nullable + as Option>, + isSubmitting: null == isSubmitting + ? _value.isSubmitting + : isSubmitting // ignore: cast_nullable_to_non_nullable + as bool, + showErrorMessages: null == showErrorMessages + ? _value.showErrorMessages + : showErrorMessages // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$LoginFormStateImpl implements _LoginFormState { + const _$LoginFormStateImpl({ + required this.email, + required this.password, + required this.failureOrAuthOption, + this.isSubmitting = false, + this.showErrorMessages = false, + }); + + @override + final String email; + @override + final String password; + @override + final Option> failureOrAuthOption; + @override + @JsonKey() + final bool isSubmitting; + @override + @JsonKey() + final bool showErrorMessages; + + @override + String toString() { + return 'LoginFormState(email: $email, password: $password, failureOrAuthOption: $failureOrAuthOption, isSubmitting: $isSubmitting, showErrorMessages: $showErrorMessages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginFormStateImpl && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.failureOrAuthOption, failureOrAuthOption) || + other.failureOrAuthOption == failureOrAuthOption) && + (identical(other.isSubmitting, isSubmitting) || + other.isSubmitting == isSubmitting) && + (identical(other.showErrorMessages, showErrorMessages) || + other.showErrorMessages == showErrorMessages)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + email, + password, + failureOrAuthOption, + isSubmitting, + showErrorMessages, + ); + + /// Create a copy of LoginFormState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoginFormStateImplCopyWith<_$LoginFormStateImpl> get copyWith => + __$$LoginFormStateImplCopyWithImpl<_$LoginFormStateImpl>( + this, + _$identity, + ); +} + +abstract class _LoginFormState implements LoginFormState { + const factory _LoginFormState({ + required final String email, + required final String password, + required final Option> failureOrAuthOption, + final bool isSubmitting, + final bool showErrorMessages, + }) = _$LoginFormStateImpl; + + @override + String get email; + @override + String get password; + @override + Option> get failureOrAuthOption; + @override + bool get isSubmitting; + @override + bool get showErrorMessages; + + /// Create a copy of LoginFormState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoginFormStateImplCopyWith<_$LoginFormStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/application/auth/login_form/login_form_event.dart b/lib/application/auth/login_form/login_form_event.dart new file mode 100644 index 0000000..4294a96 --- /dev/null +++ b/lib/application/auth/login_form/login_form_event.dart @@ -0,0 +1,9 @@ +part of 'login_form_bloc.dart'; + +@freezed +sealed class LoginFormEvent with _$LoginFormEvent { + const factory LoginFormEvent.emailChanged(String email) = _EmailChanged; + const factory LoginFormEvent.passwordChanged(String password) = + _PasswordChanged; + const factory LoginFormEvent.submitted() = _Submitted; +} diff --git a/lib/application/auth/login_form/login_form_state.dart b/lib/application/auth/login_form/login_form_state.dart new file mode 100644 index 0000000..c917b8b --- /dev/null +++ b/lib/application/auth/login_form/login_form_state.dart @@ -0,0 +1,15 @@ +part of 'login_form_bloc.dart'; + +@freezed +class LoginFormState with _$LoginFormState { + const factory LoginFormState({ + required String email, + required String password, + required Option> failureOrAuthOption, + @Default(false) bool isSubmitting, + @Default(false) bool showErrorMessages, + }) = _LoginFormState; + + factory LoginFormState.initial() => + LoginFormState(email: '', password: '', failureOrAuthOption: none()); +} diff --git a/lib/common/api/api_client.dart b/lib/common/api/api_client.dart index ca71634..95ab220 100644 --- a/lib/common/api/api_client.dart +++ b/lib/common/api/api_client.dart @@ -25,6 +25,7 @@ class ApiClient { ApiClient(this._dio, this._env) { _dio.options.baseUrl = _env.baseUrl; + _dio.options.validateStatus = (status) => status! < 500; _dio.options.connectTimeout = const Duration(seconds: 20); _dio.interceptors.add(BadNetworkErrorInterceptor()); _dio.interceptors.add(BadRequestErrorInterceptor()); diff --git a/lib/common/constant/local_storage_key.dart b/lib/common/constant/local_storage_key.dart index 612a7f3..e8a67f6 100644 --- a/lib/common/constant/local_storage_key.dart +++ b/lib/common/constant/local_storage_key.dart @@ -1,3 +1,5 @@ class LocalStorageKey { static const String lang = 'lang'; + static const String token = 'token'; + static const String user = 'user'; } diff --git a/lib/common/url/api_path.dart b/lib/common/url/api_path.dart new file mode 100644 index 0000000..78543f2 --- /dev/null +++ b/lib/common/url/api_path.dart @@ -0,0 +1,3 @@ +class ApiPath { + static const String login = '/api/v1/auth/login'; +} diff --git a/lib/domain/auth/auth.dart b/lib/domain/auth/auth.dart new file mode 100644 index 0000000..959c3c1 --- /dev/null +++ b/lib/domain/auth/auth.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +import '../../common/api/api_failure.dart'; + +part 'failures/auth_failure.dart'; +part 'repositories/i_auth_repository.dart'; +part 'entities/auth_entity.dart'; +part 'entities/user_entity.dart'; + +part 'auth.freezed.dart'; diff --git a/lib/domain/auth/auth.freezed.dart b/lib/domain/auth/auth.freezed.dart new file mode 100644 index 0000000..2a44bc1 --- /dev/null +++ b/lib/domain/auth/auth.freezed.dart @@ -0,0 +1,1039 @@ +// 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 'auth.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 _$AuthFailure { + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthFailureCopyWith<$Res> { + factory $AuthFailureCopyWith( + AuthFailure value, + $Res Function(AuthFailure) then, + ) = _$AuthFailureCopyWithImpl<$Res, AuthFailure>; +} + +/// @nodoc +class _$AuthFailureCopyWithImpl<$Res, $Val extends AuthFailure> + implements $AuthFailureCopyWith<$Res> { + _$AuthFailureCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$ServerErrorImplCopyWith<$Res> { + factory _$$ServerErrorImplCopyWith( + _$ServerErrorImpl value, + $Res Function(_$ServerErrorImpl) then, + ) = __$$ServerErrorImplCopyWithImpl<$Res>; + @useResult + $Res call({ApiFailure failure}); + + $ApiFailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$ServerErrorImplCopyWithImpl<$Res> + extends _$AuthFailureCopyWithImpl<$Res, _$ServerErrorImpl> + implements _$$ServerErrorImplCopyWith<$Res> { + __$$ServerErrorImplCopyWithImpl( + _$ServerErrorImpl _value, + $Res Function(_$ServerErrorImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? failure = null}) { + return _then( + _$ServerErrorImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as ApiFailure, + ), + ); + } + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ApiFailureCopyWith<$Res> get failure { + return $ApiFailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$ServerErrorImpl implements _ServerError { + const _$ServerErrorImpl(this.failure); + + @override + final ApiFailure failure; + + @override + String toString() { + return 'AuthFailure.serverError(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ServerErrorImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith => + __$$ServerErrorImplCopyWithImpl<_$ServerErrorImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return serverError(failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return serverError?.call(failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (serverError != null) { + return serverError(failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return serverError(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return serverError?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (serverError != null) { + return serverError(this); + } + return orElse(); + } +} + +abstract class _ServerError implements AuthFailure { + const factory _ServerError(final ApiFailure failure) = _$ServerErrorImpl; + + ApiFailure get failure; + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ServerErrorImplCopyWith<_$ServerErrorImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$UnexpectedErrorImplCopyWith<$Res> { + factory _$$UnexpectedErrorImplCopyWith( + _$UnexpectedErrorImpl value, + $Res Function(_$UnexpectedErrorImpl) then, + ) = __$$UnexpectedErrorImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$UnexpectedErrorImplCopyWithImpl<$Res> + extends _$AuthFailureCopyWithImpl<$Res, _$UnexpectedErrorImpl> + implements _$$UnexpectedErrorImplCopyWith<$Res> { + __$$UnexpectedErrorImplCopyWithImpl( + _$UnexpectedErrorImpl _value, + $Res Function(_$UnexpectedErrorImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$UnexpectedErrorImpl implements _UnexpectedError { + const _$UnexpectedErrorImpl(); + + @override + String toString() { + return 'AuthFailure.unexpectedError()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$UnexpectedErrorImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return unexpectedError(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return unexpectedError?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (unexpectedError != null) { + return unexpectedError(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return unexpectedError(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return unexpectedError?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (unexpectedError != null) { + return unexpectedError(this); + } + return orElse(); + } +} + +abstract class _UnexpectedError implements AuthFailure { + const factory _UnexpectedError() = _$UnexpectedErrorImpl; +} + +/// @nodoc +abstract class _$$DynamicErrorMessageImplCopyWith<$Res> { + factory _$$DynamicErrorMessageImplCopyWith( + _$DynamicErrorMessageImpl value, + $Res Function(_$DynamicErrorMessageImpl) then, + ) = __$$DynamicErrorMessageImplCopyWithImpl<$Res>; + @useResult + $Res call({String erroMessage}); +} + +/// @nodoc +class __$$DynamicErrorMessageImplCopyWithImpl<$Res> + extends _$AuthFailureCopyWithImpl<$Res, _$DynamicErrorMessageImpl> + implements _$$DynamicErrorMessageImplCopyWith<$Res> { + __$$DynamicErrorMessageImplCopyWithImpl( + _$DynamicErrorMessageImpl _value, + $Res Function(_$DynamicErrorMessageImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? erroMessage = null}) { + return _then( + _$DynamicErrorMessageImpl( + null == erroMessage + ? _value.erroMessage + : erroMessage // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$DynamicErrorMessageImpl implements _DynamicErrorMessage { + const _$DynamicErrorMessageImpl(this.erroMessage); + + @override + final String erroMessage; + + @override + String toString() { + return 'AuthFailure.dynamicErrorMessage(erroMessage: $erroMessage)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DynamicErrorMessageImpl && + (identical(other.erroMessage, erroMessage) || + other.erroMessage == erroMessage)); + } + + @override + int get hashCode => Object.hash(runtimeType, erroMessage); + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith => + __$$DynamicErrorMessageImplCopyWithImpl<_$DynamicErrorMessageImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(ApiFailure failure) serverError, + required TResult Function() unexpectedError, + required TResult Function(String erroMessage) dynamicErrorMessage, + }) { + return dynamicErrorMessage(erroMessage); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(ApiFailure failure)? serverError, + TResult? Function()? unexpectedError, + TResult? Function(String erroMessage)? dynamicErrorMessage, + }) { + return dynamicErrorMessage?.call(erroMessage); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(ApiFailure failure)? serverError, + TResult Function()? unexpectedError, + TResult Function(String erroMessage)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (dynamicErrorMessage != null) { + return dynamicErrorMessage(erroMessage); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_ServerError value) serverError, + required TResult Function(_UnexpectedError value) unexpectedError, + required TResult Function(_DynamicErrorMessage value) dynamicErrorMessage, + }) { + return dynamicErrorMessage(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_ServerError value)? serverError, + TResult? Function(_UnexpectedError value)? unexpectedError, + TResult? Function(_DynamicErrorMessage value)? dynamicErrorMessage, + }) { + return dynamicErrorMessage?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_ServerError value)? serverError, + TResult Function(_UnexpectedError value)? unexpectedError, + TResult Function(_DynamicErrorMessage value)? dynamicErrorMessage, + required TResult orElse(), + }) { + if (dynamicErrorMessage != null) { + return dynamicErrorMessage(this); + } + return orElse(); + } +} + +abstract class _DynamicErrorMessage implements AuthFailure { + const factory _DynamicErrorMessage(final String erroMessage) = + _$DynamicErrorMessageImpl; + + String get erroMessage; + + /// Create a copy of AuthFailure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DynamicErrorMessageImplCopyWith<_$DynamicErrorMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$Auth { + String get token => throw _privateConstructorUsedError; + DateTime get expiresAt => throw _privateConstructorUsedError; + User get user => throw _privateConstructorUsedError; + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthCopyWith<$Res> { + factory $AuthCopyWith(Auth value, $Res Function(Auth) then) = + _$AuthCopyWithImpl<$Res, Auth>; + @useResult + $Res call({String token, DateTime expiresAt, User user}); + + $UserCopyWith<$Res> get user; +} + +/// @nodoc +class _$AuthCopyWithImpl<$Res, $Val extends Auth> + implements $AuthCopyWith<$Res> { + _$AuthCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? token = null, + Object? expiresAt = null, + Object? user = null, + }) { + return _then( + _value.copyWith( + token: null == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String, + expiresAt: null == expiresAt + ? _value.expiresAt + : expiresAt // ignore: cast_nullable_to_non_nullable + as DateTime, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + ) + as $Val, + ); + } + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserCopyWith<$Res> get user { + return $UserCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AuthImplCopyWith<$Res> implements $AuthCopyWith<$Res> { + factory _$$AuthImplCopyWith( + _$AuthImpl value, + $Res Function(_$AuthImpl) then, + ) = __$$AuthImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String token, DateTime expiresAt, User user}); + + @override + $UserCopyWith<$Res> get user; +} + +/// @nodoc +class __$$AuthImplCopyWithImpl<$Res> + extends _$AuthCopyWithImpl<$Res, _$AuthImpl> + implements _$$AuthImplCopyWith<$Res> { + __$$AuthImplCopyWithImpl(_$AuthImpl _value, $Res Function(_$AuthImpl) _then) + : super(_value, _then); + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? token = null, + Object? expiresAt = null, + Object? user = null, + }) { + return _then( + _$AuthImpl( + token: null == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String, + expiresAt: null == expiresAt + ? _value.expiresAt + : expiresAt // ignore: cast_nullable_to_non_nullable + as DateTime, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + ), + ); + } +} + +/// @nodoc + +class _$AuthImpl extends _Auth { + const _$AuthImpl({ + required this.token, + required this.expiresAt, + required this.user, + }) : super._(); + + @override + final String token; + @override + final DateTime expiresAt; + @override + final User user; + + @override + String toString() { + return 'Auth(token: $token, expiresAt: $expiresAt, user: $user)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthImpl && + (identical(other.token, token) || other.token == token) && + (identical(other.expiresAt, expiresAt) || + other.expiresAt == expiresAt) && + (identical(other.user, user) || other.user == user)); + } + + @override + int get hashCode => Object.hash(runtimeType, token, expiresAt, user); + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthImplCopyWith<_$AuthImpl> get copyWith => + __$$AuthImplCopyWithImpl<_$AuthImpl>(this, _$identity); +} + +abstract class _Auth extends Auth { + const factory _Auth({ + required final String token, + required final DateTime expiresAt, + required final User user, + }) = _$AuthImpl; + const _Auth._() : super._(); + + @override + String get token; + @override + DateTime get expiresAt; + @override + User get user; + + /// Create a copy of Auth + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthImplCopyWith<_$AuthImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$User { + String get id => throw _privateConstructorUsedError; + String get organizationId => throw _privateConstructorUsedError; + String get outletId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get role => throw _privateConstructorUsedError; + Map get permissions => throw _privateConstructorUsedError; + bool get isActive => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCopyWith<$Res> { + factory $UserCopyWith(User value, $Res Function(User) then) = + _$UserCopyWithImpl<$Res, User>; + @useResult + $Res call({ + String id, + String organizationId, + String outletId, + String name, + String email, + String role, + Map permissions, + bool isActive, + DateTime createdAt, + DateTime updatedAt, + }); +} + +/// @nodoc +class _$UserCopyWithImpl<$Res, $Val extends User> + implements $UserCopyWith<$Res> { + _$UserCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? organizationId = null, + Object? outletId = null, + Object? name = null, + Object? email = null, + Object? role = null, + Object? permissions = null, + Object? isActive = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + outletId: null == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + permissions: null == permissions + ? _value.permissions + : permissions // ignore: cast_nullable_to_non_nullable + as Map, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> { + factory _$$UserImplCopyWith( + _$UserImpl value, + $Res Function(_$UserImpl) then, + ) = __$$UserImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String organizationId, + String outletId, + String name, + String email, + String role, + Map permissions, + bool isActive, + DateTime createdAt, + DateTime updatedAt, + }); +} + +/// @nodoc +class __$$UserImplCopyWithImpl<$Res> + extends _$UserCopyWithImpl<$Res, _$UserImpl> + implements _$$UserImplCopyWith<$Res> { + __$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then) + : super(_value, _then); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? organizationId = null, + Object? outletId = null, + Object? name = null, + Object? email = null, + Object? role = null, + Object? permissions = null, + Object? isActive = null, + Object? createdAt = null, + Object? updatedAt = null, + }) { + return _then( + _$UserImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + organizationId: null == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String, + outletId: null == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + permissions: null == permissions + ? _value._permissions + : permissions // ignore: cast_nullable_to_non_nullable + as Map, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} + +/// @nodoc + +class _$UserImpl extends _User { + const _$UserImpl({ + required this.id, + required this.organizationId, + required this.outletId, + required this.name, + required this.email, + required this.role, + required final Map permissions, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }) : _permissions = permissions, + super._(); + + @override + final String id; + @override + final String organizationId; + @override + final String outletId; + @override + final String name; + @override + final String email; + @override + final String role; + final Map _permissions; + @override + Map get permissions { + if (_permissions is EqualUnmodifiableMapView) return _permissions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_permissions); + } + + @override + final bool isActive; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + + @override + String toString() { + return 'User(id: $id, organizationId: $organizationId, outletId: $outletId, name: $name, email: $email, role: $role, permissions: $permissions, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.outletId, outletId) || + other.outletId == outletId) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.role, role) || other.role == role) && + const DeepCollectionEquality().equals( + other._permissions, + _permissions, + ) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + id, + organizationId, + outletId, + name, + email, + role, + const DeepCollectionEquality().hash(_permissions), + isActive, + createdAt, + updatedAt, + ); + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserImplCopyWith<_$UserImpl> get copyWith => + __$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity); +} + +abstract class _User extends User { + const factory _User({ + required final String id, + required final String organizationId, + required final String outletId, + required final String name, + required final String email, + required final String role, + required final Map permissions, + required final bool isActive, + required final DateTime createdAt, + required final DateTime updatedAt, + }) = _$UserImpl; + const _User._() : super._(); + + @override + String get id; + @override + String get organizationId; + @override + String get outletId; + @override + String get name; + @override + String get email; + @override + String get role; + @override + Map get permissions; + @override + bool get isActive; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + + /// Create a copy of User + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserImplCopyWith<_$UserImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/domain/auth/entities/auth_entity.dart b/lib/domain/auth/entities/auth_entity.dart new file mode 100644 index 0000000..fdce15a --- /dev/null +++ b/lib/domain/auth/entities/auth_entity.dart @@ -0,0 +1,18 @@ +part of '../auth.dart'; + +@freezed +class Auth with _$Auth { + const Auth._(); + const factory Auth({ + required String token, + required DateTime expiresAt, + required User user, + }) = _Auth; + + /// State kosong (misalnya untuk initial state di Bloc) + factory Auth.empty() => Auth( + token: '', + expiresAt: DateTime.fromMillisecondsSinceEpoch(0), + user: User.empty(), + ); +} diff --git a/lib/domain/auth/entities/user_entity.dart b/lib/domain/auth/entities/user_entity.dart new file mode 100644 index 0000000..822ca23 --- /dev/null +++ b/lib/domain/auth/entities/user_entity.dart @@ -0,0 +1,32 @@ +part of '../auth.dart'; + +@freezed +class User with _$User { + const User._(); + + const factory User({ + required String id, + required String organizationId, + required String outletId, + required String name, + required String email, + required String role, + required Map permissions, + required bool isActive, + required DateTime createdAt, + required DateTime updatedAt, + }) = _User; + + factory User.empty() => User( + id: '', + organizationId: '', + outletId: '', + name: '', + email: '', + role: '', + permissions: {}, + isActive: false, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); +} diff --git a/lib/domain/auth/failures/auth_failure.dart b/lib/domain/auth/failures/auth_failure.dart new file mode 100644 index 0000000..bf304fd --- /dev/null +++ b/lib/domain/auth/failures/auth_failure.dart @@ -0,0 +1,9 @@ +part of '../auth.dart'; + +@freezed +sealed class AuthFailure with _$AuthFailure { + const factory AuthFailure.serverError(ApiFailure failure) = _ServerError; + const factory AuthFailure.unexpectedError() = _UnexpectedError; + const factory AuthFailure.dynamicErrorMessage(String erroMessage) = + _DynamicErrorMessage; +} diff --git a/lib/domain/auth/repositories/i_auth_repository.dart b/lib/domain/auth/repositories/i_auth_repository.dart new file mode 100644 index 0000000..2cdc9f0 --- /dev/null +++ b/lib/domain/auth/repositories/i_auth_repository.dart @@ -0,0 +1,8 @@ +part of '../auth.dart'; + +abstract class IAuthRepository { + Future> login({ + required String email, + required String password, + }); +} diff --git a/lib/env.dart b/lib/env.dart index de21eb6..5a347a5 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -9,12 +9,12 @@ abstract class Env { @dev class DevEnv implements Env { @override - String get baseUrl => ''; // example value + String get baseUrl => 'https://enaklo-pos-be.altru.id'; // example value } @Injectable(as: Env) @prod class ProdEnv implements Env { @override - String get baseUrl => ''; + String get baseUrl => 'https://enaklo-pos-be.altru.id'; } diff --git a/lib/infrastructure/auth/auth_dtos.dart b/lib/infrastructure/auth/auth_dtos.dart new file mode 100644 index 0000000..69e8179 --- /dev/null +++ b/lib/infrastructure/auth/auth_dtos.dart @@ -0,0 +1,9 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../domain/auth/auth.dart'; + +part 'auth_dtos.freezed.dart'; +part 'auth_dtos.g.dart'; + +part 'dto/auth_dto.dart'; +part 'dto/user_dto.dart'; diff --git a/lib/infrastructure/auth/auth_dtos.freezed.dart b/lib/infrastructure/auth/auth_dtos.freezed.dart new file mode 100644 index 0000000..2de3110 --- /dev/null +++ b/lib/infrastructure/auth/auth_dtos.freezed.dart @@ -0,0 +1,642 @@ +// 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 'auth_dtos.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', +); + +AuthDto _$AuthDtoFromJson(Map json) { + return _AuthDto.fromJson(json); +} + +/// @nodoc +mixin _$AuthDto { + @JsonKey(name: 'token') + String? get token => throw _privateConstructorUsedError; + @JsonKey(name: 'expires_at') + String? get expiresAt => throw _privateConstructorUsedError; + @JsonKey(name: 'user') + UserDto? get user => throw _privateConstructorUsedError; + + /// Serializes this AuthDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthDtoCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthDtoCopyWith<$Res> { + factory $AuthDtoCopyWith(AuthDto value, $Res Function(AuthDto) then) = + _$AuthDtoCopyWithImpl<$Res, AuthDto>; + @useResult + $Res call({ + @JsonKey(name: 'token') String? token, + @JsonKey(name: 'expires_at') String? expiresAt, + @JsonKey(name: 'user') UserDto? user, + }); + + $UserDtoCopyWith<$Res>? get user; +} + +/// @nodoc +class _$AuthDtoCopyWithImpl<$Res, $Val extends AuthDto> + implements $AuthDtoCopyWith<$Res> { + _$AuthDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? token = freezed, + Object? expiresAt = freezed, + Object? user = freezed, + }) { + return _then( + _value.copyWith( + token: freezed == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String?, + expiresAt: freezed == expiresAt + ? _value.expiresAt + : expiresAt // ignore: cast_nullable_to_non_nullable + as String?, + user: freezed == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserDto?, + ) + as $Val, + ); + } + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserDtoCopyWith<$Res>? get user { + if (_value.user == null) { + return null; + } + + return $UserDtoCopyWith<$Res>(_value.user!, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AuthDtoImplCopyWith<$Res> implements $AuthDtoCopyWith<$Res> { + factory _$$AuthDtoImplCopyWith( + _$AuthDtoImpl value, + $Res Function(_$AuthDtoImpl) then, + ) = __$$AuthDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'token') String? token, + @JsonKey(name: 'expires_at') String? expiresAt, + @JsonKey(name: 'user') UserDto? user, + }); + + @override + $UserDtoCopyWith<$Res>? get user; +} + +/// @nodoc +class __$$AuthDtoImplCopyWithImpl<$Res> + extends _$AuthDtoCopyWithImpl<$Res, _$AuthDtoImpl> + implements _$$AuthDtoImplCopyWith<$Res> { + __$$AuthDtoImplCopyWithImpl( + _$AuthDtoImpl _value, + $Res Function(_$AuthDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? token = freezed, + Object? expiresAt = freezed, + Object? user = freezed, + }) { + return _then( + _$AuthDtoImpl( + token: freezed == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String?, + expiresAt: freezed == expiresAt + ? _value.expiresAt + : expiresAt // ignore: cast_nullable_to_non_nullable + as String?, + user: freezed == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserDto?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthDtoImpl extends _AuthDto { + const _$AuthDtoImpl({ + @JsonKey(name: 'token') this.token, + @JsonKey(name: 'expires_at') this.expiresAt, + @JsonKey(name: 'user') this.user, + }) : super._(); + + factory _$AuthDtoImpl.fromJson(Map json) => + _$$AuthDtoImplFromJson(json); + + @override + @JsonKey(name: 'token') + final String? token; + @override + @JsonKey(name: 'expires_at') + final String? expiresAt; + @override + @JsonKey(name: 'user') + final UserDto? user; + + @override + String toString() { + return 'AuthDto(token: $token, expiresAt: $expiresAt, user: $user)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthDtoImpl && + (identical(other.token, token) || other.token == token) && + (identical(other.expiresAt, expiresAt) || + other.expiresAt == expiresAt) && + (identical(other.user, user) || other.user == user)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, token, expiresAt, user); + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthDtoImplCopyWith<_$AuthDtoImpl> get copyWith => + __$$AuthDtoImplCopyWithImpl<_$AuthDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuthDtoImplToJson(this); + } +} + +abstract class _AuthDto extends AuthDto { + const factory _AuthDto({ + @JsonKey(name: 'token') final String? token, + @JsonKey(name: 'expires_at') final String? expiresAt, + @JsonKey(name: 'user') final UserDto? user, + }) = _$AuthDtoImpl; + const _AuthDto._() : super._(); + + factory _AuthDto.fromJson(Map json) = _$AuthDtoImpl.fromJson; + + @override + @JsonKey(name: 'token') + String? get token; + @override + @JsonKey(name: 'expires_at') + String? get expiresAt; + @override + @JsonKey(name: 'user') + UserDto? get user; + + /// Create a copy of AuthDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthDtoImplCopyWith<_$AuthDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UserDto _$UserDtoFromJson(Map json) { + return _UserDto.fromJson(json); +} + +/// @nodoc +mixin _$UserDto { + @JsonKey(name: 'id') + String? get id => throw _privateConstructorUsedError; + @JsonKey(name: 'organization_id') + String? get organizationId => throw _privateConstructorUsedError; + @JsonKey(name: 'outlet_id') + String? get outletId => throw _privateConstructorUsedError; + @JsonKey(name: 'name') + String? get name => throw _privateConstructorUsedError; + @JsonKey(name: 'email') + String? get email => throw _privateConstructorUsedError; + @JsonKey(name: 'role') + String? get role => throw _privateConstructorUsedError; + @JsonKey(name: 'permissions') + Map? get permissions => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool? get isActive => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') + String? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + String? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this UserDto to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserDtoCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserDtoCopyWith<$Res> { + factory $UserDtoCopyWith(UserDto value, $Res Function(UserDto) then) = + _$UserDtoCopyWithImpl<$Res, UserDto>; + @useResult + $Res call({ + @JsonKey(name: 'id') String? id, + @JsonKey(name: 'organization_id') String? organizationId, + @JsonKey(name: 'outlet_id') String? outletId, + @JsonKey(name: 'name') String? name, + @JsonKey(name: 'email') String? email, + @JsonKey(name: 'role') String? role, + @JsonKey(name: 'permissions') Map? permissions, + @JsonKey(name: 'is_active') bool? isActive, + @JsonKey(name: 'created_at') String? createdAt, + @JsonKey(name: 'updated_at') String? updatedAt, + }); +} + +/// @nodoc +class _$UserDtoCopyWithImpl<$Res, $Val extends UserDto> + implements $UserDtoCopyWith<$Res> { + _$UserDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? organizationId = freezed, + Object? outletId = freezed, + Object? name = freezed, + Object? email = freezed, + Object? role = freezed, + Object? permissions = freezed, + Object? isActive = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + outletId: freezed == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + role: freezed == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String?, + permissions: freezed == permissions + ? _value.permissions + : permissions // ignore: cast_nullable_to_non_nullable + as Map?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserDtoImplCopyWith<$Res> implements $UserDtoCopyWith<$Res> { + factory _$$UserDtoImplCopyWith( + _$UserDtoImpl value, + $Res Function(_$UserDtoImpl) then, + ) = __$$UserDtoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'id') String? id, + @JsonKey(name: 'organization_id') String? organizationId, + @JsonKey(name: 'outlet_id') String? outletId, + @JsonKey(name: 'name') String? name, + @JsonKey(name: 'email') String? email, + @JsonKey(name: 'role') String? role, + @JsonKey(name: 'permissions') Map? permissions, + @JsonKey(name: 'is_active') bool? isActive, + @JsonKey(name: 'created_at') String? createdAt, + @JsonKey(name: 'updated_at') String? updatedAt, + }); +} + +/// @nodoc +class __$$UserDtoImplCopyWithImpl<$Res> + extends _$UserDtoCopyWithImpl<$Res, _$UserDtoImpl> + implements _$$UserDtoImplCopyWith<$Res> { + __$$UserDtoImplCopyWithImpl( + _$UserDtoImpl _value, + $Res Function(_$UserDtoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = freezed, + Object? organizationId = freezed, + Object? outletId = freezed, + Object? name = freezed, + Object? email = freezed, + Object? role = freezed, + Object? permissions = freezed, + Object? isActive = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$UserDtoImpl( + id: freezed == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String?, + organizationId: freezed == organizationId + ? _value.organizationId + : organizationId // ignore: cast_nullable_to_non_nullable + as String?, + outletId: freezed == outletId + ? _value.outletId + : outletId // ignore: cast_nullable_to_non_nullable + as String?, + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + role: freezed == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String?, + permissions: freezed == permissions + ? _value._permissions + : permissions // ignore: cast_nullable_to_non_nullable + as Map?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as String?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserDtoImpl extends _UserDto { + const _$UserDtoImpl({ + @JsonKey(name: 'id') this.id, + @JsonKey(name: 'organization_id') this.organizationId, + @JsonKey(name: 'outlet_id') this.outletId, + @JsonKey(name: 'name') this.name, + @JsonKey(name: 'email') this.email, + @JsonKey(name: 'role') this.role, + @JsonKey(name: 'permissions') final Map? permissions, + @JsonKey(name: 'is_active') this.isActive, + @JsonKey(name: 'created_at') this.createdAt, + @JsonKey(name: 'updated_at') this.updatedAt, + }) : _permissions = permissions, + super._(); + + factory _$UserDtoImpl.fromJson(Map json) => + _$$UserDtoImplFromJson(json); + + @override + @JsonKey(name: 'id') + final String? id; + @override + @JsonKey(name: 'organization_id') + final String? organizationId; + @override + @JsonKey(name: 'outlet_id') + final String? outletId; + @override + @JsonKey(name: 'name') + final String? name; + @override + @JsonKey(name: 'email') + final String? email; + @override + @JsonKey(name: 'role') + final String? role; + final Map? _permissions; + @override + @JsonKey(name: 'permissions') + Map? get permissions { + final value = _permissions; + if (value == null) return null; + if (_permissions is EqualUnmodifiableMapView) return _permissions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + @JsonKey(name: 'is_active') + final bool? isActive; + @override + @JsonKey(name: 'created_at') + final String? createdAt; + @override + @JsonKey(name: 'updated_at') + final String? updatedAt; + + @override + String toString() { + return 'UserDto(id: $id, organizationId: $organizationId, outletId: $outletId, name: $name, email: $email, role: $role, permissions: $permissions, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserDtoImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.organizationId, organizationId) || + other.organizationId == organizationId) && + (identical(other.outletId, outletId) || + other.outletId == outletId) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.role, role) || other.role == role) && + const DeepCollectionEquality().equals( + other._permissions, + _permissions, + ) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + organizationId, + outletId, + name, + email, + role, + const DeepCollectionEquality().hash(_permissions), + isActive, + createdAt, + updatedAt, + ); + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserDtoImplCopyWith<_$UserDtoImpl> get copyWith => + __$$UserDtoImplCopyWithImpl<_$UserDtoImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserDtoImplToJson(this); + } +} + +abstract class _UserDto extends UserDto { + const factory _UserDto({ + @JsonKey(name: 'id') final String? id, + @JsonKey(name: 'organization_id') final String? organizationId, + @JsonKey(name: 'outlet_id') final String? outletId, + @JsonKey(name: 'name') final String? name, + @JsonKey(name: 'email') final String? email, + @JsonKey(name: 'role') final String? role, + @JsonKey(name: 'permissions') final Map? permissions, + @JsonKey(name: 'is_active') final bool? isActive, + @JsonKey(name: 'created_at') final String? createdAt, + @JsonKey(name: 'updated_at') final String? updatedAt, + }) = _$UserDtoImpl; + const _UserDto._() : super._(); + + factory _UserDto.fromJson(Map json) = _$UserDtoImpl.fromJson; + + @override + @JsonKey(name: 'id') + String? get id; + @override + @JsonKey(name: 'organization_id') + String? get organizationId; + @override + @JsonKey(name: 'outlet_id') + String? get outletId; + @override + @JsonKey(name: 'name') + String? get name; + @override + @JsonKey(name: 'email') + String? get email; + @override + @JsonKey(name: 'role') + String? get role; + @override + @JsonKey(name: 'permissions') + Map? get permissions; + @override + @JsonKey(name: 'is_active') + bool? get isActive; + @override + @JsonKey(name: 'created_at') + String? get createdAt; + @override + @JsonKey(name: 'updated_at') + String? get updatedAt; + + /// Create a copy of UserDto + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserDtoImplCopyWith<_$UserDtoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/infrastructure/auth/auth_dtos.g.dart b/lib/infrastructure/auth/auth_dtos.g.dart new file mode 100644 index 0000000..795ecd9 --- /dev/null +++ b/lib/infrastructure/auth/auth_dtos.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_dtos.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AuthDtoImpl _$$AuthDtoImplFromJson(Map json) => + _$AuthDtoImpl( + token: json['token'] as String?, + expiresAt: json['expires_at'] as String?, + user: json['user'] == null + ? null + : UserDto.fromJson(json['user'] as Map), + ); + +Map _$$AuthDtoImplToJson(_$AuthDtoImpl instance) => + { + 'token': instance.token, + 'expires_at': instance.expiresAt, + 'user': instance.user, + }; + +_$UserDtoImpl _$$UserDtoImplFromJson(Map json) => + _$UserDtoImpl( + id: json['id'] as String?, + organizationId: json['organization_id'] as String?, + outletId: json['outlet_id'] as String?, + name: json['name'] as String?, + email: json['email'] as String?, + role: json['role'] as String?, + permissions: json['permissions'] as Map?, + isActive: json['is_active'] as bool?, + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, + ); + +Map _$$UserDtoImplToJson(_$UserDtoImpl instance) => + { + 'id': instance.id, + 'organization_id': instance.organizationId, + 'outlet_id': instance.outletId, + 'name': instance.name, + 'email': instance.email, + 'role': instance.role, + 'permissions': instance.permissions, + 'is_active': instance.isActive, + 'created_at': instance.createdAt, + 'updated_at': instance.updatedAt, + }; diff --git a/lib/infrastructure/auth/datasources/local_data_provider.dart b/lib/infrastructure/auth/datasources/local_data_provider.dart new file mode 100644 index 0000000..0b146a6 --- /dev/null +++ b/lib/infrastructure/auth/datasources/local_data_provider.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../common/constant/local_storage_key.dart'; +import '../../../domain/auth/auth.dart'; +import '../auth_dtos.dart'; + +@injectable +class AuthLocalDataProvider { + final SharedPreferences _sharedPreferences; + + AuthLocalDataProvider(this._sharedPreferences); + + Future saveToken(String token) async { + await _sharedPreferences.setString(LocalStorageKey.token, token); + } + + Future getToken() async { + return _sharedPreferences.getString(LocalStorageKey.token); + } + + Future deleteToken() async { + await _sharedPreferences.remove(LocalStorageKey.token); + } + + Future hasToken() async { + return _sharedPreferences.containsKey(LocalStorageKey.token); + } + + Future saveCurrentUser(UserDto user) async { + final userJsonString = jsonEncode(user.toJson()); + await _sharedPreferences.setString(LocalStorageKey.user, userJsonString); + } + + Future currentUser() async { + final userString = _sharedPreferences.getString(LocalStorageKey.user); + if (userString == null) return User.empty(); + + final Map userMap = jsonDecode(userString); + final userDto = UserDto.fromJson(userMap); + return userDto.toDomain(); + } + + Future deleteCurrentUser() async { + await _sharedPreferences.remove(LocalStorageKey.user); + } + + Future deleteAllAuth() async { + await _sharedPreferences.remove(LocalStorageKey.token); + await _sharedPreferences.remove(LocalStorageKey.user); + } +} diff --git a/lib/infrastructure/auth/datasources/remote_data_provider.dart b/lib/infrastructure/auth/datasources/remote_data_provider.dart new file mode 100644 index 0000000..41033f3 --- /dev/null +++ b/lib/infrastructure/auth/datasources/remote_data_provider.dart @@ -0,0 +1,44 @@ +import 'dart:developer'; + +import 'package:injectable/injectable.dart'; +import 'package:data_channel/data_channel.dart'; + +import '../../../common/api/api_client.dart'; +import '../../../common/api/api_failure.dart'; +import '../../../common/url/api_path.dart'; +import '../../../domain/auth/auth.dart'; +import '../auth_dtos.dart'; + +@injectable +class AuthRemoteDataProvider { + final ApiClient _apiClient; + final String _logName = "AuthRemoteDataProvider"; + + AuthRemoteDataProvider(this._apiClient); + + Future> login({ + required String email, + required String password, + }) async { + try { + final response = await _apiClient.post( + ApiPath.login, + data: {'email': email, 'password': password}, + ); + + if (response.data['code'] == 401) { + return DC.error( + AuthFailure.serverError( + ApiFailure.unauthorized('Incorrect email or password'), + ), + ); + } + + final dto = AuthDto.fromJson(response.data['data']); + return DC.data(dto); + } on ApiFailure catch (e, s) { + log('login', name: _logName, error: e, stackTrace: s); + return DC.error(AuthFailure.serverError(e)); + } + } +} diff --git a/lib/infrastructure/auth/dto/auth_dto.dart b/lib/infrastructure/auth/dto/auth_dto.dart new file mode 100644 index 0000000..8ee5a68 --- /dev/null +++ b/lib/infrastructure/auth/dto/auth_dto.dart @@ -0,0 +1,23 @@ +part of '../auth_dtos.dart'; + +@freezed +class AuthDto with _$AuthDto { + const AuthDto._(); + + const factory AuthDto({ + @JsonKey(name: 'token') String? token, + @JsonKey(name: 'expires_at') String? expiresAt, + @JsonKey(name: 'user') UserDto? user, + }) = _AuthDto; + + factory AuthDto.fromJson(Map json) => + _$AuthDtoFromJson(json); + + Auth toDomain() => Auth( + token: token ?? '', + expiresAt: expiresAt != null + ? DateTime.parse(expiresAt ?? "") + : DateTime.fromMillisecondsSinceEpoch(0), + user: user?.toDomain() ?? User.empty(), + ); +} diff --git a/lib/infrastructure/auth/dto/user_dto.dart b/lib/infrastructure/auth/dto/user_dto.dart new file mode 100644 index 0000000..be57e5d --- /dev/null +++ b/lib/infrastructure/auth/dto/user_dto.dart @@ -0,0 +1,39 @@ +part of '../auth_dtos.dart'; + +@freezed +class UserDto with _$UserDto { + const UserDto._(); + + const factory UserDto({ + @JsonKey(name: 'id') String? id, + @JsonKey(name: 'organization_id') String? organizationId, + @JsonKey(name: 'outlet_id') String? outletId, + @JsonKey(name: 'name') String? name, + @JsonKey(name: 'email') String? email, + @JsonKey(name: 'role') String? role, + @JsonKey(name: 'permissions') Map? permissions, + @JsonKey(name: 'is_active') bool? isActive, + @JsonKey(name: 'created_at') String? createdAt, + @JsonKey(name: 'updated_at') String? updatedAt, + }) = _UserDto; + + factory UserDto.fromJson(Map json) => + _$UserDtoFromJson(json); + + User toDomain() => User( + id: id ?? '', + organizationId: organizationId ?? '', + outletId: outletId ?? '', + name: name ?? '', + email: email ?? '', + role: role ?? '', + permissions: permissions ?? {}, + isActive: isActive ?? false, + createdAt: createdAt != null + ? DateTime.parse(createdAt ?? "") + : DateTime.fromMillisecondsSinceEpoch(0), + updatedAt: updatedAt != null + ? DateTime.parse(updatedAt ?? "") + : DateTime.fromMillisecondsSinceEpoch(0), + ); +} diff --git a/lib/infrastructure/auth/repositories/auth_repository.dart b/lib/infrastructure/auth/repositories/auth_repository.dart new file mode 100644 index 0000000..9faa273 --- /dev/null +++ b/lib/infrastructure/auth/repositories/auth_repository.dart @@ -0,0 +1,45 @@ +import 'dart:developer'; + +import 'package:dartz/dartz.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../domain/auth/auth.dart'; +import '../datasources/local_data_provider.dart'; +import '../datasources/remote_data_provider.dart'; + +@Injectable(as: IAuthRepository) +class AuthRepository implements IAuthRepository { + final AuthLocalDataProvider _localDataProvider; + final AuthRemoteDataProvider _remoteDataProvider; + + final String _logName = 'AuthRepository'; + + AuthRepository(this._localDataProvider, this._remoteDataProvider); + + @override + Future> login({ + required String email, + required String password, + }) async { + try { + final result = await _remoteDataProvider.login( + email: email, + password: password, + ); + + if (result.hasError) { + return left(result.error!); + } + + final auth = result.data!.toDomain(); + + await _localDataProvider.saveToken(auth.token); + await _localDataProvider.saveCurrentUser(result.data!.user!); + + return right(auth); + } catch (e, s) { + log('loginError', name: _logName, error: e, stackTrace: s); + return left(const AuthFailure.unexpectedError()); + } + } +} diff --git a/lib/injection.config.dart b/lib/injection.config.dart index b62556e..4a98ad4 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -9,6 +9,8 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:apskel_owner_flutter/application/auth/login_form/login_form_bloc.dart' + as _i775; import 'package:apskel_owner_flutter/application/language/language_bloc.dart' as _i455; import 'package:apskel_owner_flutter/common/api/api_client.dart' as _i115; @@ -20,7 +22,14 @@ import 'package:apskel_owner_flutter/common/di/di_shared_preferences.dart' as _i402; import 'package:apskel_owner_flutter/common/network/network_client.dart' as _i543; +import 'package:apskel_owner_flutter/domain/auth/auth.dart' as _i49; import 'package:apskel_owner_flutter/env.dart' as _i6; +import 'package:apskel_owner_flutter/infrastructure/auth/datasources/local_data_provider.dart' + as _i991; +import 'package:apskel_owner_flutter/infrastructure/auth/datasources/remote_data_provider.dart' + as _i17; +import 'package:apskel_owner_flutter/infrastructure/auth/repositories/auth_repository.dart' + as _i1035; import 'package:apskel_owner_flutter/presentation/router/app_router.dart' as _i258; import 'package:connectivity_plus/connectivity_plus.dart' as _i895; @@ -63,10 +72,25 @@ extension GetItInjectableX on _i174.GetIt { () => _i455.LanguageBloc(gh<_i460.SharedPreferences>()), ); gh.factory<_i6.Env>(() => _i6.DevEnv(), registerFor: {_dev}); + gh.factory<_i991.AuthLocalDataProvider>( + () => _i991.AuthLocalDataProvider(gh<_i460.SharedPreferences>()), + ); gh.lazySingleton<_i115.ApiClient>( () => _i115.ApiClient(gh<_i361.Dio>(), gh<_i6.Env>()), ); gh.factory<_i6.Env>(() => _i6.ProdEnv(), registerFor: {_prod}); + gh.factory<_i17.AuthRemoteDataProvider>( + () => _i17.AuthRemoteDataProvider(gh<_i115.ApiClient>()), + ); + gh.factory<_i49.IAuthRepository>( + () => _i1035.AuthRepository( + gh<_i991.AuthLocalDataProvider>(), + gh<_i17.AuthRemoteDataProvider>(), + ), + ); + gh.factory<_i775.LoginFormBloc>( + () => _i775.LoginFormBloc(gh<_i49.IAuthRepository>()), + ); return this; } } diff --git a/lib/presentation/components/field/password_text_form_field.dart b/lib/presentation/components/field/password_text_form_field.dart index 7da90f0..ef285eb 100644 --- a/lib/presentation/components/field/password_text_form_field.dart +++ b/lib/presentation/components/field/password_text_form_field.dart @@ -8,6 +8,7 @@ class AppPasswordTextFormField extends StatefulWidget { this.hintText, required this.prefixIcon, this.validator, + this.onChanged, }); final TextEditingController? controller; @@ -15,6 +16,7 @@ class AppPasswordTextFormField extends StatefulWidget { final String? hintText; final IconData prefixIcon; final String? Function(String?)? validator; + final Function(String)? onChanged; @override State createState() => @@ -42,6 +44,7 @@ class _AppPasswordTextFormFieldState extends State { cursorColor: AppColor.primary, obscureText: _obscurePassword, style: AppStyle.md.copyWith(color: AppColor.textPrimary), + onChanged: widget.onChanged, decoration: InputDecoration( hintText: widget.hintText, prefixIcon: LineIcon( diff --git a/lib/presentation/components/field/text_form_field.dart b/lib/presentation/components/field/text_form_field.dart index 2400c8e..4b2fcc3 100644 --- a/lib/presentation/components/field/text_form_field.dart +++ b/lib/presentation/components/field/text_form_field.dart @@ -8,6 +8,7 @@ class AppTextFormField extends StatelessWidget { this.hintText, required this.prefixIcon, this.validator, + this.onChanged, }); final TextEditingController? controller; @@ -15,6 +16,7 @@ class AppTextFormField extends StatelessWidget { final String? hintText; final IconData prefixIcon; final String? Function(String?)? validator; + final Function(String)? onChanged; @override Widget build(BuildContext context) { @@ -33,6 +35,7 @@ class AppTextFormField extends StatelessWidget { controller: controller, keyboardType: TextInputType.emailAddress, cursorColor: AppColor.primary, + onChanged: onChanged, style: AppStyle.md.copyWith(color: AppColor.textPrimary), decoration: InputDecoration( hintText: hintText, diff --git a/lib/presentation/components/toast/flushbar.dart b/lib/presentation/components/toast/flushbar.dart index 6e1aa52..55732a9 100644 --- a/lib/presentation/components/toast/flushbar.dart +++ b/lib/presentation/components/toast/flushbar.dart @@ -2,6 +2,7 @@ import 'package:another_flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import '../../../common/theme/theme.dart'; +import '../../../domain/auth/auth.dart'; class AppFlushbar { static void showSuccess(BuildContext context, String message) { @@ -40,4 +41,14 @@ class AppFlushbar { margin: const EdgeInsets.all(12), ).show(context); } + + static void showAuthFailureToast(BuildContext context, AuthFailure failure) => + showError( + context, + failure.map( + serverError: (value) => value.failure.toStringFormatted(context), + dynamicErrorMessage: (value) => value.erroMessage, + unexpectedError: (value) => 'Error has eccoured', + ), + ); } diff --git a/lib/presentation/pages/auth/login/login_page.dart b/lib/presentation/pages/auth/login/login_page.dart index dd11d8f..4046c24 100644 --- a/lib/presentation/pages/auth/login/login_page.dart +++ b/lib/presentation/pages/auth/login/login_page.dart @@ -1,30 +1,32 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:math' as math; +import '../../../../application/auth/login_form/login_form_bloc.dart'; import '../../../../common/extension/extension.dart'; import '../../../../common/theme/theme.dart'; +import '../../../../injection.dart'; import '../../../components/button/button.dart'; import '../../../components/spacer/spacer.dart'; +import '../../../components/toast/flushbar.dart'; import '../../../router/app_router.gr.dart'; import 'widgets/email_field.dart'; import 'widgets/password_field.dart'; @RoutePage() -class LoginPage extends StatefulWidget { +class LoginPage extends StatefulWidget implements AutoRouteWrapper { const LoginPage({super.key}); @override State createState() => _LoginPageState(); + + @override + Widget wrappedRoute(BuildContext context) => + BlocProvider(create: (_) => getIt(), child: this); } class _LoginPageState extends State with TickerProviderStateMixin { - final _formKey = GlobalKey(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - - bool _isLoading = false; - late AnimationController _fadeController; late AnimationController _slideController; late AnimationController _backgroundController; @@ -77,9 +79,6 @@ class _LoginPageState extends State with TickerProviderStateMixin { CurvedAnimation(parent: _floatingController, curve: Curves.easeInOut), ); - _emailController.text = 'test@example.com'; - _passwordController.text = 'password'; - _fadeController.forward(); _slideController.forward(); } @@ -90,78 +89,88 @@ class _LoginPageState extends State with TickerProviderStateMixin { _slideController.dispose(); _backgroundController.dispose(); _floatingController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); super.dispose(); } Future _handleLogin() async { - if (_formKey.currentState!.validate()) { - setState(() { - _isLoading = true; - }); - - // Simulasi proses login - await Future.delayed(const Duration(seconds: 2)); - - setState(() { - _isLoading = false; - }); - - context.router.replace(const MainRoute()); - } + context.read().add(LoginFormEvent.submitted()); } @override Widget build(BuildContext context) { - return Scaffold( - body: AnimatedBuilder( - animation: Listenable.merge([ - _backgroundController, - _floatingController, - ]), - builder: (context, child) { - return Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: AppColor.primaryGradient, + return BlocListener( + listenWhen: (previous, current) => + previous.failureOrAuthOption != current.failureOrAuthOption, + listener: (context, state) { + state.failureOrAuthOption.fold( + () => null, + (either) => either.fold( + (f) => AppFlushbar.showAuthFailureToast(context, f), + (user) { + if (context.mounted) { + context.router.replace(const MainRoute()); + } + }, + ), + ); + }, + child: Scaffold( + body: AnimatedBuilder( + animation: Listenable.merge([ + _backgroundController, + _floatingController, + ]), + builder: (context, child) { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: AppColor.primaryGradient, + ), ), - ), - child: Stack( - children: [ - // Animated background elements - _buildAnimatedBackground(), + child: Stack( + children: [ + // Animated background elements + _buildAnimatedBackground(), - // Main content - SafeArea( - child: Center( - child: SingleChildScrollView( - padding: EdgeInsets.symmetric( - horizontal: AppValue.padding, - ), - child: FadeTransition( - opacity: _fadeAnimation, - child: SlideTransition( - position: _slideAnimation, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildLogo(context), - SpaceHeight(48), - _buildLoginCard(context), - ], + // Main content + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: AppValue.padding, + ), + child: FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLogo(context), + SpaceHeight(48), + _buildLoginCard( + context, + state.isSubmitting, + state.showErrorMessages, + ), + ], + ); + }, + ), ), ), ), ), ), - ), - ], - ), - ); - }, + ], + ), + ); + }, + ), ), ); } @@ -312,7 +321,11 @@ class _LoginPageState extends State with TickerProviderStateMixin { ); } - Widget _buildLoginCard(BuildContext context) { + Widget _buildLoginCard( + BuildContext context, + bool isLoading, + bool showErrorMessages, + ) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24), @@ -335,17 +348,20 @@ class _LoginPageState extends State with TickerProviderStateMixin { ], ), child: Form( - key: _formKey, + autovalidateMode: showErrorMessages + ? AutovalidateMode.always + : AutovalidateMode.disabled, + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - LoginEmailField(controller: _emailController), + LoginEmailField(), const SpaceHeight(24), - LoginPasswordField(controller: _passwordController), + LoginPasswordField(), const SpaceHeight(16), _buildForgetPassword(context), const SpaceHeight(32), - _buildLoginButton(), + _buildLoginButton(isLoading), ], ), ), @@ -368,11 +384,11 @@ class _LoginPageState extends State with TickerProviderStateMixin { ); } - Widget _buildLoginButton() { + Widget _buildLoginButton(bool isLoading) { return AppElevatedButton( text: context.lang.sign_in, - isLoading: _isLoading, - onPressed: _isLoading ? null : _handleLogin, + isLoading: isLoading, + onPressed: _handleLogin, ); } } diff --git a/lib/presentation/pages/auth/login/widgets/email_field.dart b/lib/presentation/pages/auth/login/widgets/email_field.dart index f4bca26..0c041b6 100644 --- a/lib/presentation/pages/auth/login/widgets/email_field.dart +++ b/lib/presentation/pages/auth/login/widgets/email_field.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; +import '../../../../../application/auth/login_form/login_form_bloc.dart'; import '../../../../../common/extension/extension.dart'; import '../../../../../common/validator/validator.dart'; import '../../../../components/field/field.dart'; class LoginEmailField extends StatelessWidget { - final TextEditingController? controller; - const LoginEmailField({super.key, this.controller}); + const LoginEmailField({super.key}); @override Widget build(BuildContext context) { @@ -15,8 +16,10 @@ class LoginEmailField extends StatelessWidget { title: context.lang.email, hintText: context.lang.email_placeholder, prefixIcon: LineIcons.envelope, - validator: AppValidator.validateEmail, - controller: controller, + validator: (value) => + AppValidator.validateEmail(context.read().state.email), + onChanged: (value) => + context.read().add(LoginFormEvent.emailChanged(value)), ); } } diff --git a/lib/presentation/pages/auth/login/widgets/password_field.dart b/lib/presentation/pages/auth/login/widgets/password_field.dart index 5ab2436..5ddab86 100644 --- a/lib/presentation/pages/auth/login/widgets/password_field.dart +++ b/lib/presentation/pages/auth/login/widgets/password_field.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; +import '../../../../../application/auth/login_form/login_form_bloc.dart'; import '../../../../../common/extension/extension.dart'; import '../../../../../common/validator/validator.dart'; import '../../../../components/field/field.dart'; class LoginPasswordField extends StatelessWidget { - final TextEditingController controller; - const LoginPasswordField({super.key, required this.controller}); + const LoginPasswordField({super.key}); @override Widget build(BuildContext context) { @@ -15,8 +16,12 @@ class LoginPasswordField extends StatelessWidget { title: context.lang.password, prefixIcon: LineIcons.lock, hintText: context.lang.password_placeholder, - validator: AppValidator.validatePassword, - controller: controller, + validator: (value) => AppValidator.validatePassword( + context.read().state.password, + ), + onChanged: (value) => context.read().add( + LoginFormEvent.passwordChanged(value), + ), ); } } diff --git a/lib/presentation/router/app_router.gr.dart b/lib/presentation/router/app_router.gr.dart index a530708..f7fd053 100644 --- a/lib/presentation/router/app_router.gr.dart +++ b/lib/presentation/router/app_router.gr.dart @@ -150,7 +150,7 @@ class LoginRoute extends _i17.PageRouteInfo { static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i7.LoginPage(); + return _i17.WrappedRoute(child: const _i7.LoginPage()); }, ); }