Xử lý login

This commit is contained in:
minhhieu2312 2026-02-27 10:14:14 +07:00
parent 9c67d123c8
commit 5d86cfa542
23 changed files with 4645 additions and 1982 deletions

View File

@ -1,5 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/core/constants/index.dart';
import 'package:baseproject/features/presentation/app/view/app.dart';
import 'package:baseproject/features/route/route_goto.dart';
import 'package:baseproject/features/usecases/index.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@ -8,8 +13,8 @@ class CustomInterceptor extends InterceptorsWrapper {
@override @override
// ignore: avoid_void_async // ignore: avoid_void_async
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// final String token = LocalStoreManager.getString(UserSettings.tokenUser); final String token = LocalStoreManager.getString(StorageKey.tokenUser);
// if (token.isNotEmpty) options.headers["Authorization"] = "Bearer $token"; if (token.isNotEmpty) options.headers["Authorization"] = "Bearer $token";
final String method = options.method.toLowerCase(); final String method = options.method.toLowerCase();
if (method == 'get' || method == 'put') { if (method == 'get' || method == 'put') {
@ -44,35 +49,42 @@ class CustomInterceptor extends InterceptorsWrapper {
@override @override
onError(DioError err, ErrorInterceptorHandler handler) async { onError(DioError err, ErrorInterceptorHandler handler) async {
if (retryCount >= 3) { if (retryCount < 3) {
return; await Future.delayed(Duration(milliseconds: retryCount * 500));
} if ((err.response?.statusCode == 403 || err.response?.statusCode == 401)) {
if (err.response?.statusCode == 403 || err.response?.statusCode == 401) { retryCount++;
retryCount++; final Dio dio = GetIt.I();
final Dio dio = GetIt.I(); final RequestOptions options = err.requestOptions;
dio.lock(); dio.lock();
dio.interceptors.requestLock.lock(); dio.interceptors.requestLock.lock();
dio.interceptors.responseLock.lock(); dio.interceptors.responseLock.lock();
//Refresh token // Refresh token
// final CoreUserRepository sessionRepository = GetIt.I(); final UserUseCases sessionRepository = GetIt.I();
// final Token? token = await sessionRepository.refreshToken( final bool token = await sessionRepository.tryRefreshToken();
// clientId: UserSettings.oidcClientId, refreshToken: LocalStoreManager.getString(UserSettings.refreshToken)); if (!token) {
// if (token == null) { dio.unlock();
// // final AuthenticateApp authenticateApp = GetIt.I(); dio.interceptors.requestLock.unlock();
// // await authenticateApp.authenticate(UserSettings.oidcClientId, <String>["profile", "email", "offline_access"]); dio.interceptors.responseLock.unlock();
// await Navigator.pushNamedAndRemoveUntil( // ignore: use_build_context_synchronously
// navigatorKey!.currentState!.context, vhs3LoginUser, (Route<dynamic> route) => false); gotoLogin(navigatorKey.currentState!.context);
// } else { } else {
// dio.unlock(); dio.unlock();
// dio.interceptors.requestLock.unlock(); dio.interceptors.requestLock.unlock();
// dio.interceptors.responseLock.unlock(); dio.interceptors.responseLock.unlock();
// options.headers = <String, dynamic>{ options.headers = <String, dynamic>{
// "Content-type": "application/json", "Content-type": "application/json",
// "Authorization": "Bearer ${LocalStoreManager.getString(UserSettings.tokenUser)}" "Authorization": "Bearer ${LocalStoreManager.getString(StorageKey.tokenUser)}"
// }; };
// await dio.fetch<dynamic>(options);
// } try {
final response = await dio.fetch<dynamic>(options);
return handler.resolve(response);
} catch (e) {
return handler.next(err);
}
}
}
} }
final dynamic errorData = err.response?.data; final dynamic errorData = err.response?.data;

View File

@ -8,10 +8,12 @@
import 'package:get_it/get_it.dart' as _i1; import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2; import 'package:injectable/injectable.dart' as _i2;
import '../../features/presentation/account/bloc/login_bloc.dart' as _i5; import '../../features/presentation/account/bloc/login_bloc.dart' as _i7;
import '../../features/repositories/hra_repository.dart' as _i4; import '../../features/presentation/app/bloc/user_bloc.dart' as _i3;
import '../../features/usecases/user_use_cases.dart' import '../../features/repositories/hra_repository.dart' as _i6;
as _i3; // ignore_for_file: unnecessary_lambdas import '../../features/usecases/index.dart' as _i4;
import '../../features/usecases/user/user_use_cases.dart'
as _i5; // ignore_for_file: unnecessary_lambdas
// ignore_for_file: lines_longer_than_80_chars // ignore_for_file: lines_longer_than_80_chars
/// initializes the registration of provided dependencies inside of [GetIt] /// initializes the registration of provided dependencies inside of [GetIt]
@ -25,8 +27,9 @@ _i1.GetIt $initGetIt(
environment, environment,
environmentFilter, environmentFilter,
); );
gh.lazySingleton<_i3.UserUseCases>( gh.factory<_i3.UserBloc>(() => _i3.UserBloc(get<_i4.UserUseCases>()));
() => _i3.UserUseCases(get<_i4.HraRepository>())); gh.lazySingleton<_i5.UserUseCases>(
gh.factory<_i5.LoginBloc>(() => _i5.LoginBloc(get<_i3.UserUseCases>())); () => _i5.UserUseCases(get<_i6.HraRepository>()));
gh.factory<_i7.LoginBloc>(() => _i7.LoginBloc(get<_i5.UserUseCases>()));
return get; return get;
} }

View File

@ -2,7 +2,6 @@ import 'package:baseproject/core/common/custom_interceptor.dart';
import 'package:baseproject/core/components/alice.dart'; import 'package:baseproject/core/components/alice.dart';
import 'package:baseproject/core/constants/index.dart'; import 'package:baseproject/core/constants/index.dart';
import 'package:baseproject/features/presentation/app/view/app.dart'; import 'package:baseproject/features/presentation/app/view/app.dart';
import 'package:baseproject/features/repositories/account_repository.dart';
import 'package:baseproject/features/repositories/hra_repository.dart'; import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';

View File

@ -17,4 +17,6 @@ export 'custom_pull_to_refresh.dart';
export 'date/date_time_picker.dart'; export 'date/date_time_picker.dart';
export 'alice.dart'; export 'alice.dart';
export 'tab/custom_tab.dart'; export 'tab/custom_tab.dart';
export 'switch/custom_switch_list_tile.dart'; export 'switch/custom_switch_list_tile.dart';
export 'text_field/custom_textfield.dart';
export 'text_field/text_field_password.dart';

View File

@ -0,0 +1,225 @@
import 'package:baseproject/core/components/form/form_builder_field.dart';
import 'package:baseproject/core/components/form/form_control.dart';
import 'package:baseproject/core/theme/custom_color.dart';
import 'package:baseproject/core/theme/text_style.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef ItemToString<T> = String Function(T item);
typedef ItemFromString<T> = T? Function(String string);
class CustomTextField<T> extends FormBuilderField<T> {
final FormFieldValidator<T?>? validator;
final FormFieldSetter<T?>? onSaved;
final TextEditingController? controller;
final T? initialValue;
final bool autofocus;
final int? maxLines;
final int? maxLength;
final bool enabled;
final ItemFromString<T?>? itemFromString;
final ItemToString<T?>? itemToString;
final bool isLabelTop;
final String labelText;
final TextStyle? labelTextStyle;
final GestureTapCallback? onTap;
final int? minLines;
final double borderRadius;
final InputDecoration decoration;
final bool enableSuggestions;
final TextInputType? textInputType;
final TextStyle? style;
final TextAlign textAlign;
final bool readOnly;
final ValueChanged<String>? onFieldSubmitted;
List<TextInputFormatter>? inputFormatters;
final TextInputAction? textInputAction;
final bool obscureText;
final bool autoCorrect;
final TextCapitalization textCapitalization;
final bool isShowTextRequire;
final Color? cursorColor;
final Color? borderColor;
CustomTextField({
Key? key,
String name = 'textField',
this.validator,
this.onSaved,
ValueChanged<T?>? onChanged,
this.initialValue,
this.autofocus = false,
this.maxLength,
this.enabled = true,
this.itemFromString,
this.itemToString,
this.isLabelTop = false,
this.labelText = "",
this.labelTextStyle,
//TextFormField
this.controller,
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
this.decoration = const InputDecoration(
//border: OutlineInputBorder(),
),
this.maxLines,
this.minLines,
this.borderRadius = 16,
this.onTap,
this.enableSuggestions = false,
this.textInputType,
this.style,
this.textAlign = TextAlign.start,
this.readOnly = false,
this.inputFormatters,
FocusNode? focusNode,
this.onFieldSubmitted,
this.textInputAction,
this.obscureText = false,
this.autoCorrect = false,
this.textCapitalization = TextCapitalization.sentences,
this.isShowTextRequire = false,
this.cursorColor,
this.borderColor,
}) :
//: controller = controller ?? TextEditingController(text: _toString<T>(initialValue, itemToString)),
// ??
// InputDecoration(
// border: OutlineInputBorder(borderRadius: BorderRadius.circular(borderRadius)),
// focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(borderRadius)),
// // enabledBorder: InputBorder.none,
// // errorBorder: InputBorder.none,
// // disabledBorder: InputBorder.none,
// ),
super(
name: name,
key: key,
autovalidateMode: autovalidateMode,
onChanged: onChanged,
focusNode: focusNode,
initialValue: initialValue,
decoration: decoration,
builder: (FormFieldState<T?> field) {
final _TextFieldState<T> state = field as _TextFieldState<T>;
//return state.build(state.context);
if (isLabelTop) return state._buildFieldSet();
return state._buildTextFormField();
});
@override
_TextFieldState<T> createState() => _TextFieldState<T>();
}
class _TextFieldState<T> extends FormBuilderFieldState<CustomTextField<T>, T> {
late TextEditingController textController;
@override
void dispose() {
textController.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
textController = widget.controller ?? TextEditingController();
textController.text = initialText;
// widget.decoration = widget.decoration.copyWith(
// border: OutlineInputBorder(
// borderRadius: BorderRadius.all(
// Radius.circular(widget.borderRadius),
// )),
// enabledBorder: OutlineInputBorder(
// borderRadius: BorderRadius.all(
// Radius.circular(widget.borderRadius),
// )),
//);
}
// @override
// Widget build(BuildContext context) {
// if (widget.isLabelTop)
// return _buildFieldSet();
// else
// return _buildTextFormField();
// }
@override
void reset() {
super.reset();
textController.text = initialText;
}
String get initialText => widget.itemToString?.call(initialValue) ?? initialValue?.toString() ?? '';
Widget _buildTextFormField() {
return TextFormField(
autocorrect: widget.autoCorrect,
onFieldSubmitted: widget.onFieldSubmitted,
textInputAction: widget.textInputAction,
focusNode: effectiveFocusNode,
enableSuggestions: true,
//widget.enableSuggestions,
textCapitalization: widget.textCapitalization,
controller: textController,
decoration: widget.decoration.copyWith(
labelText: !widget.isLabelTop ? widget.labelText : (widget.decoration.labelText),
labelStyle: !widget.isLabelTop ? labelTextStyle : null,
// fillColor: widget.fillColor ?? CustomColor.bgGrayLight,
// filled: true,
),
keyboardType: widget.textInputType,
style: widget.style,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscureText: widget.obscureText,
//autocorrect: widget.autocorrect,
//maxLengthEnforcement: widget.maxLengthEnforcement,
maxLines: widget.maxLines,
maxLength: widget.maxLength,
//scrollPadding: widget.scrollPadding,
//textCapitalization: widget.textCapitalization,
inputFormatters: widget.inputFormatters,
minLines: widget.minLines,
enabled: widget.enabled,
readOnly: widget.readOnly,
autovalidateMode: widget.autovalidateMode,
cursorColor: widget.cursorColor,
//initialValue: widget.controller == null ? initialText : null,
onChanged: (String value) {
didChange(_toObject<T>(value, widget.itemFromString));
},
validator: (String? value) {
if (widget.validator != null) {
return widget.validator!(_toObject<T>(value, widget.itemFromString));
}
},
onTap: widget.onTap,
onSaved: (String? value) {
if (widget.onSaved != null) {
return widget.onSaved!(_toObject<T>(value, widget.itemFromString));
}
},
);
}
TextStyle get labelTextStyle => textStyleBodySmall.copyWith(color: CustomColor.textGray);
Widget _buildFieldSet() {
return FormControl(
child: _buildTextFormField(),
isShowTextRequire: widget.isShowTextRequire,
labelText: widget.labelText,
labelTextStyle: widget.labelTextStyle,
);
}
}
String _toString<T>(T? value, ItemToString<T?>? fn) => (fn == null ? value?.toString() : fn(value)) ?? '';
T? _toObject<T>(String? s, ItemFromString<T?>? fn) => fn == null ? s as T : fn(s ?? '');

View File

@ -0,0 +1,56 @@
import 'package:baseproject/core/components/index.dart';
import 'package:baseproject/core/theme/form_theme.dart';
import 'package:flutter/material.dart';
class TextFieldPassword extends StatefulWidget {
const TextFieldPassword({
Key? key,
this.labelText = "",
this.name = "TextFieldPassword",
this.validator,
this.onChanged,
}) : super(key: key);
final String labelText;
final String name;
final FormFieldValidator<String?>? validator;
final ValueChanged<String?>? onChanged;
@override
State<TextFieldPassword> createState() => _TextFieldPasswordState();
}
class _TextFieldPasswordState extends State<TextFieldPassword> {
late bool _passwordVisible;
@override
void initState() {
super.initState();
_passwordVisible = false;
}
@override
Widget build(BuildContext context) {
return CustomTextField(
textCapitalization: TextCapitalization.none,
isShowTextRequire: true,
isLabelTop: true,
name: widget.name,
validator: widget.validator,
labelText: widget.labelText,
obscureText: !_passwordVisible,
maxLines: 1,
onChanged: widget.onChanged,
decoration: FormTheme.getInputDecoration().copyWith(
suffixIcon: IconButton(
icon: Icon(
_passwordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
),
),
);
}
}

View File

@ -1,3 +1,6 @@
class StorageKey { class StorageKey {
static const String libraryKeywordHistoryKey = 'libraryKeywordHistoryKey'; static const String libraryKeywordHistoryKey = 'libraryKeywordHistoryKey';
static const String userInfo = 'USER_INFO';
static const String tokenUser = 'TOKEN_USER';
static const String refreshToken = 'REFRESH_TOKEN';
} }

View File

@ -1,18 +1,19 @@
import 'package:baseproject/core/common/bloc/bloc_index.dart'; import 'package:baseproject/core/common/bloc/bloc_index.dart';
import 'package:baseproject/core/common/index.dart'; import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/features/presentation/app/bloc/user_bloc.dart';
import 'package:baseproject/features/repositories/hra_repository.dart'; import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:baseproject/features/usecases/user_use_cases.dart'; import 'package:baseproject/features/route/route_goto.dart';
import 'package:baseproject/features/usecases/user/user_use_cases.dart';
import 'package:flutter/material.dart';
class LoginViewModel { class LoginViewModel {
LoginViewModel({ LoginViewModel({
this.isLoading = false, this.isLoading = false,
this.errorMessage,
this.loginResponse, this.loginResponse,
this.captcha, this.captcha,
}); });
final bool isLoading; final bool isLoading;
final String? errorMessage;
final LoginResponseDto? loginResponse; final LoginResponseDto? loginResponse;
final DNTCaptchaApiResponse? captcha; final DNTCaptchaApiResponse? captcha;
@ -24,7 +25,6 @@ class LoginViewModel {
}) { }) {
return LoginViewModel( return LoginViewModel(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
loginResponse: loginResponse ?? this.loginResponse, loginResponse: loginResponse ?? this.loginResponse,
captcha: captcha ?? this.captcha, captcha: captcha ?? this.captcha,
); );
@ -33,7 +33,7 @@ class LoginViewModel {
class LoginBloc extends BaseCubit<BaseStateBloc<LoginViewModel>> { class LoginBloc extends BaseCubit<BaseStateBloc<LoginViewModel>> {
LoginBloc(this._userUseCases) : super(InitState<LoginViewModel>(LoginViewModel())) { LoginBloc(this._userUseCases) : super(InitState<LoginViewModel>(LoginViewModel())) {
loadCaptcha(); // loadCaptcha();
} }
final UserUseCases _userUseCases; final UserUseCases _userUseCases;
@ -61,7 +61,7 @@ class LoginBloc extends BaseCubit<BaseStateBloc<LoginViewModel>> {
); );
} }
Future<void> login(LoginDto request) async { Future<void> login(LoginDto request, BuildContext context) async {
final currentModel = state.model; final currentModel = state.model;
emit( emit(
@ -71,30 +71,22 @@ class LoginBloc extends BaseCubit<BaseStateBloc<LoginViewModel>> {
); );
final result = await _userUseCases.loginAccount(request); final result = await _userUseCases.loginAccount(request);
emit(
LoadedState<LoginViewModel>(
currentModel.copyWith(
isLoading: false,
errorMessage: null,
),
),
);
result.fold( result.fold(
(error) { (error) {
showErrorMessage(error); showErrorMessage(error);
emit( // loadCaptcha();
ErrorState<LoginViewModel>(
currentModel.copyWith(
isLoading: false,
errorMessage: error,
),
),
);
loadCaptcha();
}, },
(response) { (response) {
emit( BlocProvider.of<UserBloc>(context).updateUserInfo(response.userInfo ?? UserInfoDto());
LoadedState<LoginViewModel>( gotoHome(context);
currentModel.copyWith(
isLoading: false,
errorMessage: null,
loginResponse: response,
),
),
);
}, },
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/core/components/index.dart'; import 'package:baseproject/core/components/index.dart';
import 'package:baseproject/features/presentation/account/bloc/login_bloc.dart'; import 'package:baseproject/features/presentation/account/bloc/login_bloc.dart';
import 'package:baseproject/features/repositories/hra_repository.dart'; import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -36,7 +37,7 @@ class _LoginScreenState extends State<LoginScreen> {
dto.captchaToken = captcha?.dntCaptchaTokenValue; dto.captchaToken = captcha?.dntCaptchaTokenValue;
dto.captchaInputText = "999999"; dto.captchaInputText = "999999";
_loginBloc.login(dto); _loginBloc.login(dto, context);
} }
@override @override
@ -51,128 +52,40 @@ class _LoginScreenState extends State<LoginScreen> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: FormBuilder( child: FormBuilder(
initialValue: kDebugMode
? {
'userName': 'quylx',
'password': 'BearCMS0011002848238master',
}
: {},
key: _formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
child: BlocBuilder<LoginBloc, BaseStateBloc<LoginViewModel>>( child: BlocBuilder<LoginBloc, BaseStateBloc<LoginViewModel>>(
builder: (context, state) { builder: (context, state) {
final vm = state.model; final vm = state.model;
final isLoading = state is LoadingState<LoginViewModel> || vm.isLoading; final isLoading = state is LoadingState<LoginViewModel> || vm.isLoading;
final error = vm.errorMessage;
final captcha = vm.captcha;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
if (error != null) CustomTextField(
Padding( name: 'userName',
padding: const EdgeInsets.only(bottom: 12),
child: Text(
error,
style: const TextStyle(color: Colors.red),
),
),
FormControl(
labelText: 'Tên đăng nhập', labelText: 'Tên đăng nhập',
isLabelTop: true,
isShowTextRequire: true, isShowTextRequire: true,
child: TextFormField( validator: FormBuilderValidators.required<String>(
decoration: const InputDecoration( context,
border: OutlineInputBorder(),
hintText: 'Nhập tên đăng nhập',
),
validator: FormBuilderValidators.required<String>(
context,
),
onSaved: (value) {
_formKey.currentState?.setInternalFieldValue(
'userName',
value,
isUpdateState: false,
);
},
), ),
), ),
const SizedBox(height: 16), ConstantWidget.heightSpace16,
FormControl( TextFieldPassword(
name: 'password',
labelText: 'Mật khẩu', labelText: 'Mật khẩu',
isShowTextRequire: true, validator: FormBuilderValidators.required<String>(
child: TextFormField( context,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Nhập mật khẩu',
),
validator: FormBuilderValidators.required<String>(
context,
),
onSaved: (value) {
_formKey.currentState?.setInternalFieldValue(
'password',
value,
isUpdateState: false,
);
},
), ),
), ),
// const SizedBox(height: 8), ConstantWidget.heightSpace24,
// if (captcha != null) ...[
// const SizedBox(height: 16),
// GestureDetector(
// onTap: () {
// _loginBloc.loadCaptcha();
// },
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const Text('Captcha'),
// const SizedBox(height: 8),
// Image.network(
// captcha.dntCaptchaImgUrl ?? '',
// height: 60,
// errorBuilder: (_, __, ___) => Text(
// captcha.dntCaptchaImgUrl ?? '',
// style: const TextStyle(color: Colors.blue),
// ),
// ),
// ],
// ),
// ),
// const SizedBox(height: 8),
// FormControl(
// labelText: 'Nhập mã captcha',
// isShowTextRequire: true,
// child: TextFormField(
// decoration: const InputDecoration(
// border: OutlineInputBorder(),
// hintText: 'Nhập mã captcha',
// ),
// validator: FormBuilderValidators.required<String>(
// context,
// ),
// onSaved: (value) {
// _formKey.currentState?.setInternalFieldValue(
// 'captchaInput',
// value,
// isUpdateState: false,
// );
// },
// ),
// ),
// ],
const SizedBox(height: 8),
Row(
children: <Widget>[
Checkbox(
value: _rememberMe,
onChanged: (v) {
setState(() {
_rememberMe = v ?? false;
});
},
),
const Text('Ghi nhớ đăng nhập'),
],
),
const SizedBox(height: 24),
SizedBox( SizedBox(
height: 48, height: 48,
child: ConstantWidget.buildPrimaryButton( child: ConstantWidget.buildPrimaryButton(

View File

@ -0,0 +1,30 @@
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:baseproject/features/usecases/index.dart';
import 'package:baseproject/features/usecases/user/user_use_cases.dart';
class UserBloc extends BaseCubit<BaseStateBloc<UserInfoDto?>> {
UserBloc(this._userUseCases) : super(InitState<UserInfoDto?>(null));
final UserUseCases _userUseCases;
Future<bool> getUserInfo() async {
final resultRefreshToken = await _userUseCases.refreshToken();
if (resultRefreshToken.isRight()) {
final resultUserInfo = await _userUseCases.getUserInfoFromApi();
emit(LoadedState<UserInfoDto>(resultUserInfo.fold((l) => UserInfoDto(), (r) => r)));
} else {
emit(LoadedState<UserInfoDto?>(null));
}
return true;
}
updateUserInfo(UserInfoDto userInfo) {
emit(LoadedState<UserInfoDto>(userInfo));
}
logout() {
emit(LoadedState<UserInfoDto?>(null));
_userUseCases.clearData();
}
}

View File

@ -1,6 +1,8 @@
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/core/components/alice.dart'; import 'package:baseproject/core/components/alice.dart';
import 'package:baseproject/core/language/app_localizations.dart'; import 'package:baseproject/core/language/app_localizations.dart';
import 'package:baseproject/core/theme/custom_theme.dart'; import 'package:baseproject/core/theme/custom_theme.dart';
import 'package:baseproject/features/presentation/app/bloc/user_bloc.dart';
import 'package:baseproject/features/route/route_const.dart'; import 'package:baseproject/features/route/route_const.dart';
import 'package:baseproject/features/route/route_generator.dart'; import 'package:baseproject/features/route/route_generator.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -18,55 +20,59 @@ class App extends StatefulWidget {
} }
class _AppState extends State<App> { class _AppState extends State<App> {
final UserBloc _userBloc = getItSuper<UserBloc>();
String _getLanguage() { String _getLanguage() {
return 'vi'; return 'vi';
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return BlocProvider(
// navigatorObservers: [CustomNavigatorObserver()], create: (context) => _userBloc,
debugShowCheckedModeBanner: false, child: MaterialApp(
theme: getTheme(context, true), // navigatorObservers: [CustomNavigatorObserver()],
// navigatorKey: navigatorKey, debugShowCheckedModeBanner: false,
locale: Locale(_getLanguage()), theme: getTheme(context, true),
supportedLocales: AppLocalizations.locales, // navigatorKey: navigatorKey,
localizationsDelegates: <LocalizationsDelegate<dynamic>>[ locale: Locale(_getLanguage()),
AppLocalizations.delegate, supportedLocales: AppLocalizations.locales,
GlobalMaterialLocalizations.delegate, localizationsDelegates: <LocalizationsDelegate<dynamic>>[
GlobalWidgetsLocalizations.delegate, AppLocalizations.delegate,
GlobalCupertinoLocalizations.delegate GlobalMaterialLocalizations.delegate,
], GlobalWidgetsLocalizations.delegate,
navigatorKey: navigatorKey, GlobalCupertinoLocalizations.delegate
localeResolutionCallback: AppLocalizations.localeResolutionCallback, ],
initialRoute: appInitRouteName, navigatorKey: navigatorKey,
onGenerateRoute: RouteGenerator.generatorRoute, localeResolutionCallback: AppLocalizations.localeResolutionCallback,
builder: EasyLoading.init(builder: (BuildContext context, Widget? child) { initialRoute: appInitRouteName,
EasyLoading.instance.userInteractions = false; onGenerateRoute: RouteGenerator.generatorRoute,
return Container( builder: EasyLoading.init(builder: (BuildContext context, Widget? child) {
child: kDebugMode EasyLoading.instance.userInteractions = false;
? Stack( return Container(
children: <Widget>[ child: kDebugMode
child!, ? Stack(
Positioned( children: <Widget>[
bottom: 10, child!,
right: 10, Positioned(
child: Container( bottom: 10,
width: 30, right: 10,
height: 30, child: Container(
child: FloatingActionButton( width: 30,
onPressed: () { height: 30,
CustomAlice.showScreen(); child: FloatingActionButton(
}, onPressed: () {
backgroundColor: Colors.red, CustomAlice.showScreen();
child: const Text("A"), },
backgroundColor: Colors.red,
child: const Text("A"),
),
), ),
), ),
), ],
], )
) : child!,
: child!, );
); })),
})); );
} }
} }

View File

@ -0,0 +1,38 @@
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/features/presentation/app/bloc/user_bloc.dart';
import 'package:baseproject/features/route/route_goto.dart';
import 'package:baseproject/features/usecases/index.dart';
import 'package:flutter/material.dart';
class InitScreen extends StatefulWidget {
const InitScreen({super.key});
@override
State<InitScreen> createState() => _InitScreenState();
}
class _InitScreenState extends State<InitScreen> {
UserBloc get _userBloc => BlocProvider.of<UserBloc>(context);
@override
void initState() {
super.initState();
initData();
}
void initData() {
_userBloc.getUserInfo().then((value) {
if (value) {
gotoHome(context);
}
});
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}

View File

@ -1,5 +1,8 @@
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/core/components/constants_widget.dart'; import 'package:baseproject/core/components/constants_widget.dart';
import 'package:baseproject/core/language/app_localizations.dart'; import 'package:baseproject/core/language/app_localizations.dart';
import 'package:baseproject/features/model/index.dart';
import 'package:baseproject/features/presentation/app/bloc/user_bloc.dart';
import 'package:baseproject/features/route/route_goto.dart'; import 'package:baseproject/features/route/route_goto.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,18 +18,39 @@ class _HomeState extends State<Home> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Center( body: Center(
child: Column( child: BlocBuilder<UserBloc, BaseStateBloc<UserInfoDto?>>(
mainAxisAlignment: MainAxisAlignment.center, builder: (context, state) {
crossAxisAlignment: CrossAxisAlignment.center, final userInfo = state.model;
children: [ if (userInfo != null) {
Text(AppLocalizations.of(context)!.translate("first_string")), return Column(
ConstantWidget.buildPrimaryButton( mainAxisAlignment: MainAxisAlignment.center,
onPressed: () { crossAxisAlignment: CrossAxisAlignment.center,
gotoLogin(context); children: [
}, Text("Chào ${userInfo.fullName ?? ''}"),
text: 'Đăng nhập', ConstantWidget.buildPrimaryButton(
), onPressed: () {
], BlocProvider.of<UserBloc>(context).logout();
},
text: 'Đăng xuất',
),
],
);
} else {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(AppLocalizations.of(context)!.translate("first_string")),
ConstantWidget.buildPrimaryButton(
onPressed: () {
gotoLogin(context);
},
text: 'Đăng nhập',
),
],
);
}
},
), ),
), ),
); );

View File

@ -69,7 +69,8 @@ abstract class HraRepository {
/// ///
@POST('/api/v1/account/refresh-token') @POST('/api/v1/account/refresh-token')
Future<RefreshTokenResponseDtoApiResponse> accountRefreshToken(); Future<RefreshTokenResponseDtoApiResponse> accountRefreshToken(
@Body() RefreshTokenRequestDto body);
/// ///
@POST('/api/v1/account/login-with-2fa') @POST('/api/v1/account/login-with-2fa')

View File

@ -254,11 +254,12 @@ class _HraRepository implements HraRepository {
} }
@override @override
Future<RefreshTokenResponseDtoApiResponse> accountRefreshToken() async { Future<RefreshTokenResponseDtoApiResponse> accountRefreshToken(body) async {
const _extra = <String, dynamic>{}; const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{}; final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{}; final _headers = <String, dynamic>{};
final _data = <String, dynamic>{}; final _data = <String, dynamic>{};
_data.addAll(body.toJson());
final _result = await _dio.fetch<Map<String, dynamic>>( final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<RefreshTokenResponseDtoApiResponse>(Options( _setStreamType<RefreshTokenResponseDtoApiResponse>(Options(
method: 'POST', method: 'POST',

View File

@ -12641,6 +12641,22 @@ class RefreshTokenEntity {
Map<String, dynamic> toJson() => _$RefreshTokenEntityToJson(this); Map<String, dynamic> toJson() => _$RefreshTokenEntityToJson(this);
} }
@JsonSerializable(explicitToJson: true)
class RefreshTokenRequestDto {
RefreshTokenRequestDto({
this.refreshToken,
});
factory RefreshTokenRequestDto.fromJson(Map<String, dynamic> json) =>
_$RefreshTokenRequestDtoFromJson(json);
@JsonKey(name: 'refreshToken', includeIfNull: true)
String? refreshToken;
static const fromJsonFactory = _$RefreshTokenRequestDtoFromJson;
static const toJsonFactory = _$RefreshTokenRequestDtoToJson;
Map<String, dynamic> toJson() => _$RefreshTokenRequestDtoToJson(this);
}
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class RefreshTokenResponseDto { class RefreshTokenResponseDto {
RefreshTokenResponseDto({ RefreshTokenResponseDto({

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,3 @@
const String appInitRouteName = '/app_init'; const String appInitRouteName = '/app_init';
const String loginRouteName = '/login'; const String loginRouteName = '/login';
const String homeApp = '/home_app';

View File

@ -1,4 +1,5 @@
import 'package:baseproject/features/presentation/account/login_screen.dart'; import 'package:baseproject/features/presentation/account/login_screen.dart';
import 'package:baseproject/features/presentation/app/view/init_screen.dart';
import 'package:baseproject/features/presentation/home/view/home.dart'; import 'package:baseproject/features/presentation/home/view/home.dart';
import 'package:baseproject/features/route/route_const.dart'; import 'package:baseproject/features/route/route_const.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -9,6 +10,8 @@ class RouteGenerator {
// tracking vào màn // tracking vào màn
switch (setting.name) { switch (setting.name) {
case appInitRouteName: case appInitRouteName:
return MaterialPageRoute<void>(settings: setting, builder: (_) => const InitScreen());
case homeApp:
return MaterialPageRoute<void>(settings: setting, builder: (_) => const Home()); return MaterialPageRoute<void>(settings: setting, builder: (_) => const Home());
case loginRouteName: case loginRouteName:
return MaterialPageRoute<void>(settings: setting, builder: (_) => const LoginScreen()); return MaterialPageRoute<void>(settings: setting, builder: (_) => const LoginScreen());

View File

@ -4,3 +4,11 @@ import 'route_const.dart';
void gotoLogin(BuildContext context) { void gotoLogin(BuildContext context) {
Navigator.pushNamed(context, loginRouteName); Navigator.pushNamed(context, loginRouteName);
} }
void gotoHome(BuildContext context) {
try {
Navigator.pushNamedAndRemoveUntil(context, homeApp, (Route<dynamic> route) => false);
} catch (e) {
Navigator.pushReplacementNamed(context, homeApp);
}
}

View File

@ -0,0 +1 @@
export 'user/user_use_cases.dart';

View File

@ -0,0 +1,115 @@
import 'package:baseproject/core/common/index.dart';
import 'package:baseproject/core/constants/index.dart';
import 'package:baseproject/core/extension/string_extension.dart';
import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
@lazySingleton
class UserUseCases {
final HraRepository _hraRepository;
UserUseCases(this._hraRepository);
Future<Either<String, LoginResponseDto>> loginAccount(LoginDto request) async {
try {
final result = await _hraRepository.accountLoginMobile(request);
if (result.data == null || result.success == false) {
return Left<String, LoginResponseDto>(result.message ?? 'Login failed');
}
await saveToken(result.data?.token ?? '', refreshToken: result.data?.refreshToken ?? '');
await saveUserInfo(result.data?.userInfo);
return Right<String, LoginResponseDto>(result.data!);
} catch (ex) {
return Left<String, LoginResponseDto>(ex.toString());
}
}
Future<void> saveToken(String token, {String? refreshToken}) async {
await LocalStoreManager.setString(StorageKey.tokenUser, token);
if (refreshToken != null) {
await LocalStoreManager.setString(StorageKey.refreshToken, refreshToken);
}
}
Future<void> saveUserInfo(UserInfoDto? userInfo) async {
if (userInfo != null) {
await LocalStoreManager.setObject(StorageKey.userInfo, userInfo.toJson());
}
}
UserInfoDto? getUserInfo() {
final Map<String, dynamic>? temp = LocalStoreManager.getObject(StorageKey.userInfo);
if (temp != null) return UserInfoDto.fromJson(temp);
return null;
}
int getCurrentUserId() {
return getUserInfo()?.id?.toInt() ?? 0;
}
Future<void> clearData() async {
await LocalStoreManager.remove(StorageKey.refreshToken);
await LocalStoreManager.remove(StorageKey.tokenUser);
await LocalStoreManager.remove(StorageKey.userInfo);
}
Future<Either<String, DNTCaptchaApiResponse>> getCaptcha() async {
try {
final result = await _hraRepository.accountCaptcha();
return Right<String, DNTCaptchaApiResponse>(result);
} catch (ex) {
return Left<String, DNTCaptchaApiResponse>(ex.toString());
}
}
Future<Either<String, bool>> refreshToken() async {
final refreshToken = LocalStoreManager.getString(StorageKey.refreshToken);
if (refreshToken.isNullOrEmpty) {
return const Left<String, bool>('Refresh token not found');
}
try {
final result = await _hraRepository.accountRefreshToken(
RefreshTokenRequestDto(refreshToken: LocalStoreManager.getString(StorageKey.refreshToken)));
if (result.data != null) {
await saveToken(result.data?.token ?? '');
}
return Right<String, bool>(result.success ?? false);
} catch (ex) {
await clearData();
return Left<String, bool>(ex.toString());
}
}
Future<Either<String, UserInfoDto>> getUserInfoFromApi() async {
try {
final result = await _hraRepository.accountUserInfo();
await saveUserInfo(result);
return Right<String, UserInfoDto>(result);
} catch (ex) {
await clearData();
return Left<String, UserInfoDto>(ex.toString());
}
}
Future<bool> tryRefreshToken() async {
try {
RefreshTokenRequestDto requestModel =
RefreshTokenRequestDto(refreshToken: LocalStoreManager.getString(StorageKey.refreshToken));
final RefreshTokenResponseDtoApiResponse? token =
await HraRepository(Dio(), baseUrl: ApiPath.hra).accountRefreshToken(requestModel);
if (token != null) {
await saveToken(token.data?.token ?? '');
}
return token != null;
} catch (e) {
return false;
}
}
}

View File

@ -1,34 +0,0 @@
import 'package:baseproject/features/repositories/hra_repository.dart';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
@lazySingleton
class UserUseCases {
final HraRepository _hraRepository;
UserUseCases(this._hraRepository);
Future<Either<String, LoginResponseDto>> loginAccount(LoginDto request) async {
try {
final result = await _hraRepository.accountLoginMobile(request);
if (result.data == null) {
return Left<String, LoginResponseDto>(result.message ?? 'Login failed');
}
return Right<String, LoginResponseDto>(result.data!);
} catch (ex) {
return Left<String, LoginResponseDto>(ex.toString());
}
}
Future<Either<String, DNTCaptchaApiResponse>> getCaptcha() async {
try {
final result = await _hraRepository.accountCaptcha();
return Right<String, DNTCaptchaApiResponse>(result);
} catch (ex) {
return Left<String, DNTCaptchaApiResponse>(ex.toString());
}
}
}