feat : login api call

This commit is contained in:
2025-05-17 15:24:06 +03:30
parent 0e630e709b
commit 303ff86d85
22 changed files with 518 additions and 522 deletions

View File

@@ -67,11 +67,13 @@ class _SystemDesignPageState extends State<SystemDesignPage> {
child: Column( child: Column(
spacing: 14, spacing: 14,
children: [ children: [
RTextField( RTextField(
controller: TextEditingController(),
hintText: 'حجم کشتار را در روز به قطعه وارد کنید', hintText: 'حجم کشتار را در روز به قطعه وارد کنید',
hintStyle: AppFonts.yekan13, hintStyle: AppFonts.yekan13,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'تلفن مرغداری', label: 'تلفن مرغداری',
labelStyle: AppFonts.yekan10, labelStyle: AppFonts.yekan10,
), ),

View File

@@ -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,
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:rasadyar_auth/data/common/constant.dart'; 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/repositories/auth_repository_imp.dart';
import 'package:rasadyar_auth/data/services/auth_service.dart'; import 'package:rasadyar_auth/data/services/auth_service.dart';
import 'package:rasadyar_auth/data/services/token_storage_service.dart'; import 'package:rasadyar_auth/data/services/token_storage_service.dart';
@@ -16,6 +17,7 @@ Future<void> setupAuthDI() async {
diAuth.registerCachedFactory<AuthRepositoryImpl>( diAuth.registerCachedFactory<AuthRepositoryImpl>(
() => AuthRepositoryImpl(dioRemote), () => AuthRepositoryImpl(dioRemote),
); );
diAuth.registerLazySingleton(() => AuthService()); diAuth.registerLazySingleton<AuthService>(() => AuthService());
diAuth.registerLazySingleton(() => TokenStorageService()); diAuth.registerLazySingleton<TokenStorageService>(() => TokenStorageService());
diAuth.registerLazySingleton<DioErrorHandler>(() => DioErrorHandler());
} }

View File

@@ -12,10 +12,23 @@ abstract class LoginRequestModel with _$LoginRequestModel {
String? captchaKey, String? captchaKey,
}) = _LoginRequestModel; }) = _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<String, dynamic> json) => factory LoginRequestModel.fromJson(Map<String, dynamic> json) =>
_$LoginRequestModelFromJson(json); _$LoginRequestModelFromJson(json);
const LoginRequestModel._(); const LoginRequestModel._();
String get formattedCaptchaKey => 'rest_captcha_$captchaKey.0';
} }

View File

@@ -14,64 +14,34 @@ class AuthRepositoryImpl implements AuthRepository {
Future<AuthResponseModel?> login({ Future<AuthResponseModel?> login({
required Map<String, dynamic> authRequest, required Map<String, dynamic> authRequest,
}) async { }) async {
final response = await safeCall<DioResponse<AuthResponseModel>>( var res = await _httpClient.post<AuthResponseModel>(
call: '$_BASE_URL/login/',
() async => await _httpClient.post<AuthResponseModel>( data: authRequest,
'$_BASE_URL/login/', fromJson: AuthResponseModel.fromJson,
data: authRequest, headers: {'Content-Type': 'application/json'},
headers: {'Content-Type': 'application/json'},
),
onSuccess: (response) {
iLog(response);
},
onError: (error, trace) {
throw Exception('Error during sign in: $error');
},
); );
return res.data;
return response?.data;
} }
@override @override
Future<CaptchaResponseModel?> captcha() async { Future<CaptchaResponseModel?> captcha() async {
final response = await safeCall<CaptchaResponseModel?>( var res = await _httpClient.post<CaptchaResponseModel?>(
call: () async { 'captcha/',
var res = await _httpClient.post<CaptchaResponseModel?>( fromJson: CaptchaResponseModel.fromJson,
'captcha/',
fromJson: CaptchaResponseModel.fromJson,
);
return res.data;
},
onSuccess: (response) {
return response;
},
onError: (error, trace) {
throw Exception('Error during captcha : $error');
},
); );
return response; return res.data;
} }
@override @override
Future<AuthResponseModel?> loginWithRefreshToken({ Future<AuthResponseModel?> loginWithRefreshToken({
required Map<String, dynamic> authRequest, required Map<String, dynamic> authRequest,
}) async { }) async {
final response = await safeCall<DioResponse<AuthResponseModel>>( var res = await _httpClient.post<AuthResponseModel>(
call: '$_BASE_URL/login/',
() async => await _httpClient.post<AuthResponseModel>( data: authRequest,
'$_BASE_URL/login/', headers: {'Content-Type': 'application/json'},
data: authRequest,
headers: {'Content-Type': 'application/json'},
),
onSuccess: (response) {
iLog(response);
},
onError: (error, trace) {
throw Exception('Error during sign in: $error');
},
); );
return res.data;
return response?.data;
} }
@override @override
@@ -82,20 +52,11 @@ class AuthRepositoryImpl implements AuthRepository {
@override @override
Future<bool> hasAuthenticated() async { Future<bool> hasAuthenticated() async {
final response = await safeCall<DioResponse<bool>>( final response = await _httpClient.get<bool>(
call: '$_BASE_URL/login/',
() async => await _httpClient.get<bool>( headers: {'Content-Type': 'application/json'},
'$_BASE_URL/login/',
headers: {'Content-Type': 'application/json'},
),
onSuccess: (response) {
iLog(response);
},
onError: (error, trace) {
throw Exception('Error during sign in: $error');
},
); );
return response?.data ?? false; return response.data ?? false;
} }
} }

View File

@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'package:rasadyar_core/injection/di.dart';
class TokenStorageService extends GetxService { class TokenStorageService extends GetxService {
static const String _boxName = 'secureBox'; static const String _boxName = 'secureBox';

View File

@@ -2,7 +2,12 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_auth/auth.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/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'; import 'package:rasadyar_core/core.dart';
enum AuthType { useAndPass, otp } enum AuthType { useAndPass, otp }
@@ -12,7 +17,8 @@ enum AuthStatus { init }
enum OtpStatus { init, sent, verified, reSend } enum OtpStatus { init, sent, verified, reSend }
class AuthLogic extends GetxController { class AuthLogic extends GetxController {
Rx<GlobalKey<FormState>> formKey = GlobalKey<FormState>().obs; GlobalKey<FormState> formKey = GlobalKey<FormState>();
Rx<GlobalKey<FormState>> formKeyOtp = GlobalKey<FormState>().obs; Rx<GlobalKey<FormState>> formKeyOtp = GlobalKey<FormState>().obs;
Rx<GlobalKey<FormState>> formKeySentOtp = GlobalKey<FormState>().obs; Rx<GlobalKey<FormState>> formKeySentOtp = GlobalKey<FormState>().obs;
Rx<TextEditingController> phoneNumberController = TextEditingController().obs; Rx<TextEditingController> phoneNumberController = TextEditingController().obs;
@@ -21,11 +27,12 @@ class AuthLogic extends GetxController {
TextEditingController().obs; TextEditingController().obs;
Rx<TextEditingController> otpCodeController = TextEditingController().obs; Rx<TextEditingController> otpCodeController = TextEditingController().obs;
var captchaController = Get.find<CaptchaWidgetLogic>();
RxnString phoneNumber = RxnString(null); RxnString phoneNumber = RxnString(null);
RxnString password = RxnString(null); RxBool isLoading = false.obs;
RxBool isOnError = false.obs; TokenStorageService tokenStorageService = diAuth.get<TokenStorageService>();
RxBool hidePassword = true.obs;
Rx<AuthType> authType = AuthType.useAndPass.obs; Rx<AuthType> authType = AuthType.useAndPass.obs;
Rx<AuthStatus> authStatus = AuthStatus.init.obs; Rx<AuthStatus> authStatus = AuthStatus.init.obs;
Rx<OtpStatus> otpStatus = OtpStatus.init.obs; Rx<OtpStatus> otpStatus = OtpStatus.init.obs;
@@ -61,7 +68,7 @@ class AuthLogic extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
tokenStorageService.init();
} }
@override @override
@@ -76,5 +83,46 @@ class AuthLogic extends GetxController {
super.onClose(); 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<void> submitLoginForm() async {
if (!_isFormValid()) return;
final loginRequestModel = _buildLoginRequest();
isLoading.value = true;
await safeCall<AuthResponseModel?>(
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<DioErrorHandler>().handle(error);
}
captchaController.getCaptcha();
},
);
isLoading.value = false;
}
} }

View File

@@ -1,8 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_auth/presentation/widget/captcha/view.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/clear_button.dart';
import 'package:rasadyar_auth/presentation/widget/logo_widget.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import 'logic.dart'; import 'logic.dart';
@@ -17,11 +17,11 @@ class AuthPage extends GetView<AuthLogic> {
child: Column( child: Column(
children: [ children: [
SizedBox(height: 80), SizedBox(height: 80),
logoWidget(), LogoWidget(),
ObxValue((types) { ObxValue((types) {
switch (types.value) { switch (types.value) {
case AuthType.otp: case AuthType.otp:
//return otpForm(); //return otpForm();
case AuthType.useAndPass: case AuthType.useAndPass:
return useAndPassFrom(); return useAndPassFrom();
} }
@@ -87,180 +87,114 @@ class AuthPage extends GetView<AuthLogic> {
} }
Widget useAndPassFrom() { Widget useAndPassFrom() {
return ObxValue((data) { return Padding(
return Padding( padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 50), child: Form(
child: Form( key: controller.formKey,
key: data.value, child: Column(
child: Column( children: [
children: [ ObxValue(
ObxValue((phoneController) { (phoneController) => RTextField(
return TextFormField( label: 'نام کاربری',
controller: controller.phoneNumberController.value, maxLength: 11,
decoration: InputDecoration( maxLines: 1,
border: OutlineInputBorder( controller: phoneController.value,
borderRadius: BorderRadius.circular(8), keyboardType: TextInputType.number,
gapPadding: 11, initText: phoneController.value.text,
), onChanged: (value) {
labelText: 'نام کاربری', phoneController.value.text = value;
labelStyle: AppFonts.yekan13, phoneController.refresh();
errorStyle: AppFonts.yekan13.copyWith( },
color: AppColor.redNormal, prefixIcon: Padding(
), padding: const EdgeInsets.fromLTRB(0, 8, 6, 8),
child: vecWidget(Assets.vecCallSvg),
prefixIconConstraints: BoxConstraints( ),
maxHeight: 40, suffixIcon:
minHeight: 40, phoneController.value.text.trim().isNotEmpty
maxWidth: 40, ? clearButton(() {
minWidth: 40, phoneController.value.clear();
), phoneController.refresh();
prefixIcon: Padding( })
padding: const EdgeInsets.fromLTRB(0, 8, 6, 8), : null,
child: vecWidget(Assets.vecCallSvg), validator: (value) {
), if (value == null || value.isEmpty) {
suffix: return '⚠️ شماره موبایل را وارد کنید';
phoneController.value.text.trim().isNotEmpty }
? clearButton(() { /*else if (value.length < 11) {
phoneController.value.clear(); return '⚠️ شماره موبایل باید 11 رقم باشد';
phoneController.refresh(); }*/
}) return null;
: null, },
counterText: '', style: AppFonts.yekan13,
), errorStyle: AppFonts.yekan13.copyWith(
keyboardType: TextInputType.numberWithOptions( color: AppColor.redNormal,
decimal: false, ),
signed: false, labelStyle: AppFonts.yekan13,
), boxConstraints: const BoxConstraints(
maxHeight: 40,
maxLines: 1, minHeight: 40,
maxLength: 11, maxWidth: 40,
onChanged: (value) { minWidth: 40,
if (controller.isOnError.value) { ),
controller.isOnError.value = !controller.isOnError.value; ),
data.value.currentState?.reset(); controller.phoneNumberController,
),
data.refresh(); const SizedBox(height: 26),
phoneController.value.text = value; ObxValue(
} (passwordController) => RTextField(
phoneController.refresh(); label: 'رمز عبور',
}, filled: false,
textInputAction: TextInputAction.next, controller: passwordController.value,
validator: (value) { variant: RTextFieldVariant.password,
if (value == null) { initText: passwordController.value.text,
return '⚠️ شماره موبایل را وارد کنید'; onChanged: (value) {
} else if (value.length < 11) { passwordController.refresh();
return '⚠️ شماره موبایل باید 11 رقم باشد'; },
} validator: (value) {
return null; if (value == null || value.isEmpty) {
}, return '⚠️ رمز عبور را وارد کنید';
style: AppFonts.yekan13, }
); return null;
}, controller.phoneNumberController), },
style: AppFonts.yekan13,
SizedBox(height: 26), errorStyle: AppFonts.yekan13.copyWith(
color: AppColor.redNormal,
ObxValue((passwordController) { ),
return TextFormField( labelStyle: AppFonts.yekan13,
controller: passwordController.value, prefixIcon: Padding(
obscureText: controller.hidePassword.value, padding: const EdgeInsets.fromLTRB(0, 8, 8, 8),
decoration: InputDecoration( child: vecWidget(Assets.vecKeySvg),
border: OutlineInputBorder( ),
borderRadius: BorderRadius.circular(8), boxConstraints: const BoxConstraints(
gapPadding: 11, maxHeight: 34,
), minHeight: 34,
labelText: 'رمز عبور', maxWidth: 34,
labelStyle: AppFonts.yekan13, minWidth: 34,
errorStyle: AppFonts.yekan13.copyWith( ),
color: AppColor.redNormal, ),
), controller.passwordController,
),
prefixIconConstraints: BoxConstraints( SizedBox(height: 26),
maxHeight: 34, CaptchaWidget(),
minHeight: 34, SizedBox(height: 23),
maxWidth: 34, ObxValue((data) {
minWidth: 34, return RElevated(
),
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(
text: 'ورود', text: 'ورود',
isLoading: data.value,
onPressed: () async { onPressed: () async {
Jalali? picked = await showPersianDatePicker( await controller.submitLoginForm();
context: Get.context!,
initialDate: Jalali.now(),
firstDate: Jalali(1385, 8),
lastDate: Jalali(1450, 9),
initialEntryMode: PersianDatePickerEntryMode.calendarOnly,
initialDatePickerMode: PersianDatePickerMode.year,
);
}, },
width: Get.width, width: Get.width,
height: 48, 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() { Widget sendCodeForm() {
return ObxValue((data) { return ObxValue((data) {
return Form( return Form(
@@ -494,18 +428,5 @@ class AuthPage extends GetView<AuthLogic> {
), ),
); );
}, controller.formKeySentOtp); }, controller.formKeySentOtp);
} }*/
Widget logoWidget() {
return Column(
children: [
Row(),
Image.asset(Assets.imagesInnerSplash, width: 120, height: 120),
Text(
'سامانه رصدیار',
style: AppFonts.yekan16.copyWith(color: AppColor.darkGreyNormal),
),
],
);
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:rasadyar_auth/presentation/widget/captcha/logic.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import '../pages/auth/logic.dart'; import '../pages/auth/logic.dart';
@@ -14,15 +15,16 @@ sealed class AuthPages {
page: () => AuthPage(), page: () => AuthPage(),
binding: BindingsBuilder(() { binding: BindingsBuilder(() {
Get.lazyPut(() => AuthLogic()); Get.lazyPut(() => AuthLogic());
Get.lazyPut(() => CaptchaWidgetLogic());
}), }),
), ),
GetPage( GetPage(
name: AuthPaths.auth, name: AuthPaths.auth,
page: () => AuthPage(), page: () => AuthPage(),
binding: BindingsBuilder(() { binding: BindingsBuilder(() {
Get.lazyPut(() => AuthLogic()); Get.lazyPut(() => AuthLogic());
Get.lazyPut(() => CaptchaWidgetLogic());
}), }),
), ),
]; ];

View File

@@ -6,7 +6,8 @@ import 'package:rasadyar_core/core.dart';
class CaptchaWidgetLogic extends GetxController class CaptchaWidgetLogic extends GetxController
with StateMixin<CaptchaResponseModel> { with StateMixin<CaptchaResponseModel> {
TextEditingController textController = TextEditingController(); Rx<TextEditingController> textController = TextEditingController().obs;
RxnString captchaKey = RxnString();
GlobalKey<FormState> formKey = GlobalKey<FormState>(); GlobalKey<FormState> formKey = GlobalKey<FormState>();
AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>(); AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>();
@@ -17,17 +18,19 @@ class CaptchaWidgetLogic extends GetxController
getCaptcha(); getCaptcha();
} }
@override @override
void onClose() { void onClose() {
textController.value.dispose();
super.onClose(); super.onClose();
} }
Future<void> getCaptcha() async { Future<void> getCaptcha() async {
change(null, status: RxStatus.loading()); change(null, status: RxStatus.loading());
textController.value.clear();
safeCall( safeCall(
call: () async => await authRepository.captcha(), call: () async => await authRepository.captcha(),
onSuccess: (value) { onSuccess: (value) {
captchaKey.value = value?.captchaKey;
change(value, status: RxStatus.success()); change(value, status: RxStatus.success());
}, },
onError: (error, stackTrace) { onError: (error, stackTrace) {

View File

@@ -8,9 +8,7 @@ import 'package:rasadyar_core/core.dart';
import 'logic.dart'; import 'logic.dart';
class CaptchaWidget extends GetView<CaptchaWidgetLogic> { class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
CaptchaWidget({super.key}); const CaptchaWidget({super.key});
final CaptchaWidgetLogic logic = Get.put(CaptchaWidgetLogic());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,7 +17,7 @@ class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
children: [ children: [
Container( Container(
width: 135, width: 135,
height: 48, height: 50,
clipBehavior: Clip.antiAliasWithSaveLayer, clipBehavior: Clip.antiAliasWithSaveLayer,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColor.whiteNormalHover, color: AppColor.whiteNormalHover,
@@ -27,75 +25,62 @@ class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: controller.obx( child: controller.obx(
(state) => Image.memory(base64Decode(state?.captchaImage??''),fit: BoxFit.cover,), (state) => Image.memory(
base64Decode(state?.captchaImage ?? ''),
fit: BoxFit.cover,
),
onLoading: const Center( onLoading: const Center(
child: CupertinoActivityIndicator( child: CupertinoActivityIndicator(color: AppColor.blueNormal),
color: AppColor.blueNormal,
),
), ),
onError: (error) { onError: (error) {
return const Center( return const Center(
child: Text( child: Text(
'خطا در بارگذاری کد امنیتی', 'خطا در بارگذاری کد امنیتی',
style: AppFonts.yekan13,)); style: AppFonts.yekan13,
),
);
}, },
), ),
), ),
const SizedBox(height: 20), GestureDetector(
IconButton( onTap: controller.getCaptcha,
padding: EdgeInsets.zero, child: Padding(
onPressed: controller.getCaptcha, padding: const EdgeInsets.symmetric(horizontal: 3),
icon: Icon(CupertinoIcons.refresh, size: 16), child: Icon(CupertinoIcons.refresh, size: 20),
),
), ),
const SizedBox(width: 8),
Expanded( Expanded(
child: Form( child: Form(
key: controller.formKey, key: controller.formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.disabled,
child: TextFormField( child: ObxValue((data) {
controller: controller.textController, return RTextField(
decoration: InputDecoration( label: 'کد امنیتی',
border: OutlineInputBorder( controller: data.value,
borderRadius: BorderRadius.circular(8), keyboardType: TextInputType.numberWithOptions(
gapPadding: 11, decimal: false,
signed: false,
), ),
labelText: 'کد امنیتی', maxLines: 1,
labelStyle: AppFonts.yekan13, maxLength: 6,
errorStyle: AppFonts.yekan10.copyWith( suffixIcon:
color: AppColor.redNormal, (data.value.text.trim().isNotEmpty ?? false)
fontSize: 8, ? clearButton(
), () => controller.textController.value.clear(),
suffixIconConstraints: BoxConstraints( )
maxHeight: 24, : null,
minHeight: 24,
maxWidth: 24, onSubmitted: (data) {},
minWidth: 24, validator: (value) {
), if (value == null || value.isEmpty) {
suffix: return 'کد امنیتی را وارد کنید';
controller.textController.text }
.trim() return null;
.isNotEmpty },
? clearButton(() => controller.textController.clear()) style: AppFonts.yekan13,
: null, );
counterText: '', }, controller.textController),
),
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,
),
), ),
), ),
], ],

View File

@@ -3,6 +3,6 @@ import 'package:flutter/cupertino.dart';
Widget clearButton(VoidCallback onTap) { Widget clearButton(VoidCallback onTap) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Icon(CupertinoIcons.multiply_circle, size: 24), child: Icon(CupertinoIcons.multiply_circle, size: 20),
); );
} }

View File

@@ -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),
),
],
);
}
}

View File

@@ -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/utils/utils.dart';
export 'package:rasadyar_core/presentation/widget/widget.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart';
export 'infrastructure/remote/dio_form_data.dart';
//network //network
export 'infrastructure/remote/dio_form_data.dart';
export 'infrastructure/remote/dio_remote.dart'; export 'infrastructure/remote/dio_remote.dart';
export 'infrastructure/remote/dio_response.dart'; export 'infrastructure/remote/dio_response.dart';
export 'package:dio/dio.dart' show DioException;
//utils //utils
export 'utils/logger_utils.dart'; export 'utils/logger_utils.dart';
export 'utils/safe_call_utils.dart'; export 'utils/safe_call_utils.dart';

View File

@@ -16,7 +16,11 @@ class DioRemote implements IHttpClient {
Future<void> init() async { Future<void> init() async {
final dio = Dio(BaseOptions(baseUrl: baseUrl)); final dio = Dio(BaseOptions(baseUrl: baseUrl));
if (kDebugMode) { if (kDebugMode) {
dio.interceptors.add(PrettyDioLogger()); dio.interceptors.add(PrettyDioLogger(
requestHeader: true,
responseHeader: true,
requestBody: true
));
} }
_dio = dio; _dio = dio;
} }

View File

@@ -3,51 +3,65 @@ import 'package:rasadyar_core/presentation/common/app_color.dart';
import 'package:rasadyar_core/presentation/common/app_fonts.dart'; import 'package:rasadyar_core/presentation/common/app_fonts.dart';
class RElevated extends StatelessWidget { class RElevated extends StatelessWidget {
RElevated({ const RElevated({
super.key, super.key,
required this.text, required this.text,
required this.onPressed, required this.onPressed,
this.foregroundColor, this.foregroundColor = Colors.white,
this.backgroundColor, this.backgroundColor = AppColor.blueNormal,
this.disabledBackgroundColor, this.disabledBackgroundColor,
this.disabledForegroundColor, this.disabledForegroundColor = Colors.white,
this.radius, this.radius = 8.0,
this.textStyle, this.textStyle,
this.width = 150.0, this.width = 150.0,
this.height = 56.0, this.height = 56.0,
this.isFullWidth, this.isFullWidth = false,
this.isLoading = false,
}); });
final String text; final String text;
final VoidCallback? onPressed; final VoidCallback? onPressed;
final double width; final double width;
final double height; final double height;
final bool? isFullWidth; final bool isFullWidth;
Color? foregroundColor; final Color foregroundColor;
Color? backgroundColor; final Color backgroundColor;
Color? disabledForegroundColor; final Color? disabledForegroundColor;
Color? disabledBackgroundColor; final Color? disabledBackgroundColor;
double? radius; final double radius;
TextStyle? textStyle; final TextStyle? textStyle;
final bool isLoading;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isEnabled = onPressed != null && !isLoading;
return ElevatedButton( return ElevatedButton(
onPressed: onPressed, onPressed: isEnabled ? onPressed : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? AppColor.blueNormal, backgroundColor: backgroundColor,
foregroundColor: foregroundColor ?? Colors.white, foregroundColor: foregroundColor,
disabledBackgroundColor: disabledBackgroundColor:
disabledBackgroundColor ?? AppColor.blueNormal.withAlpha(38), disabledBackgroundColor ?? backgroundColor.withAlpha(38),
disabledForegroundColor: disabledForegroundColor ?? Colors.white, disabledForegroundColor: disabledForegroundColor,
shape: RoundedRectangleBorder( 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, padding: EdgeInsets.zero,
textStyle: textStyle ?? AppFonts.yekan24, textStyle: textStyle ?? AppFonts.yekan24,
), ),
child: Text(text), child:
isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(foregroundColor),
),
)
: Text(text),
); );
} }
} }

View File

@@ -2,164 +2,106 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
@immutable enum RTextFieldVariant {
normal,
noBorder,
password,
passwordNoBorder,
}
class RTextField extends StatefulWidget { class RTextField extends StatefulWidget {
RTextField({ final TextEditingController controller;
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 String? label; 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? style;
final TextStyle? errorStyle;
final TextStyle? hintStyle; final TextStyle? hintStyle;
final TextStyle? labelStyle; final TextStyle? labelStyle;
final bool showCounter; final TextStyle? errorStyle;
final bool? isDense; final EdgeInsets? padding;
final bool? isForNumber; final FormFieldValidator<String>? validator;
final bool readonly; final void Function(String)? onChanged;
bool? obscure; final void Function(String)? onSubmitted;
final bool? enabled;
final double? radius; const RTextField({
final TextInputType? keyboardType; super.key,
final Function(String)? onChanged; required this.controller,
final Function(String)? onSubmitted; this.label,
final FormFieldValidator? validator; this.hintText,
final String? initText; this.initText,
Widget? suffixIcon; this.obscure = false,
Widget? prefixIcon; this.readonly = false,
bool? filled; this.enabled = true,
Color? filledColor; this.maxLength,
bool _isPassword = false; 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 @override
State<RTextField> createState() => _RTextFieldState(); State<RTextField> 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<RTextField> { class _RTextFieldState extends State<RTextField> {
final TextEditingController _controller = TextEditingController(); late bool obscure;
bool? obscure;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.initText != null) { if (widget.initText != null) {
_controller.text = widget.initText!; widget.controller.text = widget.initText!;
} }
obscure = widget.obscure; obscure = widget.obscure;
} }
@@ -167,51 +109,56 @@ class _RTextFieldState extends State<RTextField> {
@override @override
void didUpdateWidget(covariant RTextField oldWidget) { void didUpdateWidget(covariant RTextField oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.initText != oldWidget.initText) { if (widget.initText != null && widget.initText != oldWidget.initText) {
_controller.text = widget.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: widget.padding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
child: TextFormField( child: TextFormField(
controller: _controller, controller: widget.controller,
readOnly: widget.readonly, readOnly: widget.readonly,
minLines: widget.minLines, minLines: widget.minLines,
maxLines: widget.maxLines, maxLines: widget.maxLines,
onChanged: widget.onChanged, onChanged: widget.onChanged,
validator: widget.validator, validator: widget.validator,
enabled: widget.enabled, enabled: widget.enabled,
obscureText: obscure ?? false, obscureText: obscure,
onTapOutside: (event) { onTapOutside: (_) => FocusScope.of(context).unfocus(),
FocusScope.of(context).unfocus();
},
onFieldSubmitted: widget.onSubmitted, onFieldSubmitted: widget.onSubmitted,
maxLength: widget.maxLength, maxLength: widget.maxLength,
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
style: widget.style, style: widget.style,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
errorStyle: widget.errorStyle, errorStyle: widget.errorStyle,
errorMaxLines: 1, errorMaxLines: 1,
isDense: widget.isDense, isDense: widget.isDense,
suffixIcon: suffixIcon: _buildSuffixIcon(),
widget.suffixIcon ??
(widget._isPassword
? IconButton(
onPressed: () {
setState(() {
obscure = !obscure!;
});
},
icon: Icon(
!obscure! ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
),
)
: null),
suffixIconConstraints: widget.boxConstraints, suffixIconConstraints: widget.boxConstraints,
prefixIcon: widget.prefixIcon, prefixIcon: widget.prefixIcon,
prefixIconConstraints: widget.boxConstraints, prefixIconConstraints: widget.boxConstraints,
@@ -221,7 +168,7 @@ class _RTextFieldState extends State<RTextField> {
labelStyle: AppFonts.yekan14 labelStyle: AppFonts.yekan14
.copyWith(color: AppColor.lightGreyDarkActive) .copyWith(color: AppColor.lightGreyDarkActive)
.merge(widget.labelStyle), .merge(widget.labelStyle),
filled: widget.filled, filled: widget.filled || widget._noBorder || widget._passwordNoBorder,
fillColor: widget.filledColor, fillColor: widget.filledColor,
counter: widget.showCounter ? null : const SizedBox(), counter: widget.showCounter ? null : const SizedBox(),
hintStyle: widget.hintStyle, hintStyle: widget.hintStyle,
@@ -232,4 +179,4 @@ class _RTextFieldState extends State<RTextField> {
), ),
); );
} }
} }

View File

@@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
@@ -6,7 +7,7 @@ typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace);
typedef VoidCallback = void Function(); typedef VoidCallback = void Function();
// تعریف دقیق تابع safeCall // تعریف دقیق تابع safeCall
Future<T?> safeCall<T>({ Future<void> safeCall<T>({
required AsyncCallback<T> call, required AsyncCallback<T> call,
Function(T result)? onSuccess, Function(T result)? onSuccess,
ErrorCallback? onError, ErrorCallback? onError,
@@ -33,7 +34,7 @@ Future<T?> safeCall<T>({
} }
onSuccess?.call(result); onSuccess?.call(result);
return result;
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (showError) { if (showError) {
@@ -45,7 +46,6 @@ Future<T?> safeCall<T>({
print('safeCall error: $error\n$stackTrace'); print('safeCall error: $error\n$stackTrace');
} }
return null;
} finally { } finally {
if (showLoading) { if (showLoading) {
(onHideLoading ?? _defaultHideLoading)(); (onHideLoading ?? _defaultHideLoading)();

View File

@@ -68,24 +68,28 @@ Container mobileInspectorWidget() {
spacing: 16, spacing: 16,
children: [ children: [
RTextField( RTextField(
controller: TextEditingController(),
label: 'نام و نام خانوادگی', label: 'نام و نام خانوادگی',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'شماره مجوز', label: 'شماره مجوز',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'شماره ثبت', label: 'شماره ثبت',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'کد اقتصادی', label: 'کد اقتصادی',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,

View File

@@ -86,13 +86,17 @@ class AddSupervisionPage extends GetView<AddSupervisionLogic> {
); );
}, controller.violationSegmentsSelected), }, controller.violationSegmentsSelected),
SizedBox(height: 8), SizedBox(height: 8),
RTextField(label: 'صادر کننده پروانه'), RTextField(
controller: TextEditingController(),label: 'صادر کننده پروانه'),
SizedBox(height: 8), SizedBox(height: 8),
RTextField(label: 'شماره مجوز'), RTextField(
controller: TextEditingController(),label: 'شماره مجوز'),
SizedBox(height: 8), SizedBox(height: 8),
RTextField(label: 'شماره ثبت'), RTextField(
controller: TextEditingController(),label: 'شماره ثبت'),
SizedBox(height: 8), SizedBox(height: 8),
RTextField(label: 'کد اقتصادی'), RTextField(
controller: TextEditingController(),label: 'کد اقتصادی'),
], ],
), ),
), ),

View File

@@ -108,29 +108,34 @@ Widget violationWidget() {
child: Column( child: Column(
spacing: 16, spacing: 16,
children: [ children: [
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'توضیحات تخلف', label: 'توضیحات تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
maxLines: 3, maxLines: 3,
minLines: 3, minLines: 3,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'توضیحات تخلف', label: 'توضیحات تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,

View File

@@ -69,29 +69,34 @@ Container violationWidget() {
child: Column( child: Column(
spacing: 16, spacing: 16,
children: [ children: [
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'توضیحات تخلف', label: 'توضیحات تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
maxLines: 3, maxLines: 3,
minLines: 3, minLines: 3,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'عنوان تخلف', label: 'عنوان تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,
), ),
RTextField( RTextField(
controller: TextEditingController(),
label: 'توضیحات تخلف', label: 'توضیحات تخلف',
filled: true, filled: true,
filledColor: AppColor.whiteLight, filledColor: AppColor.whiteLight,