From 303ff86d8567c50bb5704cd00de7d14cc0f221db Mon Sep 17 00:00:00 2001 From: "mr.mojtaba" Date: Sat, 17 May 2025 15:24:06 +0330 Subject: [PATCH] feat : login api call --- .../pages/system_design/system_design.dart | 6 +- .../lib/data/common/dio_error_handler.dart | 53 ++++ packages/auth/lib/data/di/auth_di.dart | 6 +- .../login_request/login_request_model.dart | 15 +- .../repositories/auth_repository_imp.dart | 77 ++--- .../data/services/token_storage_service.dart | 2 +- .../lib/presentation/pages/auth/logic.dart | 58 +++- .../lib/presentation/pages/auth/view.dart | 287 +++++++----------- .../auth/lib/presentation/routes/pages.dart | 4 +- .../presentation/widget/captcha/logic.dart | 7 +- .../lib/presentation/widget/captcha/view.dart | 107 +++---- .../lib/presentation/widget/clear_button.dart | 2 +- .../lib/presentation/widget/logo_widget.dart | 20 ++ packages/core/lib/core.dart | 5 +- .../lib/infrastructure/remote/dio_remote.dart | 6 +- .../presentation/widget/buttons/elevated.dart | 56 ++-- .../presentation/widget/inputs/r_input.dart | 277 +++++++---------- packages/core/lib/utils/safe_call_utils.dart | 6 +- .../add_mobile_inspector/view.dart | 4 + .../presentation/add_supervision/view.dart | 12 +- .../display_information/view.dart | 15 +- .../registration_of_violation/view.dart | 15 +- 22 files changed, 518 insertions(+), 522 deletions(-) create mode 100644 packages/auth/lib/data/common/dio_error_handler.dart create mode 100644 packages/auth/lib/presentation/widget/logo_widget.dart diff --git a/lib/presentation/pages/system_design/system_design.dart b/lib/presentation/pages/system_design/system_design.dart index f53eb75..bacf390 100644 --- a/lib/presentation/pages/system_design/system_design.dart +++ b/lib/presentation/pages/system_design/system_design.dart @@ -67,11 +67,13 @@ class _SystemDesignPageState extends State { child: Column( spacing: 14, children: [ - RTextField( + RTextField( + controller: TextEditingController(), hintText: 'حجم کشتار را در روز به قطعه وارد کنید', hintStyle: AppFonts.yekan13, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'تلفن مرغداری', labelStyle: AppFonts.yekan10, ), diff --git a/packages/auth/lib/data/common/dio_error_handler.dart b/packages/auth/lib/data/common/dio_error_handler.dart new file mode 100644 index 0000000..963707c --- /dev/null +++ b/packages/auth/lib/data/common/dio_error_handler.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; + +class DioErrorHandler { + void handle(DioException error) { + switch (error.response?.statusCode) { + case 401: + _handle401(); + break; + case 403: + _handle403(); + break; + default: + _handleGeneric(error); + } + } + + //wrong password/user name => "detail": "No active account found with the given credentials" - 401 + void _handle401() { + Get.showSnackbar( + _errorSnackBar('نام کاربری یا رمز عبور اشتباه است'), + ); + } + + //wrong captcha => "detail": "Captcha code is incorrect" - 403 + void _handle403() { + Get.showSnackbar( + _errorSnackBar('کد امنیتی اشتباه است'), + ); + } + + void _handleGeneric(DioException error) { + // General error handling + } + + GetSnackBar _errorSnackBar(String message) { + return GetSnackBar( + titleText: Text( + 'خطا', + style: AppFonts.yekan14.copyWith(color: Colors.white), + ), + messageText: Text( + message, + style: AppFonts.yekan12.copyWith(color: Colors.white), + ), + backgroundColor: AppColor.error, + margin: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + borderRadius: 12, + duration: Duration(milliseconds: 3500), + snackPosition: SnackPosition.TOP, + ); + } +} diff --git a/packages/auth/lib/data/di/auth_di.dart b/packages/auth/lib/data/di/auth_di.dart index 391647f..d654ef1 100644 --- a/packages/auth/lib/data/di/auth_di.dart +++ b/packages/auth/lib/data/di/auth_di.dart @@ -1,4 +1,5 @@ import 'package:rasadyar_auth/data/common/constant.dart'; +import 'package:rasadyar_auth/data/common/dio_error_handler.dart'; import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart'; import 'package:rasadyar_auth/data/services/auth_service.dart'; import 'package:rasadyar_auth/data/services/token_storage_service.dart'; @@ -16,6 +17,7 @@ Future setupAuthDI() async { diAuth.registerCachedFactory( () => AuthRepositoryImpl(dioRemote), ); - diAuth.registerLazySingleton(() => AuthService()); - diAuth.registerLazySingleton(() => TokenStorageService()); + diAuth.registerLazySingleton(() => AuthService()); + diAuth.registerLazySingleton(() => TokenStorageService()); + diAuth.registerLazySingleton(() => DioErrorHandler()); } diff --git a/packages/auth/lib/data/models/request/login_request/login_request_model.dart b/packages/auth/lib/data/models/request/login_request/login_request_model.dart index 3794525..0e76bc2 100644 --- a/packages/auth/lib/data/models/request/login_request/login_request_model.dart +++ b/packages/auth/lib/data/models/request/login_request/login_request_model.dart @@ -12,10 +12,23 @@ abstract class LoginRequestModel with _$LoginRequestModel { String? captchaKey, }) = _LoginRequestModel; + factory LoginRequestModel.createWithCaptcha({ + required String username, + required String password, + required String captchaCode, + required String captchaKey, + }) { + return LoginRequestModel( + username: username, + password: password, + captchaCode: captchaCode, + captchaKey: 'rest_captcha_$captchaKey.0', + ); + } + factory LoginRequestModel.fromJson(Map json) => _$LoginRequestModelFromJson(json); const LoginRequestModel._(); - String get formattedCaptchaKey => 'rest_captcha_$captchaKey.0'; } diff --git a/packages/auth/lib/data/repositories/auth_repository_imp.dart b/packages/auth/lib/data/repositories/auth_repository_imp.dart index 9ddcd17..6f03fc0 100644 --- a/packages/auth/lib/data/repositories/auth_repository_imp.dart +++ b/packages/auth/lib/data/repositories/auth_repository_imp.dart @@ -14,64 +14,34 @@ class AuthRepositoryImpl implements AuthRepository { Future login({ required Map authRequest, }) async { - final response = await safeCall>( - call: - () async => await _httpClient.post( - '$_BASE_URL/login/', - data: authRequest, - headers: {'Content-Type': 'application/json'}, - ), - onSuccess: (response) { - iLog(response); - }, - onError: (error, trace) { - throw Exception('Error during sign in: $error'); - }, + var res = await _httpClient.post( + '$_BASE_URL/login/', + data: authRequest, + fromJson: AuthResponseModel.fromJson, + headers: {'Content-Type': 'application/json'}, ); - - return response?.data; + return res.data; } @override Future captcha() async { - final response = await safeCall( - call: () async { - var res = await _httpClient.post( - 'captcha/', - fromJson: CaptchaResponseModel.fromJson, - ); - return res.data; - }, - onSuccess: (response) { - return response; - }, - onError: (error, trace) { - throw Exception('Error during captcha : $error'); - }, + var res = await _httpClient.post( + 'captcha/', + fromJson: CaptchaResponseModel.fromJson, ); - return response; + return res.data; } @override Future loginWithRefreshToken({ required Map authRequest, }) async { - final response = await safeCall>( - call: - () async => await _httpClient.post( - '$_BASE_URL/login/', - data: authRequest, - headers: {'Content-Type': 'application/json'}, - ), - onSuccess: (response) { - iLog(response); - }, - onError: (error, trace) { - throw Exception('Error during sign in: $error'); - }, + var res = await _httpClient.post( + '$_BASE_URL/login/', + data: authRequest, + headers: {'Content-Type': 'application/json'}, ); - - return response?.data; + return res.data; } @override @@ -82,20 +52,11 @@ class AuthRepositoryImpl implements AuthRepository { @override Future hasAuthenticated() async { - final response = await safeCall>( - call: - () async => await _httpClient.get( - '$_BASE_URL/login/', - headers: {'Content-Type': 'application/json'}, - ), - onSuccess: (response) { - iLog(response); - }, - onError: (error, trace) { - throw Exception('Error during sign in: $error'); - }, + final response = await _httpClient.get( + '$_BASE_URL/login/', + headers: {'Content-Type': 'application/json'}, ); - return response?.data ?? false; + return response.data ?? false; } } diff --git a/packages/auth/lib/data/services/token_storage_service.dart b/packages/auth/lib/data/services/token_storage_service.dart index 0f0eed6..b73122c 100644 --- a/packages/auth/lib/data/services/token_storage_service.dart +++ b/packages/auth/lib/data/services/token_storage_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:rasadyar_core/core.dart'; -import 'package:rasadyar_core/injection/di.dart'; + class TokenStorageService extends GetxService { static const String _boxName = 'secureBox'; diff --git a/packages/auth/lib/presentation/pages/auth/logic.dart b/packages/auth/lib/presentation/pages/auth/logic.dart index 737c4a3..66a8887 100644 --- a/packages/auth/lib/presentation/pages/auth/logic.dart +++ b/packages/auth/lib/presentation/pages/auth/logic.dart @@ -2,7 +2,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:rasadyar_auth/auth.dart'; +import 'package:rasadyar_auth/data/common/dio_error_handler.dart'; +import 'package:rasadyar_auth/data/models/request/login_request/login_request_model.dart'; +import 'package:rasadyar_auth/data/models/response/auth/auth_response_model.dart'; import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart'; +import 'package:rasadyar_auth/data/services/token_storage_service.dart'; +import 'package:rasadyar_auth/presentation/widget/captcha/logic.dart'; import 'package:rasadyar_core/core.dart'; enum AuthType { useAndPass, otp } @@ -12,7 +17,8 @@ enum AuthStatus { init } enum OtpStatus { init, sent, verified, reSend } class AuthLogic extends GetxController { - Rx> formKey = GlobalKey().obs; + GlobalKey formKey = GlobalKey(); + Rx> formKeyOtp = GlobalKey().obs; Rx> formKeySentOtp = GlobalKey().obs; Rx phoneNumberController = TextEditingController().obs; @@ -21,11 +27,12 @@ class AuthLogic extends GetxController { TextEditingController().obs; Rx otpCodeController = TextEditingController().obs; + var captchaController = Get.find(); RxnString phoneNumber = RxnString(null); - RxnString password = RxnString(null); - RxBool isOnError = false.obs; - RxBool hidePassword = true.obs; + RxBool isLoading = false.obs; + TokenStorageService tokenStorageService = diAuth.get(); + Rx authType = AuthType.useAndPass.obs; Rx authStatus = AuthStatus.init.obs; Rx otpStatus = OtpStatus.init.obs; @@ -61,7 +68,7 @@ class AuthLogic extends GetxController { @override void onInit() { super.onInit(); - + tokenStorageService.init(); } @override @@ -76,5 +83,46 @@ class AuthLogic extends GetxController { super.onClose(); } + bool _isFormValid() { + final isCaptchaValid = + captchaController.formKey.currentState?.validate() ?? false; + final isFormValid = formKey.currentState?.validate() ?? false; + return isCaptchaValid && isFormValid; + } + LoginRequestModel _buildLoginRequest() { + final phone = phoneNumberController.value.text; + final pass = passwordController.value.text; + final code = captchaController.textController.value.text; + final key = captchaController.captchaKey.value; + + return LoginRequestModel.createWithCaptcha( + username: phone, + password: pass, + captchaCode: code, + captchaKey: key!, + ); + } + + Future submitLoginForm() async { + if (!_isFormValid()) return; + + final loginRequestModel = _buildLoginRequest(); + isLoading.value = true; + await safeCall( + call: () => authRepository.login(authRequest: loginRequestModel.toJson()), + onSuccess: (result) async{ + await tokenStorageService.saveRefreshToken(result!.refresh!); + await tokenStorageService.saveAccessToken(result!.access!); + //Get.offAndToNamed(Routes.home); + }, + onError: (error, stackTrace) { + if (error is DioException) { + diAuth.get().handle(error); + } + captchaController.getCaptcha(); + }, + ); + isLoading.value = false; + } } diff --git a/packages/auth/lib/presentation/pages/auth/view.dart b/packages/auth/lib/presentation/pages/auth/view.dart index 98cc7aa..674a796 100644 --- a/packages/auth/lib/presentation/pages/auth/view.dart +++ b/packages/auth/lib/presentation/pages/auth/view.dart @@ -1,8 +1,8 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:rasadyar_auth/presentation/widget/captcha/view.dart'; import 'package:rasadyar_auth/presentation/widget/clear_button.dart'; +import 'package:rasadyar_auth/presentation/widget/logo_widget.dart'; import 'package:rasadyar_core/core.dart'; import 'logic.dart'; @@ -17,11 +17,11 @@ class AuthPage extends GetView { child: Column( children: [ SizedBox(height: 80), - logoWidget(), + LogoWidget(), ObxValue((types) { switch (types.value) { case AuthType.otp: - //return otpForm(); + //return otpForm(); case AuthType.useAndPass: return useAndPassFrom(); } @@ -87,180 +87,114 @@ class AuthPage extends GetView { } Widget useAndPassFrom() { - return ObxValue((data) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50), - child: Form( - key: data.value, - child: Column( - children: [ - ObxValue((phoneController) { - return TextFormField( - controller: controller.phoneNumberController.value, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - gapPadding: 11, - ), - labelText: 'نام کاربری', - labelStyle: AppFonts.yekan13, - errorStyle: AppFonts.yekan13.copyWith( - color: AppColor.redNormal, - ), - - prefixIconConstraints: BoxConstraints( - maxHeight: 40, - minHeight: 40, - maxWidth: 40, - minWidth: 40, - ), - prefixIcon: Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 6, 8), - child: vecWidget(Assets.vecCallSvg), - ), - suffix: - phoneController.value.text.trim().isNotEmpty - ? clearButton(() { - phoneController.value.clear(); - phoneController.refresh(); - }) - : null, - counterText: '', - ), - keyboardType: TextInputType.numberWithOptions( - decimal: false, - signed: false, - ), - - maxLines: 1, - maxLength: 11, - onChanged: (value) { - if (controller.isOnError.value) { - controller.isOnError.value = !controller.isOnError.value; - data.value.currentState?.reset(); - - data.refresh(); - phoneController.value.text = value; - } - phoneController.refresh(); - }, - textInputAction: TextInputAction.next, - validator: (value) { - if (value == null) { - return '⚠️ شماره موبایل را وارد کنید'; - } else if (value.length < 11) { - return '⚠️ شماره موبایل باید 11 رقم باشد'; - } - return null; - }, - style: AppFonts.yekan13, - ); - }, controller.phoneNumberController), - - SizedBox(height: 26), - - ObxValue((passwordController) { - return TextFormField( - controller: passwordController.value, - obscureText: controller.hidePassword.value, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - gapPadding: 11, - ), - labelText: 'رمز عبور', - labelStyle: AppFonts.yekan13, - errorStyle: AppFonts.yekan13.copyWith( - color: AppColor.redNormal, - ), - - prefixIconConstraints: BoxConstraints( - maxHeight: 34, - minHeight: 34, - maxWidth: 34, - minWidth: 34, - ), - prefixIcon: Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: vecWidget(Assets.vecKeySvg), - ), - suffix: - passwordController.value.text.trim().isNotEmpty - ? GestureDetector( - onTap: () { - controller.hidePassword.value = - !controller.hidePassword.value; - }, - child: Icon( - controller.hidePassword.value - ? CupertinoIcons.eye - : CupertinoIcons.eye_slash, - ), - ) - : null, - counterText: '', - ), - textInputAction: TextInputAction.done, - keyboardType: TextInputType.visiblePassword, - maxLines: 1, - onChanged: (value) { - if (controller.isOnError.value) { - controller.isOnError.value = !controller.isOnError.value; - data.value.currentState?.reset(); - passwordController.value.text = value; - } - passwordController.refresh(); - }, - validator: (value) { - if (value == null || value.isEmpty) { - return '⚠️ رمز عبور را وارد کنید'; // "Please enter the password" - } - return null; - }, - style: AppFonts.yekan13, - ); - }, controller.passwordController), - SizedBox(height: 26), - - CaptchaWidget(), - - SizedBox(height: 23), - RElevated( + return Padding( + padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50), + child: Form( + key: controller.formKey, + child: Column( + children: [ + ObxValue( + (phoneController) => RTextField( + label: 'نام کاربری', + maxLength: 11, + maxLines: 1, + controller: phoneController.value, + keyboardType: TextInputType.number, + initText: phoneController.value.text, + onChanged: (value) { + phoneController.value.text = value; + phoneController.refresh(); + }, + prefixIcon: Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 6, 8), + child: vecWidget(Assets.vecCallSvg), + ), + suffixIcon: + phoneController.value.text.trim().isNotEmpty + ? clearButton(() { + phoneController.value.clear(); + phoneController.refresh(); + }) + : null, + validator: (value) { + if (value == null || value.isEmpty) { + return '⚠️ شماره موبایل را وارد کنید'; + } + /*else if (value.length < 11) { + return '⚠️ شماره موبایل باید 11 رقم باشد'; + }*/ + return null; + }, + style: AppFonts.yekan13, + errorStyle: AppFonts.yekan13.copyWith( + color: AppColor.redNormal, + ), + labelStyle: AppFonts.yekan13, + boxConstraints: const BoxConstraints( + maxHeight: 40, + minHeight: 40, + maxWidth: 40, + minWidth: 40, + ), + ), + controller.phoneNumberController, + ), + const SizedBox(height: 26), + ObxValue( + (passwordController) => RTextField( + label: 'رمز عبور', + filled: false, + controller: passwordController.value, + variant: RTextFieldVariant.password, + initText: passwordController.value.text, + onChanged: (value) { + passwordController.refresh(); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return '⚠️ رمز عبور را وارد کنید'; + } + return null; + }, + style: AppFonts.yekan13, + errorStyle: AppFonts.yekan13.copyWith( + color: AppColor.redNormal, + ), + labelStyle: AppFonts.yekan13, + prefixIcon: Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), + child: vecWidget(Assets.vecKeySvg), + ), + boxConstraints: const BoxConstraints( + maxHeight: 34, + minHeight: 34, + maxWidth: 34, + minWidth: 34, + ), + ), + controller.passwordController, + ), + SizedBox(height: 26), + CaptchaWidget(), + SizedBox(height: 23), + ObxValue((data) { + return RElevated( text: 'ورود', + isLoading: data.value, onPressed: () async { - Jalali? picked = await showPersianDatePicker( - context: Get.context!, - - initialDate: Jalali.now(), - firstDate: Jalali(1385, 8), - lastDate: Jalali(1450, 9), - initialEntryMode: PersianDatePickerEntryMode.calendarOnly, - initialDatePickerMode: PersianDatePickerMode.year, - ); + await controller.submitLoginForm(); }, width: Get.width, height: 48, - ), - ], - ), + ); + }, controller.isLoading), + ], ), - ); - }, controller.formKey); + ), + ); } -/* Widget otpForm() { - return ObxValue((status) { - switch (status.value) { - case OtpStatus.init: - return sendCodeForm(); - case OtpStatus.sent: - case OtpStatus.verified: - case OtpStatus.reSend: - return confirmCodeForm(); - } - }, controller.otpStatus); - }*/ - + /* Widget sendCodeForm() { return ObxValue((data) { return Form( @@ -494,18 +428,5 @@ class AuthPage extends GetView { ), ); }, controller.formKeySentOtp); - } - - Widget logoWidget() { - return Column( - children: [ - Row(), - Image.asset(Assets.imagesInnerSplash, width: 120, height: 120), - Text( - 'سامانه رصدیار', - style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyNormal), - ), - ], - ); - } + }*/ } diff --git a/packages/auth/lib/presentation/routes/pages.dart b/packages/auth/lib/presentation/routes/pages.dart index 4b12ffa..11b8d56 100644 --- a/packages/auth/lib/presentation/routes/pages.dart +++ b/packages/auth/lib/presentation/routes/pages.dart @@ -1,3 +1,4 @@ +import 'package:rasadyar_auth/presentation/widget/captcha/logic.dart'; import 'package:rasadyar_core/core.dart'; import '../pages/auth/logic.dart'; @@ -14,15 +15,16 @@ sealed class AuthPages { page: () => AuthPage(), binding: BindingsBuilder(() { Get.lazyPut(() => AuthLogic()); + Get.lazyPut(() => CaptchaWidgetLogic()); }), ), - GetPage( name: AuthPaths.auth, page: () => AuthPage(), binding: BindingsBuilder(() { Get.lazyPut(() => AuthLogic()); + Get.lazyPut(() => CaptchaWidgetLogic()); }), ), ]; diff --git a/packages/auth/lib/presentation/widget/captcha/logic.dart b/packages/auth/lib/presentation/widget/captcha/logic.dart index de379b6..4adb51c 100644 --- a/packages/auth/lib/presentation/widget/captcha/logic.dart +++ b/packages/auth/lib/presentation/widget/captcha/logic.dart @@ -6,7 +6,8 @@ import 'package:rasadyar_core/core.dart'; class CaptchaWidgetLogic extends GetxController with StateMixin { - TextEditingController textController = TextEditingController(); + Rx textController = TextEditingController().obs; + RxnString captchaKey = RxnString(); GlobalKey formKey = GlobalKey(); AuthRepositoryImpl authRepository = diAuth.get(); @@ -17,17 +18,19 @@ class CaptchaWidgetLogic extends GetxController getCaptcha(); } - @override void onClose() { + textController.value.dispose(); super.onClose(); } Future getCaptcha() async { change(null, status: RxStatus.loading()); + textController.value.clear(); safeCall( call: () async => await authRepository.captcha(), onSuccess: (value) { + captchaKey.value = value?.captchaKey; change(value, status: RxStatus.success()); }, onError: (error, stackTrace) { diff --git a/packages/auth/lib/presentation/widget/captcha/view.dart b/packages/auth/lib/presentation/widget/captcha/view.dart index db795c2..af4d955 100644 --- a/packages/auth/lib/presentation/widget/captcha/view.dart +++ b/packages/auth/lib/presentation/widget/captcha/view.dart @@ -8,9 +8,7 @@ import 'package:rasadyar_core/core.dart'; import 'logic.dart'; class CaptchaWidget extends GetView { - CaptchaWidget({super.key}); - - final CaptchaWidgetLogic logic = Get.put(CaptchaWidgetLogic()); + const CaptchaWidget({super.key}); @override Widget build(BuildContext context) { @@ -19,7 +17,7 @@ class CaptchaWidget extends GetView { children: [ Container( width: 135, - height: 48, + height: 50, clipBehavior: Clip.antiAliasWithSaveLayer, decoration: BoxDecoration( color: AppColor.whiteNormalHover, @@ -27,75 +25,62 @@ class CaptchaWidget extends GetView { borderRadius: BorderRadius.circular(8), ), child: controller.obx( - (state) => Image.memory(base64Decode(state?.captchaImage??''),fit: BoxFit.cover,), + (state) => Image.memory( + base64Decode(state?.captchaImage ?? ''), + fit: BoxFit.cover, + ), onLoading: const Center( - child: CupertinoActivityIndicator( - color: AppColor.blueNormal, - ), + child: CupertinoActivityIndicator(color: AppColor.blueNormal), ), onError: (error) { return const Center( - child: Text( - 'خطا در بارگذاری کد امنیتی', - style: AppFonts.yekan13,)); + child: Text( + 'خطا در بارگذاری کد امنیتی', + style: AppFonts.yekan13, + ), + ); }, ), ), - const SizedBox(height: 20), - IconButton( - padding: EdgeInsets.zero, - onPressed: controller.getCaptcha, - icon: Icon(CupertinoIcons.refresh, size: 16), + GestureDetector( + onTap: controller.getCaptcha, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 3), + child: Icon(CupertinoIcons.refresh, size: 20), + ), ), + const SizedBox(width: 8), Expanded( child: Form( key: controller.formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: TextFormField( - controller: controller.textController, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - gapPadding: 11, + autovalidateMode: AutovalidateMode.disabled, + child: ObxValue((data) { + return RTextField( + label: 'کد امنیتی', + controller: data.value, + keyboardType: TextInputType.numberWithOptions( + decimal: false, + signed: false, ), - labelText: 'کد امنیتی', - labelStyle: AppFonts.yekan13, - errorStyle: AppFonts.yekan10.copyWith( - color: AppColor.redNormal, - fontSize: 8, - ), - suffixIconConstraints: BoxConstraints( - maxHeight: 24, - minHeight: 24, - maxWidth: 24, - minWidth: 24, - ), - suffix: - controller.textController.text - .trim() - .isNotEmpty - ? clearButton(() => controller.textController.clear()) - : null, - counterText: '', - ), - keyboardType: TextInputType.numberWithOptions( - decimal: false, - signed: false, - ), - maxLines: 1, - maxLength: 6, - onChanged: (value) {}, - validator: (value) { - if (value == null || value.isEmpty) { - return 'کد امنیتی را وارد کنید'; - } - /*if (value != controller.captchaCode.toString()) { - return '⚠️کد امنیتی وارد شده اشتباه است'; - }*/ - return null; - }, - style: AppFonts.yekan13, - ), + maxLines: 1, + maxLength: 6, + suffixIcon: + (data.value.text.trim().isNotEmpty ?? false) + ? clearButton( + () => controller.textController.value.clear(), + ) + : null, + + onSubmitted: (data) {}, + validator: (value) { + if (value == null || value.isEmpty) { + return 'کد امنیتی را وارد کنید'; + } + return null; + }, + style: AppFonts.yekan13, + ); + }, controller.textController), ), ), ], diff --git a/packages/auth/lib/presentation/widget/clear_button.dart b/packages/auth/lib/presentation/widget/clear_button.dart index 393ebed..523d6b5 100644 --- a/packages/auth/lib/presentation/widget/clear_button.dart +++ b/packages/auth/lib/presentation/widget/clear_button.dart @@ -3,6 +3,6 @@ import 'package:flutter/cupertino.dart'; Widget clearButton(VoidCallback onTap) { return GestureDetector( onTap: onTap, - child: Icon(CupertinoIcons.multiply_circle, size: 24), + child: Icon(CupertinoIcons.multiply_circle, size: 20), ); } diff --git a/packages/auth/lib/presentation/widget/logo_widget.dart b/packages/auth/lib/presentation/widget/logo_widget.dart new file mode 100644 index 0000000..ea4a5a9 --- /dev/null +++ b/packages/auth/lib/presentation/widget/logo_widget.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:rasadyar_core/core.dart'; + +class LogoWidget extends StatelessWidget { + const LogoWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row(), + Image.asset(Assets.imagesInnerSplash, width: 120, height: 120), + Text( + 'سامانه رصدیار', + style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyNormal), + ), + ], + ); + } +} diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart index 4e48c2f..6108fd6 100644 --- a/packages/core/lib/core.dart +++ b/packages/core/lib/core.dart @@ -30,10 +30,13 @@ export 'package:rasadyar_core/presentation/common/common.dart'; export 'package:rasadyar_core/presentation/utils/utils.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart'; -export 'infrastructure/remote/dio_form_data.dart'; + //network +export 'infrastructure/remote/dio_form_data.dart'; export 'infrastructure/remote/dio_remote.dart'; export 'infrastructure/remote/dio_response.dart'; +export 'package:dio/dio.dart' show DioException; + //utils export 'utils/logger_utils.dart'; export 'utils/safe_call_utils.dart'; diff --git a/packages/core/lib/infrastructure/remote/dio_remote.dart b/packages/core/lib/infrastructure/remote/dio_remote.dart index 31b84e5..c8f0189 100644 --- a/packages/core/lib/infrastructure/remote/dio_remote.dart +++ b/packages/core/lib/infrastructure/remote/dio_remote.dart @@ -16,7 +16,11 @@ class DioRemote implements IHttpClient { Future init() async { final dio = Dio(BaseOptions(baseUrl: baseUrl)); if (kDebugMode) { - dio.interceptors.add(PrettyDioLogger()); + dio.interceptors.add(PrettyDioLogger( + requestHeader: true, + responseHeader: true, + requestBody: true + )); } _dio = dio; } diff --git a/packages/core/lib/presentation/widget/buttons/elevated.dart b/packages/core/lib/presentation/widget/buttons/elevated.dart index 3e4e5ff..bcbe124 100644 --- a/packages/core/lib/presentation/widget/buttons/elevated.dart +++ b/packages/core/lib/presentation/widget/buttons/elevated.dart @@ -3,51 +3,65 @@ import 'package:rasadyar_core/presentation/common/app_color.dart'; import 'package:rasadyar_core/presentation/common/app_fonts.dart'; class RElevated extends StatelessWidget { - RElevated({ + const RElevated({ super.key, required this.text, required this.onPressed, - this.foregroundColor, - this.backgroundColor, + this.foregroundColor = Colors.white, + this.backgroundColor = AppColor.blueNormal, this.disabledBackgroundColor, - this.disabledForegroundColor, - this.radius, + this.disabledForegroundColor = Colors.white, + this.radius = 8.0, this.textStyle, this.width = 150.0, this.height = 56.0, - this.isFullWidth, + this.isFullWidth = false, + this.isLoading = false, }); final String text; final VoidCallback? onPressed; final double width; final double height; - final bool? isFullWidth; - Color? foregroundColor; - Color? backgroundColor; - Color? disabledForegroundColor; - Color? disabledBackgroundColor; - double? radius; - TextStyle? textStyle; + final bool isFullWidth; + final Color foregroundColor; + final Color backgroundColor; + final Color? disabledForegroundColor; + final Color? disabledBackgroundColor; + final double radius; + final TextStyle? textStyle; + final bool isLoading; @override Widget build(BuildContext context) { + final bool isEnabled = onPressed != null && !isLoading; + return ElevatedButton( - onPressed: onPressed, + onPressed: isEnabled ? onPressed : null, style: ElevatedButton.styleFrom( - backgroundColor: backgroundColor ?? AppColor.blueNormal, - foregroundColor: foregroundColor ?? Colors.white, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, disabledBackgroundColor: - disabledBackgroundColor ?? AppColor.blueNormal.withAlpha(38), - disabledForegroundColor: disabledForegroundColor ?? Colors.white, + disabledBackgroundColor ?? backgroundColor.withAlpha(38), + disabledForegroundColor: disabledForegroundColor, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius ?? 8), + borderRadius: BorderRadius.circular(radius), ), - minimumSize: Size((isFullWidth ??false) ? double.infinity : width, height), + minimumSize: Size(isFullWidth ? double.infinity : width, height), padding: EdgeInsets.zero, textStyle: textStyle ?? AppFonts.yekan24, ), - child: Text(text), + child: + isLoading + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(foregroundColor), + ), + ) + : Text(text), ); } } diff --git a/packages/core/lib/presentation/widget/inputs/r_input.dart b/packages/core/lib/presentation/widget/inputs/r_input.dart index c6b9d9a..2780b59 100644 --- a/packages/core/lib/presentation/widget/inputs/r_input.dart +++ b/packages/core/lib/presentation/widget/inputs/r_input.dart @@ -2,164 +2,106 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:rasadyar_core/core.dart'; -@immutable +enum RTextFieldVariant { + normal, + noBorder, + password, + passwordNoBorder, +} + class RTextField extends StatefulWidget { - RTextField({ - super.key, - this.maxLines, - this.maxLength, - this.hintText, - this.padding, - this.onChanged, - this.onSubmitted, - this.keyboardType, - this.showCounter = false, - this.isDense, - this.initText, - this.isForNumber = false, - this.style, - this.hintStyle, - this.suffixIcon, - this.prefixIcon, - this.validator, - this.readonly = false, - this.boxConstraints, - this.minLines, - this.radius, - this.filled, - this.filledColor, - this.enabled, - this.errorStyle, - this.labelStyle, - this.label, - }) { - filled = filled ?? false; - obscure = false; - _inputBorder = OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(radius ?? 8), - ); - } - - RTextField.noBorder({ - super.key, - this.maxLines, - this.maxLength, - this.hintText, - this.padding, - this.onChanged, - this.onSubmitted, - this.keyboardType, - this.showCounter = false, - this.isDense, - this.initText, - this.style, - this.hintStyle, - this.suffixIcon, - this.radius, - this.validator, - this.boxConstraints, - this.minLines, - this.isForNumber = false, - this.readonly = false, - this.label, - this.filled, - this.filledColor, - this.errorStyle, - this.labelStyle, - this.enabled, - }) { - _inputBorder = OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(radius ?? 16), - ); - obscure = false; - filled = filled ?? true; - } - - RTextField.password({ - super.key, - this.maxLines = 1, - this.maxLength, - this.hintText, - this.padding, - this.onChanged, - this.onSubmitted, - this.keyboardType, - this.showCounter = false, - this.isDense, - this.initText, - this.style, - this.hintStyle, - this.suffixIcon, - this.prefixIcon, - this.radius, - this.validator, - this.boxConstraints, - this.minLines, - this.isForNumber = false, - this.readonly = false, - this.label, - this.filled, - this.filledColor, - this.errorStyle, - this.labelStyle, - this.enabled, - }) { - _inputBorder = OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(radius ?? 16), - ); - filled = filled ?? true; - obscure = true; - _isPassword = true; - prefixIcon = prefixIcon ?? const Icon(CupertinoIcons.person); - } - - final int? maxLines; - final int? minLines; - final int? maxLength; - final String? hintText; + final TextEditingController controller; final String? label; - final EdgeInsets? padding; + final String? hintText; + final String? initText; + final bool obscure; + final bool readonly; + final bool enabled; + final int? maxLength; + final int? minLines; + final int? maxLines; + final Widget? suffixIcon; + final Widget? prefixIcon; + final BoxConstraints? boxConstraints; + final RTextFieldVariant variant; + final bool filled; + final Color? filledColor; + final bool showCounter; + final bool isDense; + final TextInputType? keyboardType; final TextStyle? style; - final TextStyle? errorStyle; final TextStyle? hintStyle; final TextStyle? labelStyle; - final bool showCounter; - final bool? isDense; - final bool? isForNumber; - final bool readonly; - bool? obscure; - final bool? enabled; - final double? radius; - final TextInputType? keyboardType; - final Function(String)? onChanged; - final Function(String)? onSubmitted; - final FormFieldValidator? validator; - final String? initText; - Widget? suffixIcon; - Widget? prefixIcon; - bool? filled; - Color? filledColor; - bool _isPassword = false; + final TextStyle? errorStyle; + final EdgeInsets? padding; + final FormFieldValidator? validator; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + + const RTextField({ + super.key, + required this.controller, + this.label, + this.hintText, + this.initText, + this.obscure = false, + this.readonly = false, + this.enabled = true, + this.maxLength, + this.minLines, + this.maxLines = 1, + this.suffixIcon, + this.prefixIcon, + this.boxConstraints, + this.variant = RTextFieldVariant.normal, + this.filled = false, + this.filledColor, + this.showCounter = false, + this.isDense = false, + this.keyboardType, + this.style, + this.hintStyle, + this.labelStyle, + this.errorStyle, + this.padding, + this.validator, + this.onChanged, + this.onSubmitted, + }); - final BoxConstraints? boxConstraints; - late final InputBorder? _inputBorder; @override State createState() => _RTextFieldState(); + + bool get _isPassword => variant == RTextFieldVariant.password; + bool get _noBorder => variant == RTextFieldVariant.noBorder; + bool get _passwordNoBorder => variant == RTextFieldVariant.passwordNoBorder; + + + InputBorder get _inputBorder => + _noBorder || _passwordNoBorder ? InputBorder.none : OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: AppColor.lightGreyDarkActive, + width: 1, + ), + ); + + + + + + } class _RTextFieldState extends State { - final TextEditingController _controller = TextEditingController(); - bool? obscure; + late bool obscure; @override void initState() { super.initState(); if (widget.initText != null) { - _controller.text = widget.initText!; + widget.controller.text = widget.initText!; } obscure = widget.obscure; } @@ -167,51 +109,56 @@ class _RTextFieldState extends State { @override void didUpdateWidget(covariant RTextField oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.initText != oldWidget.initText) { - _controller.text = widget.initText ?? ''; + if (widget.initText != null && widget.initText != oldWidget.initText) { + widget.controller.text = widget.initText!; } } + Widget _buildSuffixIcon() { + if (widget.suffixIcon != null) return widget.suffixIcon!; + if (!widget._isPassword) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: GestureDetector( + onTap: () { + setState(() { + obscure = !obscure; + }); + }, + child: Icon( + obscure ? CupertinoIcons.eye : CupertinoIcons.eye_slash, + color: AppColor.darkGreyDarkActive, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Padding( padding: widget.padding ?? EdgeInsets.zero, child: TextFormField( - controller: _controller, + controller: widget.controller, readOnly: widget.readonly, minLines: widget.minLines, maxLines: widget.maxLines, onChanged: widget.onChanged, validator: widget.validator, enabled: widget.enabled, - obscureText: obscure ?? false, - onTapOutside: (event) { - FocusScope.of(context).unfocus(); - }, + obscureText: obscure, + onTapOutside: (_) => FocusScope.of(context).unfocus(), onFieldSubmitted: widget.onSubmitted, maxLength: widget.maxLength, textDirection: TextDirection.rtl, style: widget.style, keyboardType: widget.keyboardType, decoration: InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 16), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), errorStyle: widget.errorStyle, errorMaxLines: 1, isDense: widget.isDense, - suffixIcon: - widget.suffixIcon ?? - (widget._isPassword - ? IconButton( - onPressed: () { - setState(() { - obscure = !obscure!; - }); - }, - icon: Icon( - !obscure! ? CupertinoIcons.eye_slash : CupertinoIcons.eye, - ), - ) - : null), + suffixIcon: _buildSuffixIcon(), suffixIconConstraints: widget.boxConstraints, prefixIcon: widget.prefixIcon, prefixIconConstraints: widget.boxConstraints, @@ -221,7 +168,7 @@ class _RTextFieldState extends State { labelStyle: AppFonts.yekan14 .copyWith(color: AppColor.lightGreyDarkActive) .merge(widget.labelStyle), - filled: widget.filled, + filled: widget.filled || widget._noBorder || widget._passwordNoBorder, fillColor: widget.filledColor, counter: widget.showCounter ? null : const SizedBox(), hintStyle: widget.hintStyle, @@ -232,4 +179,4 @@ class _RTextFieldState extends State { ), ); } -} +} \ No newline at end of file diff --git a/packages/core/lib/utils/safe_call_utils.dart b/packages/core/lib/utils/safe_call_utils.dart index 78195cc..e210b40 100644 --- a/packages/core/lib/utils/safe_call_utils.dart +++ b/packages/core/lib/utils/safe_call_utils.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:rasadyar_core/core.dart'; @@ -6,7 +7,7 @@ typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace); typedef VoidCallback = void Function(); // تعریف دقیق تابع safeCall -Future safeCall({ +Future safeCall({ required AsyncCallback call, Function(T result)? onSuccess, ErrorCallback? onError, @@ -33,7 +34,7 @@ Future safeCall({ } onSuccess?.call(result); - return result; + } catch (error, stackTrace) { if (showError) { @@ -45,7 +46,6 @@ Future safeCall({ print('safeCall error: $error\n$stackTrace'); } - return null; } finally { if (showLoading) { (onHideLoading ?? _defaultHideLoading)(); diff --git a/packages/inspection/lib/presentation/add_mobile_inspector/view.dart b/packages/inspection/lib/presentation/add_mobile_inspector/view.dart index 75bf4d1..61a9aba 100644 --- a/packages/inspection/lib/presentation/add_mobile_inspector/view.dart +++ b/packages/inspection/lib/presentation/add_mobile_inspector/view.dart @@ -68,24 +68,28 @@ Container mobileInspectorWidget() { spacing: 16, children: [ RTextField( + controller: TextEditingController(), label: 'نام و نام خانوادگی', filled: true, filledColor: AppColor.whiteLight, padding: EdgeInsets.zero, ), RTextField( + controller: TextEditingController(), label: 'شماره مجوز', filled: true, filledColor: AppColor.whiteLight, padding: EdgeInsets.zero, ), RTextField( + controller: TextEditingController(), label: 'شماره ثبت', filled: true, filledColor: AppColor.whiteLight, padding: EdgeInsets.zero, ), RTextField( + controller: TextEditingController(), label: 'کد اقتصادی', filled: true, filledColor: AppColor.whiteLight, diff --git a/packages/inspection/lib/presentation/add_supervision/view.dart b/packages/inspection/lib/presentation/add_supervision/view.dart index 8be8f92..46738a1 100644 --- a/packages/inspection/lib/presentation/add_supervision/view.dart +++ b/packages/inspection/lib/presentation/add_supervision/view.dart @@ -86,13 +86,17 @@ class AddSupervisionPage extends GetView { ); }, controller.violationSegmentsSelected), SizedBox(height: 8), - RTextField(label: 'صادر کننده پروانه'), + RTextField( + controller: TextEditingController(),label: 'صادر کننده پروانه'), SizedBox(height: 8), - RTextField(label: 'شماره مجوز'), + RTextField( + controller: TextEditingController(),label: 'شماره مجوز'), SizedBox(height: 8), - RTextField(label: 'شماره ثبت'), + RTextField( + controller: TextEditingController(),label: 'شماره ثبت'), SizedBox(height: 8), - RTextField(label: 'کد اقتصادی'), + RTextField( + controller: TextEditingController(),label: 'کد اقتصادی'), ], ), ), diff --git a/packages/inspection/lib/presentation/display_information/view.dart b/packages/inspection/lib/presentation/display_information/view.dart index 55b52d9..3adbe1f 100644 --- a/packages/inspection/lib/presentation/display_information/view.dart +++ b/packages/inspection/lib/presentation/display_information/view.dart @@ -108,29 +108,34 @@ Widget violationWidget() { child: Column( spacing: 16, children: [ - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'توضیحات تخلف', filled: true, filledColor: AppColor.whiteLight, maxLines: 3, minLines: 3, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'توضیحات تخلف', filled: true, filledColor: AppColor.whiteLight, diff --git a/packages/inspection/lib/presentation/registration_of_violation/view.dart b/packages/inspection/lib/presentation/registration_of_violation/view.dart index b74ee54..9e75e0a 100644 --- a/packages/inspection/lib/presentation/registration_of_violation/view.dart +++ b/packages/inspection/lib/presentation/registration_of_violation/view.dart @@ -69,29 +69,34 @@ Container violationWidget() { child: Column( spacing: 16, children: [ - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'توضیحات تخلف', filled: true, filledColor: AppColor.whiteLight, maxLines: 3, minLines: 3, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'عنوان تخلف', filled: true, filledColor: AppColor.whiteLight, ), - RTextField( + RTextField( + controller: TextEditingController(), label: 'توضیحات تخلف', filled: true, filledColor: AppColor.whiteLight,