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

@@ -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/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<void> setupAuthDI() async {
diAuth.registerCachedFactory<AuthRepositoryImpl>(
() => AuthRepositoryImpl(dioRemote),
);
diAuth.registerLazySingleton(() => AuthService());
diAuth.registerLazySingleton(() => TokenStorageService());
diAuth.registerLazySingleton<AuthService>(() => AuthService());
diAuth.registerLazySingleton<TokenStorageService>(() => TokenStorageService());
diAuth.registerLazySingleton<DioErrorHandler>(() => DioErrorHandler());
}

View File

@@ -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<String, dynamic> json) =>
_$LoginRequestModelFromJson(json);
const LoginRequestModel._();
String get formattedCaptchaKey => 'rest_captcha_$captchaKey.0';
}

View File

@@ -14,64 +14,34 @@ class AuthRepositoryImpl implements AuthRepository {
Future<AuthResponseModel?> login({
required Map<String, dynamic> authRequest,
}) async {
final response = await safeCall<DioResponse<AuthResponseModel>>(
call:
() async => await _httpClient.post<AuthResponseModel>(
'$_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<AuthResponseModel>(
'$_BASE_URL/login/',
data: authRequest,
fromJson: AuthResponseModel.fromJson,
headers: {'Content-Type': 'application/json'},
);
return response?.data;
return res.data;
}
@override
Future<CaptchaResponseModel?> captcha() async {
final response = await safeCall<CaptchaResponseModel?>(
call: () async {
var res = await _httpClient.post<CaptchaResponseModel?>(
'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<CaptchaResponseModel?>(
'captcha/',
fromJson: CaptchaResponseModel.fromJson,
);
return response;
return res.data;
}
@override
Future<AuthResponseModel?> loginWithRefreshToken({
required Map<String, dynamic> authRequest,
}) async {
final response = await safeCall<DioResponse<AuthResponseModel>>(
call:
() async => await _httpClient.post<AuthResponseModel>(
'$_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<AuthResponseModel>(
'$_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<bool> hasAuthenticated() async {
final response = await safeCall<DioResponse<bool>>(
call:
() async => await _httpClient.get<bool>(
'$_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<bool>(
'$_BASE_URL/login/',
headers: {'Content-Type': 'application/json'},
);
return response?.data ?? false;
return response.data ?? false;
}
}

View File

@@ -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';

View File

@@ -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<GlobalKey<FormState>> formKey = GlobalKey<FormState>().obs;
GlobalKey<FormState> formKey = GlobalKey<FormState>();
Rx<GlobalKey<FormState>> formKeyOtp = GlobalKey<FormState>().obs;
Rx<GlobalKey<FormState>> formKeySentOtp = GlobalKey<FormState>().obs;
Rx<TextEditingController> phoneNumberController = TextEditingController().obs;
@@ -21,11 +27,12 @@ class AuthLogic extends GetxController {
TextEditingController().obs;
Rx<TextEditingController> otpCodeController = TextEditingController().obs;
var captchaController = Get.find<CaptchaWidgetLogic>();
RxnString phoneNumber = RxnString(null);
RxnString password = RxnString(null);
RxBool isOnError = false.obs;
RxBool hidePassword = true.obs;
RxBool isLoading = false.obs;
TokenStorageService tokenStorageService = diAuth.get<TokenStorageService>();
Rx<AuthType> authType = AuthType.useAndPass.obs;
Rx<AuthStatus> authStatus = AuthStatus.init.obs;
Rx<OtpStatus> 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<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/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<AuthLogic> {
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<AuthLogic> {
}
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<AuthLogic> {
),
);
}, 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 '../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());
}),
),
];

View File

@@ -6,7 +6,8 @@ import 'package:rasadyar_core/core.dart';
class CaptchaWidgetLogic extends GetxController
with StateMixin<CaptchaResponseModel> {
TextEditingController textController = TextEditingController();
Rx<TextEditingController> textController = TextEditingController().obs;
RxnString captchaKey = RxnString();
GlobalKey<FormState> formKey = GlobalKey<FormState>();
AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>();
@@ -17,17 +18,19 @@ class CaptchaWidgetLogic extends GetxController
getCaptcha();
}
@override
void onClose() {
textController.value.dispose();
super.onClose();
}
Future<void> 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) {

View File

@@ -8,9 +8,7 @@ import 'package:rasadyar_core/core.dart';
import 'logic.dart';
class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
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<CaptchaWidgetLogic> {
children: [
Container(
width: 135,
height: 48,
height: 50,
clipBehavior: Clip.antiAliasWithSaveLayer,
decoration: BoxDecoration(
color: AppColor.whiteNormalHover,
@@ -27,75 +25,62 @@ class CaptchaWidget extends GetView<CaptchaWidgetLogic> {
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),
),
),
],

View File

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

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