Compare commits

..

3 Commits

Author SHA1 Message Date
efrilm
5387d7b7a6 feat: global loading overlay 2025-08-16 19:47:40 +07:00
efrilm
f138e1dcc9 feat: logout 2025-08-16 19:29:21 +07:00
efrilm
e1bde24a83 feat: login 2025-08-16 19:05:43 +07:00
17 changed files with 613 additions and 92 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:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../env.dart'; import '../../env.dart';
import '../constant/local_storage_key.dart';
import 'api_failure.dart'; import 'api_failure.dart';
import 'errors/bad_network_error.dart'; import 'errors/bad_network_error.dart';
import 'errors/bad_request_error.dart'; import 'errors/bad_request_error.dart';
@ -22,11 +24,16 @@ import 'interceptors/unauthorized_interceptor.dart';
class ApiClient { class ApiClient {
final Dio _dio; final Dio _dio;
final Env _env; 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.baseUrl = _env.baseUrl;
_dio.options.validateStatus = (status) => status! < 500; _dio.options.validateStatus = (status) => status! < 500;
_dio.options.connectTimeout = const Duration(seconds: 20); _dio.options.connectTimeout = const Duration(seconds: 20);
_dio.options.headers = {
'authorization':
'Bearer ${_preferences.getString(LocalStorageKey.token)}',
};
_dio.interceptors.add(BadNetworkErrorInterceptor()); _dio.interceptors.add(BadNetworkErrorInterceptor());
_dio.interceptors.add(BadRequestErrorInterceptor()); _dio.interceptors.add(BadRequestErrorInterceptor());
_dio.interceptors.add(InternalServerErrorInterceptor()); _dio.interceptors.add(InternalServerErrorInterceptor());

View File

@ -1,3 +1,5 @@
class ApiPath { class ApiPath {
// Auth
static const String login = '/api/v1/auth/login'; 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<bool> hasToken();
Future<Either<AuthFailure, User>> currentUser(); Future<Either<AuthFailure, User>> currentUser();
Future<Either<AuthFailure, Unit>> logout();
} }

View File

@ -1,5 +1,6 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart'; import 'package:injectable/injectable.dart';
import 'package:data_channel/data_channel.dart'; import 'package:data_channel/data_channel.dart';
@ -41,4 +42,14 @@ class AuthRemoteDataProvider {
return DC.error(AuthFailure.serverError(e)); 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 { Future<bool> hasToken() async {
return await _localDataProvider.hasToken(); 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/auth_bloc.dart' as _i945;
import 'package:apskel_owner_flutter/application/auth/login_form/login_form_bloc.dart' import 'package:apskel_owner_flutter/application/auth/login_form/login_form_bloc.dart'
as _i775; 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' import 'package:apskel_owner_flutter/application/language/language_bloc.dart'
as _i455; as _i455;
import 'package:apskel_owner_flutter/common/api/api_client.dart' as _i115; 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>( gh.factory<_i991.AuthLocalDataProvider>(
() => _i991.AuthLocalDataProvider(gh<_i460.SharedPreferences>()), () => _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<_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>( gh.factory<_i17.AuthRemoteDataProvider>(
() => _i17.AuthRemoteDataProvider(gh<_i115.ApiClient>()), () => _i17.AuthRemoteDataProvider(gh<_i115.ApiClient>()),
); );
@ -95,6 +101,9 @@ extension GetItInjectableX on _i174.GetIt {
gh.factory<_i945.AuthBloc>( gh.factory<_i945.AuthBloc>(
() => _i945.AuthBloc(gh<_i49.IAuthRepository>()), () => _i945.AuthBloc(gh<_i49.IAuthRepository>()),
); );
gh.factory<_i574.LogoutFormBloc>(
() => _i574.LogoutFormBloc(gh<_i49.IAuthRepository>()),
);
return this; return this;
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:loader_overlay/loader_overlay.dart';
import '../application/auth/auth_bloc.dart'; import '../application/auth/auth_bloc.dart';
import '../application/language/language_bloc.dart'; import '../application/language/language_bloc.dart';
@ -7,6 +8,7 @@ import '../common/theme/theme.dart';
import '../common/constant/app_constant.dart'; import '../common/constant/app_constant.dart';
import '../injection.dart'; import '../injection.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import 'components/loading/loading_overlay.dart';
import 'router/app_router.dart'; import 'router/app_router.dart';
import 'router/app_router_observer.dart'; import 'router/app_router_observer.dart';
@ -27,20 +29,27 @@ class _AppWidgetState extends State<AppWidget> {
BlocProvider(create: (context) => getIt<LanguageBloc>()), BlocProvider(create: (context) => getIt<LanguageBloc>()),
BlocProvider(create: (context) => getIt<AuthBloc>()), BlocProvider(create: (context) => getIt<AuthBloc>()),
], ],
child: BlocBuilder<LanguageBloc, LanguageState>( child: GlobalLoaderOverlay(
builder: (context, state) { useDefaultLoading: false,
return MaterialApp.router( overlayWidgetBuilder: (progress) => LoadingOverlay(),
debugShowCheckedModeBanner: false, overlayColor: AppColor.black.withOpacity(0.35),
title: AppConstant.appName, child: BlocBuilder<LanguageBloc, LanguageState>(
locale: state.language.locale, builder: (context, state) {
supportedLocales: AppLocalizations.supportedLocales, return MaterialApp.router(
localizationsDelegates: AppLocalizations.localizationsDelegates, debugShowCheckedModeBanner: false,
theme: ThemeApp.theme, title: AppConstant.appName,
routerConfig: _appRouter.config( locale: state.language.locale,
navigatorObservers: () => <NavigatorObserver>[AppRouteObserver()], supportedLocales: AppLocalizations.supportedLocales,
), localizationsDelegates: AppLocalizations.localizationsDelegates,
); theme: ThemeApp.theme,
}, routerConfig: _appRouter.config(
navigatorObservers: () => <NavigatorObserver>[
AppRouteObserver(),
],
),
);
},
),
), ),
); );
} }

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import '../../../common/theme/theme.dart';
class LoadingOverlay extends StatelessWidget {
const LoadingOverlay({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColor.white,
borderRadius: BorderRadius.circular(AppValue.radius),
),
child: SpinKitCircle(color: AppColor.primary, size: 32.0),
),
);
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:math' as math; import 'dart:math' as math;
import '../../../../application/auth/auth_bloc.dart';
import '../../../../application/auth/login_form/login_form_bloc.dart'; import '../../../../application/auth/login_form/login_form_bloc.dart';
import '../../../../common/extension/extension.dart'; import '../../../../common/extension/extension.dart';
import '../../../../common/theme/theme.dart'; import '../../../../common/theme/theme.dart';
@ -108,6 +109,7 @@ class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
(f) => AppFlushbar.showAuthFailureToast(context, f), (f) => AppFlushbar.showAuthFailureToast(context, f),
(user) { (user) {
if (context.mounted) { if (context.mounted) {
context.read<AuthBloc>().add(AuthEvent.fetchCurrentUser());
context.router.replace(const MainRoute()); context.router.replace(const MainRoute());
} }
}, },

View File

@ -1,10 +1,16 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:loader_overlay/loader_overlay.dart';
import '../../../application/auth/logout_form/logout_form_bloc.dart';
import '../../../common/theme/theme.dart'; import '../../../common/theme/theme.dart';
import '../../../injection.dart';
import '../../components/button/button.dart'; import '../../components/button/button.dart';
import '../../components/spacer/spacer.dart'; import '../../components/spacer/spacer.dart';
import '../../components/toast/flushbar.dart';
import '../../router/app_router.gr.dart';
import 'widgets/account_info.dart'; import 'widgets/account_info.dart';
import 'widgets/app_setting.dart'; import 'widgets/app_setting.dart';
import 'widgets/business_setting.dart'; import 'widgets/business_setting.dart';
@ -13,88 +19,113 @@ import 'widgets/header.dart';
import 'widgets/support.dart'; import 'widgets/support.dart';
@RoutePage() @RoutePage()
class ProfilePage extends StatefulWidget { class ProfilePage extends StatelessWidget implements AutoRouteWrapper {
const ProfilePage({super.key}); const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return MultiBlocListener(
backgroundColor: AppColor.background, listeners: [
body: CustomScrollView( BlocListener<LogoutFormBloc, LogoutFormState>(
slivers: [ listener: (context, state) {
SliverAppBar( state.failureOrAuthOption.fold(
backgroundColor: AppColor.primary, () => null,
elevation: 0, (either) => either.fold(
pinned: true, (f) => AppFlushbar.showAuthFailureToast(context, f),
expandedHeight: 264.0, (_) => context.router.replace(const LoginRoute()),
flexibleSpace: LayoutBuilder( ),
builder: (BuildContext context, BoxConstraints constraints) { );
// Calculate the collapse ratio },
final double top = constraints.biggest.height; ),
final double collapsedHeight = BlocListener<LogoutFormBloc, LogoutFormState>(
MediaQuery.of(context).padding.top + kToolbarHeight; listenWhen: (previous, current) =>
final double expandedHeight = 264.0; previous.isSubmitting != current.isSubmitting,
final double shrinkRatio = listener: (context, state) {
((expandedHeight - top) / if (state.isSubmitting) {
(expandedHeight - collapsedHeight)) context.loaderOverlay.show();
.clamp(0.0, 1.0); } else {
context.loaderOverlay.hide();
}
},
),
],
child: Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
SliverAppBar(
backgroundColor: AppColor.primary,
elevation: 0,
pinned: true,
expandedHeight: 264.0,
flexibleSpace: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Calculate the collapse ratio
final double top = constraints.biggest.height;
final double collapsedHeight =
MediaQuery.of(context).padding.top + kToolbarHeight;
final double expandedHeight = 264.0;
final double shrinkRatio =
((expandedHeight - top) /
(expandedHeight - collapsedHeight))
.clamp(0.0, 1.0);
return FlexibleSpaceBar( return FlexibleSpaceBar(
background: ProfileHeader(), background: ProfileHeader(),
titlePadding: const EdgeInsets.only( titlePadding: const EdgeInsets.only(
left: 20, left: 20,
right: 12, right: 12,
bottom: 16, bottom: 16,
),
title: Opacity(
opacity: shrinkRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Profile',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
letterSpacing: -0.5,
color: AppColor.white,
),
),
ActionIconButton(
onTap: () {},
icon: LineIcons.userEdit,
),
],
), ),
), title: Opacity(
); opacity: shrinkRatio,
}, child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Profile',
style: AppStyle.xl.copyWith(
fontWeight: FontWeight.w700,
fontSize: 18,
letterSpacing: -0.5,
color: AppColor.white,
),
),
ActionIconButton(
onTap: () {},
icon: LineIcons.userEdit,
),
],
),
),
);
},
),
), ),
), SliverToBoxAdapter(
SliverToBoxAdapter( child: Column(
child: Column( children: [
children: [ const SpaceHeight(20),
const SpaceHeight(20), ProfileAccountInfo(),
ProfileAccountInfo(), const SpaceHeight(12),
const SpaceHeight(12), ProfileBusinessSetting(),
ProfileBusinessSetting(), const SpaceHeight(12),
const SpaceHeight(12), ProfileAppSetting(),
ProfileAppSetting(), const SpaceHeight(12),
const SpaceHeight(12), ProfileSupport(),
ProfileSupport(), const SpaceHeight(12),
const SpaceHeight(12), ProfileDangerZone(),
ProfileDangerZone(), const SpaceHeight(30),
const SpaceHeight(30), ],
], ),
), ),
), ],
], ),
), ),
); );
} }
@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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../application/auth/logout_form/logout_form_bloc.dart';
import '../../../../common/theme/theme.dart'; import '../../../../common/theme/theme.dart';
import '../../../components/spacer/spacer.dart'; import '../../../components/spacer/spacer.dart';
import 'profile_tile.dart'; import 'profile_tile.dart';
@ -49,7 +51,9 @@ class ProfileDangerZone extends StatelessWidget {
subtitle: 'Sign out from your account', subtitle: 'Sign out from your account',
iconColor: AppColor.error, iconColor: AppColor.error,
textColor: AppColor.error, textColor: AppColor.error,
onTap: () {}, onTap: () {
context.read<LogoutFormBloc>().add(LogoutFormEvent.submitted());
},
), ),
], ],
), ),

View File

@ -73,6 +73,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
back_button_interceptor:
dependency: transitive
description:
name: back_button_interceptor
sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a
url: "https://pub.dev"
source: hosted
version: "8.0.4"
bloc: bloc:
dependency: transitive dependency: transitive
description: description:
@ -741,6 +749,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "5.1.1"
loader_overlay:
dependency: "direct main"
description:
name: loader_overlay
sha256: "285c9ccab9a42a392ba948bd0b14376fd0ee9ddd7b63e3018bcd54460fd3e021"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:

View File

@ -39,6 +39,7 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
table_calendar: ^3.2.0 table_calendar: ^3.2.0
package_info_plus: ^8.3.1 package_info_plus: ^8.3.1
loader_overlay: ^5.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: