feat: logout

This commit is contained in:
efrilm 2025-08-16 19:29:21 +07:00
parent e1bde24a83
commit f138e1dcc9
12 changed files with 532 additions and 78 deletions

View File

@ -0,0 +1,36 @@
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 'logout_form_event.dart';
part 'logout_form_state.dart';
part 'logout_form_bloc.freezed.dart';
@injectable
class LogoutFormBloc extends Bloc<LogoutFormEvent, LogoutFormState> {
final IAuthRepository _repository;
LogoutFormBloc(this._repository) : super(LogoutFormState.initial()) {
on<LogoutFormEvent>(_onLogoutFormEvent);
}
Future<void> _onLogoutFormEvent(
LogoutFormEvent event,
Emitter<LogoutFormState> emit,
) {
return event.map(
submitted: (e) async {
emit(state.copyWith(isSubmitting: true, failureOrAuthOption: none()));
final failureOrAuth = await _repository.logout();
emit(
state.copyWith(
isSubmitting: false,
failureOrAuthOption: optionOf(failureOrAuth),
),
);
},
);
}
}

View File

@ -0,0 +1,335 @@
// 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 'logout_form_bloc.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(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 _$LogoutFormEvent {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function() submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Submitted value) submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Submitted value)? submitted,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogoutFormEventCopyWith<$Res> {
factory $LogoutFormEventCopyWith(
LogoutFormEvent value,
$Res Function(LogoutFormEvent) then,
) = _$LogoutFormEventCopyWithImpl<$Res, LogoutFormEvent>;
}
/// @nodoc
class _$LogoutFormEventCopyWithImpl<$Res, $Val extends LogoutFormEvent>
implements $LogoutFormEventCopyWith<$Res> {
_$LogoutFormEventCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LogoutFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$SubmittedImplCopyWith<$Res> {
factory _$$SubmittedImplCopyWith(
_$SubmittedImpl value,
$Res Function(_$SubmittedImpl) then,
) = __$$SubmittedImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$SubmittedImplCopyWithImpl<$Res>
extends _$LogoutFormEventCopyWithImpl<$Res, _$SubmittedImpl>
implements _$$SubmittedImplCopyWith<$Res> {
__$$SubmittedImplCopyWithImpl(
_$SubmittedImpl _value,
$Res Function(_$SubmittedImpl) _then,
) : super(_value, _then);
/// Create a copy of LogoutFormEvent
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$SubmittedImpl implements _Submitted {
const _$SubmittedImpl();
@override
String toString() {
return 'LogoutFormEvent.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<TResult extends Object?>({
required TResult Function() submitted,
}) {
return submitted();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function()? submitted,
}) {
return submitted?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function()? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(_Submitted value) submitted,
}) {
return submitted(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(_Submitted value)? submitted,
}) {
return submitted?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(_Submitted value)? submitted,
required TResult orElse(),
}) {
if (submitted != null) {
return submitted(this);
}
return orElse();
}
}
abstract class _Submitted implements LogoutFormEvent {
const factory _Submitted() = _$SubmittedImpl;
}
/// @nodoc
mixin _$LogoutFormState {
Option<Either<AuthFailure, Unit>> get failureOrAuthOption =>
throw _privateConstructorUsedError;
bool get isSubmitting => throw _privateConstructorUsedError;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LogoutFormStateCopyWith<LogoutFormState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LogoutFormStateCopyWith<$Res> {
factory $LogoutFormStateCopyWith(
LogoutFormState value,
$Res Function(LogoutFormState) then,
) = _$LogoutFormStateCopyWithImpl<$Res, LogoutFormState>;
@useResult
$Res call({
Option<Either<AuthFailure, Unit>> failureOrAuthOption,
bool isSubmitting,
});
}
/// @nodoc
class _$LogoutFormStateCopyWithImpl<$Res, $Val extends LogoutFormState>
implements $LogoutFormStateCopyWith<$Res> {
_$LogoutFormStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? failureOrAuthOption = null, Object? isSubmitting = null}) {
return _then(
_value.copyWith(
failureOrAuthOption: null == failureOrAuthOption
? _value.failureOrAuthOption
: failureOrAuthOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Unit>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$LogoutFormStateImplCopyWith<$Res>
implements $LogoutFormStateCopyWith<$Res> {
factory _$$LogoutFormStateImplCopyWith(
_$LogoutFormStateImpl value,
$Res Function(_$LogoutFormStateImpl) then,
) = __$$LogoutFormStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
Option<Either<AuthFailure, Unit>> failureOrAuthOption,
bool isSubmitting,
});
}
/// @nodoc
class __$$LogoutFormStateImplCopyWithImpl<$Res>
extends _$LogoutFormStateCopyWithImpl<$Res, _$LogoutFormStateImpl>
implements _$$LogoutFormStateImplCopyWith<$Res> {
__$$LogoutFormStateImplCopyWithImpl(
_$LogoutFormStateImpl _value,
$Res Function(_$LogoutFormStateImpl) _then,
) : super(_value, _then);
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? failureOrAuthOption = null, Object? isSubmitting = null}) {
return _then(
_$LogoutFormStateImpl(
failureOrAuthOption: null == failureOrAuthOption
? _value.failureOrAuthOption
: failureOrAuthOption // ignore: cast_nullable_to_non_nullable
as Option<Either<AuthFailure, Unit>>,
isSubmitting: null == isSubmitting
? _value.isSubmitting
: isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$LogoutFormStateImpl implements _LogoutFormState {
const _$LogoutFormStateImpl({
required this.failureOrAuthOption,
this.isSubmitting = false,
});
@override
final Option<Either<AuthFailure, Unit>> failureOrAuthOption;
@override
@JsonKey()
final bool isSubmitting;
@override
String toString() {
return 'LogoutFormState(failureOrAuthOption: $failureOrAuthOption, isSubmitting: $isSubmitting)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LogoutFormStateImpl &&
(identical(other.failureOrAuthOption, failureOrAuthOption) ||
other.failureOrAuthOption == failureOrAuthOption) &&
(identical(other.isSubmitting, isSubmitting) ||
other.isSubmitting == isSubmitting));
}
@override
int get hashCode =>
Object.hash(runtimeType, failureOrAuthOption, isSubmitting);
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LogoutFormStateImplCopyWith<_$LogoutFormStateImpl> get copyWith =>
__$$LogoutFormStateImplCopyWithImpl<_$LogoutFormStateImpl>(
this,
_$identity,
);
}
abstract class _LogoutFormState implements LogoutFormState {
const factory _LogoutFormState({
required final Option<Either<AuthFailure, Unit>> failureOrAuthOption,
final bool isSubmitting,
}) = _$LogoutFormStateImpl;
@override
Option<Either<AuthFailure, Unit>> get failureOrAuthOption;
@override
bool get isSubmitting;
/// Create a copy of LogoutFormState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LogoutFormStateImplCopyWith<_$LogoutFormStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,6 @@
part of 'logout_form_bloc.dart';
@freezed
class LogoutFormEvent with _$LogoutFormEvent {
const factory LogoutFormEvent.submitted() = _Submitted;
}

View File

@ -0,0 +1,12 @@
part of 'logout_form_bloc.dart';
@freezed
class LogoutFormState with _$LogoutFormState {
const factory LogoutFormState({
required Option<Either<AuthFailure, Unit>> failureOrAuthOption,
@Default(false) bool isSubmitting,
}) = _LogoutFormState;
factory LogoutFormState.initial() =>
LogoutFormState(failureOrAuthOption: none(), isSubmitting: false);
}

View File

@ -2,8 +2,10 @@ import 'package:awesome_dio_interceptor/awesome_dio_interceptor.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../env.dart';
import '../constant/local_storage_key.dart';
import 'api_failure.dart';
import 'errors/bad_network_error.dart';
import 'errors/bad_request_error.dart';
@ -22,11 +24,16 @@ import 'interceptors/unauthorized_interceptor.dart';
class ApiClient {
final Dio _dio;
final Env _env;
final SharedPreferences _preferences;
ApiClient(this._dio, this._env) {
ApiClient(this._dio, this._env, this._preferences) {
_dio.options.baseUrl = _env.baseUrl;
_dio.options.validateStatus = (status) => status! < 500;
_dio.options.connectTimeout = const Duration(seconds: 20);
_dio.options.headers = {
'authorization':
'Bearer ${_preferences.getString(LocalStorageKey.token)}',
};
_dio.interceptors.add(BadNetworkErrorInterceptor());
_dio.interceptors.add(BadRequestErrorInterceptor());
_dio.interceptors.add(InternalServerErrorInterceptor());

View File

@ -1,3 +1,5 @@
class ApiPath {
// Auth
static const String login = '/api/v1/auth/login';
static const String logout = '/api/v1/auth/logout';
}

View File

@ -7,4 +7,5 @@ abstract class IAuthRepository {
});
Future<bool> hasToken();
Future<Either<AuthFailure, User>> currentUser();
Future<Either<AuthFailure, Unit>> logout();
}

View File

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import 'package:data_channel/data_channel.dart';
@ -41,4 +42,14 @@ class AuthRemoteDataProvider {
return DC.error(AuthFailure.serverError(e));
}
}
Future<DC<AuthFailure, Unit>> logout() async {
try {
await _apiClient.post(ApiPath.logout);
return DC.data(unit);
} on ApiFailure catch (e, s) {
log('login', name: _logName, error: e, stackTrace: s);
return DC.error(AuthFailure.serverError(e));
}
}
}

View File

@ -58,4 +58,20 @@ class AuthRepository implements IAuthRepository {
Future<bool> hasToken() async {
return await _localDataProvider.hasToken();
}
@override
Future<Either<AuthFailure, Unit>> logout() async {
try {
final result = await _remoteDataProvider.logout();
if (result.hasError) {
return left(result.error!);
}
await _localDataProvider.deleteAllAuth();
return right(unit);
} catch (e, s) {
log('logoutError', name: _logName, error: e, stackTrace: s);
return left(const AuthFailure.unexpectedError());
}
}
}

View File

@ -12,6 +12,8 @@
import 'package:apskel_owner_flutter/application/auth/auth_bloc.dart' as _i945;
import 'package:apskel_owner_flutter/application/auth/login_form/login_form_bloc.dart'
as _i775;
import 'package:apskel_owner_flutter/application/auth/logout_form/logout_form_bloc.dart'
as _i574;
import 'package:apskel_owner_flutter/application/language/language_bloc.dart'
as _i455;
import 'package:apskel_owner_flutter/common/api/api_client.dart' as _i115;
@ -76,10 +78,14 @@ extension GetItInjectableX on _i174.GetIt {
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.lazySingleton<_i115.ApiClient>(
() => _i115.ApiClient(
gh<_i361.Dio>(),
gh<_i6.Env>(),
gh<_i460.SharedPreferences>(),
),
);
gh.factory<_i17.AuthRemoteDataProvider>(
() => _i17.AuthRemoteDataProvider(gh<_i115.ApiClient>()),
);
@ -95,6 +101,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i945.AuthBloc>(
() => _i945.AuthBloc(gh<_i49.IAuthRepository>()),
);
gh.factory<_i574.LogoutFormBloc>(
() => _i574.LogoutFormBloc(gh<_i49.IAuthRepository>()),
);
return this;
}
}

View File

@ -1,10 +1,15 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart';
import '../../../application/auth/logout_form/logout_form_bloc.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/account_info.dart';
import 'widgets/app_setting.dart';
import 'widgets/business_setting.dart';
@ -13,17 +18,22 @@ import 'widgets/header.dart';
import 'widgets/support.dart';
@RoutePage()
class ProfilePage extends StatefulWidget {
class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
return BlocListener<LogoutFormBloc, LogoutFormState>(
listener: (context, state) {
state.failureOrAuthOption.fold(
() => null,
(either) => either.fold(
(f) => AppFlushbar.showAuthFailureToast(context, f),
(_) => context.router.replace(const LoginRoute()),
),
);
},
child: Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
@ -95,6 +105,11 @@ class _ProfilePageState extends State<ProfilePage> {
),
],
),
),
);
}
@override
Widget wrappedRoute(BuildContext context) =>
BlocProvider(create: (_) => getIt<LogoutFormBloc>(), child: this);
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/auth/logout_form/logout_form_bloc.dart';
import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart';
import 'profile_tile.dart';
@ -49,7 +51,9 @@ class ProfileDangerZone extends StatelessWidget {
subtitle: 'Sign out from your account',
iconColor: AppColor.error,
textColor: AppColor.error,
onTap: () {},
onTap: () {
context.read<LogoutFormBloc>().add(LogoutFormEvent.submitted());
},
),
],
),