From 006486bc2a22f754b581c44d7a98993583253bd7 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 18 Sep 2025 09:31:42 +0700 Subject: [PATCH] Auth --- lib/application/auth/auth_bloc.dart | 49 ++ lib/application/auth/auth_bloc.freezed.dart | 806 ++++++++++++++++++ lib/application/auth/auth_event.dart | 6 + lib/application/auth/auth_state.dart | 26 + lib/common/constant/local_storage_key.dart | 4 + .../auth/repositories/i_auth_repository.dart | 6 + .../auth/datasources/local_data_provider.dart | 60 ++ .../auth/repositories/auth_repository.dart | 37 +- lib/injection.config.dart | 20 +- lib/presentation/app_widget.dart | 17 +- lib/presentation/components/field/field.dart | 1 - .../create_password/create_password_page.dart | 2 + .../pages/auth/password/password_page.dart | 2 + .../pages/splash/splash_page.dart | 100 ++- 14 files changed, 1081 insertions(+), 55 deletions(-) create mode 100644 lib/application/auth/auth_bloc.dart create mode 100644 lib/application/auth/auth_bloc.freezed.dart create mode 100644 lib/application/auth/auth_event.dart create mode 100644 lib/application/auth/auth_state.dart create mode 100644 lib/common/constant/local_storage_key.dart create mode 100644 lib/infrastructure/auth/datasources/local_data_provider.dart diff --git a/lib/application/auth/auth_bloc.dart b/lib/application/auth/auth_bloc.dart new file mode 100644 index 0000000..0ca87c5 --- /dev/null +++ b/lib/application/auth/auth_bloc.dart @@ -0,0 +1,49 @@ +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 'auth_event.dart'; +part 'auth_state.dart'; +part 'auth_bloc.freezed.dart'; + +@injectable +class AuthBloc extends Bloc { + final IAuthRepository _repository; + AuthBloc(this._repository) : super(AuthState.initial()) { + on(_onAuthEvent); + } + + Future _onAuthEvent(AuthEvent event, Emitter emit) { + return event.map( + fetchCurrentUser: (e) async { + emit(state.copyWith(failureOption: none())); + + final token = await _repository.hasToken(); + + final failureOrAuth = await _repository.currentUser(); + + failureOrAuth.fold( + (f) => emit( + state.copyWith( + failureOption: optionOf(f), + status: token + ? AuthStatus.authenticated() + : AuthStatus.unauthenticated(), + ), + ), + (user) => emit( + state.copyWith( + user: user, + status: token + ? AuthStatus.authenticated() + : AuthStatus.unauthenticated(), + ), + ), + ); + }, + ); + } +} diff --git a/lib/application/auth/auth_bloc.freezed.dart b/lib/application/auth/auth_bloc.freezed.dart new file mode 100644 index 0000000..7fd3e02 --- /dev/null +++ b/lib/application/auth/auth_bloc.freezed.dart @@ -0,0 +1,806 @@ +// 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_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 _$AuthEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() fetchCurrentUser, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchCurrentUser, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchCurrentUser, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchCurrentUser value) fetchCurrentUser, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchCurrentUser value)? fetchCurrentUser, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchCurrentUser value)? fetchCurrentUser, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthEventCopyWith<$Res> { + factory $AuthEventCopyWith(AuthEvent value, $Res Function(AuthEvent) then) = + _$AuthEventCopyWithImpl<$Res, AuthEvent>; +} + +/// @nodoc +class _$AuthEventCopyWithImpl<$Res, $Val extends AuthEvent> + implements $AuthEventCopyWith<$Res> { + _$AuthEventCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$FetchCurrentUserImplCopyWith<$Res> { + factory _$$FetchCurrentUserImplCopyWith( + _$FetchCurrentUserImpl value, + $Res Function(_$FetchCurrentUserImpl) then, + ) = __$$FetchCurrentUserImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$FetchCurrentUserImplCopyWithImpl<$Res> + extends _$AuthEventCopyWithImpl<$Res, _$FetchCurrentUserImpl> + implements _$$FetchCurrentUserImplCopyWith<$Res> { + __$$FetchCurrentUserImplCopyWithImpl( + _$FetchCurrentUserImpl _value, + $Res Function(_$FetchCurrentUserImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$FetchCurrentUserImpl implements _FetchCurrentUser { + const _$FetchCurrentUserImpl(); + + @override + String toString() { + return 'AuthEvent.fetchCurrentUser()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$FetchCurrentUserImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() fetchCurrentUser, + }) { + return fetchCurrentUser(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? fetchCurrentUser, + }) { + return fetchCurrentUser?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? fetchCurrentUser, + required TResult orElse(), + }) { + if (fetchCurrentUser != null) { + return fetchCurrentUser(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchCurrentUser value) fetchCurrentUser, + }) { + return fetchCurrentUser(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchCurrentUser value)? fetchCurrentUser, + }) { + return fetchCurrentUser?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchCurrentUser value)? fetchCurrentUser, + required TResult orElse(), + }) { + if (fetchCurrentUser != null) { + return fetchCurrentUser(this); + } + return orElse(); + } +} + +abstract class _FetchCurrentUser implements AuthEvent { + const factory _FetchCurrentUser() = _$FetchCurrentUserImpl; +} + +/// @nodoc +mixin _$AuthState { + User get user => throw _privateConstructorUsedError; + AuthStatus get status => throw _privateConstructorUsedError; + Option get failureOption => throw _privateConstructorUsedError; + bool get isFetching => throw _privateConstructorUsedError; + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthStateCopyWith<$Res> { + factory $AuthStateCopyWith(AuthState value, $Res Function(AuthState) then) = + _$AuthStateCopyWithImpl<$Res, AuthState>; + @useResult + $Res call({ + User user, + AuthStatus status, + Option failureOption, + bool isFetching, + }); + + $UserCopyWith<$Res> get user; + $AuthStatusCopyWith<$Res> get status; +} + +/// @nodoc +class _$AuthStateCopyWithImpl<$Res, $Val extends AuthState> + implements $AuthStateCopyWith<$Res> { + _$AuthStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? status = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _value.copyWith( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as AuthStatus, + failureOption: null == failureOption + ? _value.failureOption + : failureOption // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } + + /// Create a copy of AuthState + /// 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); + }); + } + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthStatusCopyWith<$Res> get status { + return $AuthStatusCopyWith<$Res>(_value.status, (value) { + return _then(_value.copyWith(status: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AuthStateImplCopyWith<$Res> + implements $AuthStateCopyWith<$Res> { + factory _$$AuthStateImplCopyWith( + _$AuthStateImpl value, + $Res Function(_$AuthStateImpl) then, + ) = __$$AuthStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + User user, + AuthStatus status, + Option failureOption, + bool isFetching, + }); + + @override + $UserCopyWith<$Res> get user; + @override + $AuthStatusCopyWith<$Res> get status; +} + +/// @nodoc +class __$$AuthStateImplCopyWithImpl<$Res> + extends _$AuthStateCopyWithImpl<$Res, _$AuthStateImpl> + implements _$$AuthStateImplCopyWith<$Res> { + __$$AuthStateImplCopyWithImpl( + _$AuthStateImpl _value, + $Res Function(_$AuthStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? user = null, + Object? status = null, + Object? failureOption = null, + Object? isFetching = null, + }) { + return _then( + _$AuthStateImpl( + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as User, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as AuthStatus, + failureOption: null == failureOption + ? _value.failureOption + : failureOption // ignore: cast_nullable_to_non_nullable + as Option, + isFetching: null == isFetching + ? _value.isFetching + : isFetching // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$AuthStateImpl extends _AuthState { + const _$AuthStateImpl({ + required this.user, + this.status = const AuthStatus.initial(), + required this.failureOption, + this.isFetching = false, + }) : super._(); + + @override + final User user; + @override + @JsonKey() + final AuthStatus status; + @override + final Option failureOption; + @override + @JsonKey() + final bool isFetching; + + @override + String toString() { + return 'AuthState(user: $user, status: $status, failureOption: $failureOption, isFetching: $isFetching)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthStateImpl && + (identical(other.user, user) || other.user == user) && + (identical(other.status, status) || other.status == status) && + (identical(other.failureOption, failureOption) || + other.failureOption == failureOption) && + (identical(other.isFetching, isFetching) || + other.isFetching == isFetching)); + } + + @override + int get hashCode => + Object.hash(runtimeType, user, status, failureOption, isFetching); + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthStateImplCopyWith<_$AuthStateImpl> get copyWith => + __$$AuthStateImplCopyWithImpl<_$AuthStateImpl>(this, _$identity); +} + +abstract class _AuthState extends AuthState { + const factory _AuthState({ + required final User user, + final AuthStatus status, + required final Option failureOption, + final bool isFetching, + }) = _$AuthStateImpl; + const _AuthState._() : super._(); + + @override + User get user; + @override + AuthStatus get status; + @override + Option get failureOption; + @override + bool get isFetching; + + /// Create a copy of AuthState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthStateImplCopyWith<_$AuthStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AuthStatus { + @optionalTypeArgs + TResult when({ + required TResult Function() authenticated, + required TResult Function() unauthenticated, + required TResult Function() initial, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? authenticated, + TResult? Function()? unauthenticated, + TResult? Function()? initial, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? authenticated, + TResult Function()? unauthenticated, + TResult Function()? initial, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_Authenticated value) authenticated, + required TResult Function(_Unauthenticated value) unauthenticated, + required TResult Function(_Initial value) initial, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Authenticated value)? authenticated, + TResult? Function(_Unauthenticated value)? unauthenticated, + TResult? Function(_Initial value)? initial, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Authenticated value)? authenticated, + TResult Function(_Unauthenticated value)? unauthenticated, + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthStatusCopyWith<$Res> { + factory $AuthStatusCopyWith( + AuthStatus value, + $Res Function(AuthStatus) then, + ) = _$AuthStatusCopyWithImpl<$Res, AuthStatus>; +} + +/// @nodoc +class _$AuthStatusCopyWithImpl<$Res, $Val extends AuthStatus> + implements $AuthStatusCopyWith<$Res> { + _$AuthStatusCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthStatus + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc +abstract class _$$AuthenticatedImplCopyWith<$Res> { + factory _$$AuthenticatedImplCopyWith( + _$AuthenticatedImpl value, + $Res Function(_$AuthenticatedImpl) then, + ) = __$$AuthenticatedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AuthenticatedImplCopyWithImpl<$Res> + extends _$AuthStatusCopyWithImpl<$Res, _$AuthenticatedImpl> + implements _$$AuthenticatedImplCopyWith<$Res> { + __$$AuthenticatedImplCopyWithImpl( + _$AuthenticatedImpl _value, + $Res Function(_$AuthenticatedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthStatus + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$AuthenticatedImpl implements _Authenticated { + const _$AuthenticatedImpl(); + + @override + String toString() { + return 'AuthStatus.authenticated()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$AuthenticatedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() authenticated, + required TResult Function() unauthenticated, + required TResult Function() initial, + }) { + return authenticated(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? authenticated, + TResult? Function()? unauthenticated, + TResult? Function()? initial, + }) { + return authenticated?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? authenticated, + TResult Function()? unauthenticated, + TResult Function()? initial, + required TResult orElse(), + }) { + if (authenticated != null) { + return authenticated(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Authenticated value) authenticated, + required TResult Function(_Unauthenticated value) unauthenticated, + required TResult Function(_Initial value) initial, + }) { + return authenticated(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Authenticated value)? authenticated, + TResult? Function(_Unauthenticated value)? unauthenticated, + TResult? Function(_Initial value)? initial, + }) { + return authenticated?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Authenticated value)? authenticated, + TResult Function(_Unauthenticated value)? unauthenticated, + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) { + if (authenticated != null) { + return authenticated(this); + } + return orElse(); + } +} + +abstract class _Authenticated implements AuthStatus { + const factory _Authenticated() = _$AuthenticatedImpl; +} + +/// @nodoc +abstract class _$$UnauthenticatedImplCopyWith<$Res> { + factory _$$UnauthenticatedImplCopyWith( + _$UnauthenticatedImpl value, + $Res Function(_$UnauthenticatedImpl) then, + ) = __$$UnauthenticatedImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$UnauthenticatedImplCopyWithImpl<$Res> + extends _$AuthStatusCopyWithImpl<$Res, _$UnauthenticatedImpl> + implements _$$UnauthenticatedImplCopyWith<$Res> { + __$$UnauthenticatedImplCopyWithImpl( + _$UnauthenticatedImpl _value, + $Res Function(_$UnauthenticatedImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthStatus + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$UnauthenticatedImpl implements _Unauthenticated { + const _$UnauthenticatedImpl(); + + @override + String toString() { + return 'AuthStatus.unauthenticated()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$UnauthenticatedImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() authenticated, + required TResult Function() unauthenticated, + required TResult Function() initial, + }) { + return unauthenticated(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? authenticated, + TResult? Function()? unauthenticated, + TResult? Function()? initial, + }) { + return unauthenticated?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? authenticated, + TResult Function()? unauthenticated, + TResult Function()? initial, + required TResult orElse(), + }) { + if (unauthenticated != null) { + return unauthenticated(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Authenticated value) authenticated, + required TResult Function(_Unauthenticated value) unauthenticated, + required TResult Function(_Initial value) initial, + }) { + return unauthenticated(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Authenticated value)? authenticated, + TResult? Function(_Unauthenticated value)? unauthenticated, + TResult? Function(_Initial value)? initial, + }) { + return unauthenticated?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Authenticated value)? authenticated, + TResult Function(_Unauthenticated value)? unauthenticated, + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) { + if (unauthenticated != null) { + return unauthenticated(this); + } + return orElse(); + } +} + +abstract class _Unauthenticated implements AuthStatus { + const factory _Unauthenticated() = _$UnauthenticatedImpl; +} + +/// @nodoc +abstract class _$$InitialImplCopyWith<$Res> { + factory _$$InitialImplCopyWith( + _$InitialImpl value, + $Res Function(_$InitialImpl) then, + ) = __$$InitialImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InitialImplCopyWithImpl<$Res> + extends _$AuthStatusCopyWithImpl<$Res, _$InitialImpl> + implements _$$InitialImplCopyWith<$Res> { + __$$InitialImplCopyWithImpl( + _$InitialImpl _value, + $Res Function(_$InitialImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthStatus + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$InitialImpl implements _Initial { + const _$InitialImpl(); + + @override + String toString() { + return 'AuthStatus.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$InitialImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() authenticated, + required TResult Function() unauthenticated, + required TResult Function() initial, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? authenticated, + TResult? Function()? unauthenticated, + TResult? Function()? initial, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? authenticated, + TResult Function()? unauthenticated, + TResult Function()? initial, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_Authenticated value) authenticated, + required TResult Function(_Unauthenticated value) unauthenticated, + required TResult Function(_Initial value) initial, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_Authenticated value)? authenticated, + TResult? Function(_Unauthenticated value)? unauthenticated, + TResult? Function(_Initial value)? initial, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_Authenticated value)? authenticated, + TResult Function(_Unauthenticated value)? unauthenticated, + TResult Function(_Initial value)? initial, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _Initial implements AuthStatus { + const factory _Initial() = _$InitialImpl; +} diff --git a/lib/application/auth/auth_event.dart b/lib/application/auth/auth_event.dart new file mode 100644 index 0000000..6c3c293 --- /dev/null +++ b/lib/application/auth/auth_event.dart @@ -0,0 +1,6 @@ +part of 'auth_bloc.dart'; + +@freezed +class AuthEvent with _$AuthEvent { + const factory AuthEvent.fetchCurrentUser() = _FetchCurrentUser; +} diff --git a/lib/application/auth/auth_state.dart b/lib/application/auth/auth_state.dart new file mode 100644 index 0000000..483740d --- /dev/null +++ b/lib/application/auth/auth_state.dart @@ -0,0 +1,26 @@ +part of 'auth_bloc.dart'; + +@freezed +class AuthState with _$AuthState { + const AuthState._(); + + const factory AuthState({ + required User user, + @Default(AuthStatus.initial()) AuthStatus status, + required Option failureOption, + @Default(false) bool isFetching, + }) = _AuthState; + + factory AuthState.initial() => + AuthState(user: User.empty(), failureOption: none()); + + bool get isAuthenticated => status == const AuthStatus.authenticated(); + bool get isInitial => status == const AuthStatus.initial(); +} + +@freezed +sealed class AuthStatus with _$AuthStatus { + const factory AuthStatus.authenticated() = _Authenticated; + const factory AuthStatus.unauthenticated() = _Unauthenticated; + const factory AuthStatus.initial() = _Initial; +} diff --git a/lib/common/constant/local_storage_key.dart b/lib/common/constant/local_storage_key.dart new file mode 100644 index 0000000..b23712d --- /dev/null +++ b/lib/common/constant/local_storage_key.dart @@ -0,0 +1,4 @@ +class LocalStorageKey { + static const token = 'token'; + static const user = 'user'; +} diff --git a/lib/domain/auth/repositories/i_auth_repository.dart b/lib/domain/auth/repositories/i_auth_repository.dart index af2076c..5d50e11 100644 --- a/lib/domain/auth/repositories/i_auth_repository.dart +++ b/lib/domain/auth/repositories/i_auth_repository.dart @@ -31,4 +31,10 @@ abstract class IAuthRepository { required String phoneNumber, required String purpose, }); + + Future hasToken(); + + Future> currentUser(); + + Future> logout(); } 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..26c460c --- /dev/null +++ b/lib/infrastructure/auth/datasources/local_data_provider.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'dart:developer'; + +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; + final String _logName = 'AuthLocalDataProvider'; + + 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 { + try { + await _sharedPreferences.remove(LocalStorageKey.token); + await _sharedPreferences.remove(LocalStorageKey.user); + } catch (e) { + log('deleteAllAuthError', name: _logName, error: e); + } + } +} diff --git a/lib/infrastructure/auth/repositories/auth_repository.dart b/lib/infrastructure/auth/repositories/auth_repository.dart index 6d58085..6e0f817 100644 --- a/lib/infrastructure/auth/repositories/auth_repository.dart +++ b/lib/infrastructure/auth/repositories/auth_repository.dart @@ -4,15 +4,17 @@ 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._remoteDataProvider); + AuthRepository(this._remoteDataProvider, this._localDataProvider); @override Future> checkPhone({ @@ -105,6 +107,9 @@ class AuthRepository implements IAuthRepository { final auth = result.data!.toDomain(); + await _localDataProvider.saveToken(auth.accessToken); + await _localDataProvider.saveCurrentUser(result.data!.data!.user!); + return right(auth); } catch (e, s) { log('setPasswordError', name: _logName, error: e, stackTrace: s); @@ -129,6 +134,9 @@ class AuthRepository implements IAuthRepository { final auth = result.data!.toDomain(); + await _localDataProvider.saveToken(auth.accessToken); + await _localDataProvider.saveCurrentUser(result.data!.data!.user!); + return right(auth); } catch (e, s) { log('loginError', name: _logName, error: e, stackTrace: s); @@ -159,4 +167,31 @@ class AuthRepository implements IAuthRepository { return left(const AuthFailure.unexpectedError()); } } + + @override + Future> currentUser() async { + try { + User user = await _localDataProvider.currentUser(); + return right(user); + } catch (e, s) { + log('currentUserError', name: _logName, error: e, stackTrace: s); + return left(const AuthFailure.unexpectedError()); + } + } + + @override + Future hasToken() async { + return await _localDataProvider.hasToken(); + } + + @override + Future> logout() async { + try { + await _localDataProvider.deleteAllAuth(); + return right(unit); + } catch (e, s) { + log('logoutError', name: _logName, error: e, stackTrace: s); + return left(const AuthFailure.unexpectedError()); + } + } } diff --git a/lib/injection.config.dart b/lib/injection.config.dart index 3f17f41..ac18927 100644 --- a/lib/injection.config.dart +++ b/lib/injection.config.dart @@ -11,6 +11,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:connectivity_plus/connectivity_plus.dart' as _i895; import 'package:dio/dio.dart' as _i361; +import 'package:enaklo/application/auth/auth_bloc.dart' as _i771; import 'package:enaklo/application/auth/check_phone_form/check_phone_form_bloc.dart' as _i869; import 'package:enaklo/application/auth/login_form/login_form_bloc.dart' @@ -31,6 +32,8 @@ import 'package:enaklo/common/di/di_shared_preferences.dart' as _i672; import 'package:enaklo/common/network/network_client.dart' as _i109; import 'package:enaklo/domain/auth/auth.dart' as _i995; import 'package:enaklo/env.dart' as _i372; +import 'package:enaklo/infrastructure/auth/datasources/local_data_provider.dart' + as _i1003; import 'package:enaklo/infrastructure/auth/datasources/remote_data_provider.dart' as _i818; import 'package:enaklo/infrastructure/auth/repositories/auth_repository.dart' @@ -65,6 +68,9 @@ extension GetItInjectableX on _i174.GetIt { () => _i109.NetworkClient(gh<_i895.Connectivity>()), ); gh.factory<_i372.Env>(() => _i372.DevEnv(), registerFor: {_dev}); + gh.factory<_i1003.AuthLocalDataProvider>( + () => _i1003.AuthLocalDataProvider(gh<_i460.SharedPreferences>()), + ); gh.factory<_i372.Env>(() => _i372.ProdEnv(), registerFor: {_prod}); gh.lazySingleton<_i842.ApiClient>( () => _i842.ApiClient(gh<_i361.Dio>(), gh<_i372.Env>()), @@ -73,10 +79,10 @@ extension GetItInjectableX on _i174.GetIt { () => _i818.AuthRemoteDataProvider(gh<_i842.ApiClient>()), ); gh.factory<_i995.IAuthRepository>( - () => _i879.AuthRepository(gh<_i818.AuthRemoteDataProvider>()), - ); - gh.factory<_i510.LoginFormBloc>( - () => _i510.LoginFormBloc(gh<_i995.IAuthRepository>()), + () => _i879.AuthRepository( + gh<_i818.AuthRemoteDataProvider>(), + gh<_i1003.AuthLocalDataProvider>(), + ), ); gh.factory<_i627.ResendFormBloc>( () => _i627.ResendFormBloc(gh<_i995.IAuthRepository>()), @@ -93,6 +99,12 @@ extension GetItInjectableX on _i174.GetIt { gh.factory<_i521.VerifyFormBloc>( () => _i521.VerifyFormBloc(gh<_i995.IAuthRepository>()), ); + gh.factory<_i771.AuthBloc>( + () => _i771.AuthBloc(gh<_i995.IAuthRepository>()), + ); + gh.factory<_i510.LoginFormBloc>( + () => _i510.LoginFormBloc(gh<_i995.IAuthRepository>()), + ); return this; } } diff --git a/lib/presentation/app_widget.dart b/lib/presentation/app_widget.dart index 38ae400..9e1e18f 100644 --- a/lib/presentation/app_widget.dart +++ b/lib/presentation/app_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../application/auth/auth_bloc.dart'; import '../common/theme/theme.dart'; import '../common/constant/app_constant.dart'; import '../injection.dart'; @@ -18,12 +20,15 @@ class _AppWidgetState extends State { @override Widget build(BuildContext context) { - return MaterialApp.router( - debugShowCheckedModeBanner: false, - title: AppConstant.appName, - theme: ThemeApp.theme, - routerConfig: _appRouter.config( - navigatorObservers: () => [AppRouteObserver()], + return MultiBlocProvider( + providers: [BlocProvider(create: (context) => getIt())], + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + title: AppConstant.appName, + theme: ThemeApp.theme, + routerConfig: _appRouter.config( + navigatorObservers: () => [AppRouteObserver()], + ), ), ); } diff --git a/lib/presentation/components/field/field.dart b/lib/presentation/components/field/field.dart index b4e8984..6a1c77b 100644 --- a/lib/presentation/components/field/field.dart +++ b/lib/presentation/components/field/field.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import '../../../common/extension/extension.dart'; import '../../../common/theme/theme.dart'; diff --git a/lib/presentation/pages/auth/create_password/create_password_page.dart b/lib/presentation/pages/auth/create_password/create_password_page.dart index 48d912f..33afd3e 100644 --- a/lib/presentation/pages/auth/create_password/create_password_page.dart +++ b/lib/presentation/pages/auth/create_password/create_password_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/auth/auth_bloc.dart'; import '../../../../application/auth/set_password/set_password_form_bloc.dart'; import '../../../../injection.dart'; import '../../../components/button/button.dart'; @@ -27,6 +28,7 @@ class CreatePasswordPage extends StatelessWidget implements AutoRouteWrapper { (data) { AppFlushbar.showSuccess(context, data.message); Future.delayed(Duration(milliseconds: 1000), () { + context.read().add(AuthEvent.fetchCurrentUser()); context.router.replaceAll([MainRoute()]); }); }, diff --git a/lib/presentation/pages/auth/password/password_page.dart b/lib/presentation/pages/auth/password/password_page.dart index 3554506..147789a 100644 --- a/lib/presentation/pages/auth/password/password_page.dart +++ b/lib/presentation/pages/auth/password/password_page.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../application/auth/auth_bloc.dart'; import '../../../../application/auth/login_form/login_form_bloc.dart'; import '../../../../injection.dart'; import '../../../components/button/button.dart'; @@ -26,6 +27,7 @@ class PasswordPage extends StatelessWidget implements AutoRouteWrapper { (data) { AppFlushbar.showSuccess(context, data.message); Future.delayed(Duration(milliseconds: 1000), () { + context.read().add(AuthEvent.fetchCurrentUser()); context.router.replaceAll([MainRoute()]); }); }, diff --git a/lib/presentation/pages/splash/splash_page.dart b/lib/presentation/pages/splash/splash_page.dart index 59ca6cc..84f74e2 100644 --- a/lib/presentation/pages/splash/splash_page.dart +++ b/lib/presentation/pages/splash/splash_page.dart @@ -1,7 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:async'; +import '../../../application/auth/auth_bloc.dart'; import '../../../common/theme/theme.dart'; import '../../components/assets/assets.gen.dart'; import '../../router/app_router.gr.dart'; @@ -54,7 +56,9 @@ class _SplashPageState extends State void _navigateToHome() { Timer(const Duration(milliseconds: 2500), () { - context.router.push(OnboardingRoute()); + if (mounted) { + context.read().add(const AuthEvent.fetchCurrentUser()); + } }); } @@ -66,53 +70,63 @@ class _SplashPageState extends State @override Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [AppColor.backgroundLight, AppColor.background], + return BlocListener( + listenWhen: (previous, current) => previous.status != current.status, + listener: (context, state) { + if (state.isAuthenticated) { + context.router.replace(const MainRoute()); + } else { + context.router.replace(const OnboardingRoute()); + } + }, + child: Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColor.backgroundLight, AppColor.background], + ), ), - ), - child: Center( - child: AnimatedBuilder( - animation: _logoController, - builder: (context, child) { - return Transform.scale( - scale: _logoScaleAnimation.value, - child: Opacity( - opacity: _logoOpacityAnimation.value, - child: Container( - width: 140, - height: 140, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: AppColor.primaryGradient, - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: AppColor.primaryWithOpacity(0.4), - blurRadius: 25, - offset: const Offset(0, 12), + child: Center( + child: AnimatedBuilder( + animation: _logoController, + builder: (context, child) { + return Transform.scale( + scale: _logoScaleAnimation.value, + child: Opacity( + opacity: _logoOpacityAnimation.value, + child: Container( + width: 140, + height: 140, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppColor.primaryWithOpacity(0.4), + blurRadius: 25, + offset: const Offset(0, 12), + ), + ], + ), + child: ClipOval( + child: Padding( + padding: const EdgeInsets.all(20), + child: Assets.images.logo.image(fit: BoxFit.contain), ), - ], - ), - child: ClipOval( - child: Padding( - padding: const EdgeInsets.all(20), - child: Assets.images.logo.image(fit: BoxFit.contain), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ),