feat : captcha widget.dart

This commit is contained in:
2025-05-14 15:01:03 +03:30
parent a132b21b18
commit 3a017b5956
10 changed files with 57 additions and 305 deletions

View File

@@ -1,5 +1,5 @@
enum ApiEnvironment {
dam(url: 'https://api.dam.rasadyar.net');
dam(url: 'https://api.dam.rasadyar.net/');
const ApiEnvironment({required this.url});

View File

@@ -8,9 +8,12 @@ class DioRemoteManager {
DioRemote? _currentClient;
ApiEnvironment? _currentEnv;
DioRemote setEnvironment(ApiEnvironment env) {
Future<DioRemote> setEnvironment([
ApiEnvironment env = ApiEnvironment.dam,
]) async {
if (_currentEnv != env) {
_currentClient = DioRemote(env.baseUrl);
await _currentClient?.init();
_currentEnv = env;
}
return _currentClient!;
@@ -30,10 +33,10 @@ class DioRemoteManager {
Future<void> switchAuthEnvironment(ApiEnvironment env) async {
final manager = diAuth.get<DioRemoteManager>();
final dioRemote = manager.setEnvironment(env);
final dioRemote = await manager.setEnvironment(env);
if (diAuth.isRegistered<AuthRepositoryImpl>()) {
await diAuth.unregister<AuthRepositoryImpl>();
await diAuth.unregister<AuthRepositoryImpl>();
}
diAuth.registerLazySingleton<AuthRepositoryImpl>(

View File

@@ -12,14 +12,10 @@ Future<void> setupAuthDI() async {
diAuth.registerLazySingleton(() => DioRemoteManager());
final manager = diAuth.get<DioRemoteManager>();
final dioRemote = manager.setEnvironment(ApiEnvironment.dam);
diAuth.registerLazySingleton<AuthRepositoryImpl>(
final dioRemote = await manager.setEnvironment(ApiEnvironment.dam);
diAuth.registerCachedFactory<AuthRepositoryImpl>(
() => AuthRepositoryImpl(dioRemote),
);
diAuth.registerLazySingleton(() => AuthService());
diAuth.registerLazySingleton(() => TokenStorageService());
//hive
//await diAuth.registerCachedFactoryAsync(() async=>await ,)
}

View File

@@ -34,17 +34,18 @@ class AuthRepositoryImpl implements AuthRepository {
@override
Future<CaptchaResponseModel?> captcha() async {
final response = await safeCall<DioResponse<CaptchaResponseModel>>(
final response = await safeCall(
call:
() async => await _httpClient.post<CaptchaResponseModel>(
'$_BASE_URL/login/',
'captcha/',
headers: {'Content-Type': 'application/json'},
fromJson: CaptchaResponseModel.fromJson
),
onSuccess: (response) {
iLog(response);
},
onError: (error, trace) {
throw Exception('Error during sign in: $error');
throw Exception('Error during captcha : $error');
},
);

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rasadyar_auth/auth.dart';
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
import 'package:rasadyar_core/core.dart';
enum AuthType { useAndPass, otp }
@@ -18,8 +20,7 @@ class AuthLogic extends GetxController {
Rx<TextEditingController> phoneOtpNumberController =
TextEditingController().obs;
Rx<TextEditingController> otpCodeController = TextEditingController().obs;
CaptchaController captchaController = CaptchaController();
CaptchaController captchaOtpController = CaptchaController();
RxnString phoneNumber = RxnString(null);
RxnString password = RxnString(null);
@@ -32,6 +33,8 @@ class AuthLogic extends GetxController {
RxInt secondsRemaining = 120.obs;
Timer? _timer;
AuthRepositoryImpl authRepository = diAuth.get<AuthRepositoryImpl>();
void startTimer() {
_timer?.cancel();
secondsRemaining.value = 120;
@@ -55,6 +58,12 @@ class AuthLogic extends GetxController {
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
// TODO: implement onReady
@@ -66,4 +75,6 @@ class AuthLogic extends GetxController {
_timer?.cancel();
super.onClose();
}
}

View File

@@ -1,12 +1,12 @@
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_core/core.dart';
import 'logic.dart';
class AuthPage extends GetView<AuthLogic> {
const AuthPage({super.key});
@@ -21,7 +21,7 @@ class AuthPage extends GetView<AuthLogic> {
ObxValue((types) {
switch (types.value) {
case AuthType.otp:
return otpForm();
//return otpForm();
case AuthType.useAndPass:
return useAndPassFrom();
}
@@ -222,7 +222,7 @@ class AuthPage extends GetView<AuthLogic> {
}, controller.passwordController),
SizedBox(height: 26),
CaptchaWidget(controller: controller.captchaController),
CaptchaWidget(),
SizedBox(height: 23),
RElevated(
@@ -237,9 +237,6 @@ class AuthPage extends GetView<AuthLogic> {
initialEntryMode: PersianDatePickerEntryMode.calendarOnly,
initialDatePickerMode: PersianDatePickerMode.year,
);
if (data.value.currentState?.validate() == true &&
controller.captchaController.validate()) {}
},
width: Get.width,
height: 48,
@@ -251,7 +248,7 @@ class AuthPage extends GetView<AuthLogic> {
}, controller.formKey);
}
Widget otpForm() {
/* Widget otpForm() {
return ObxValue((status) {
switch (status.value) {
case OtpStatus.init:
@@ -262,7 +259,7 @@ class AuthPage extends GetView<AuthLogic> {
return confirmCodeForm();
}
}, controller.otpStatus);
}
}*/
Widget sendCodeForm() {
return ObxValue((data) {
@@ -335,14 +332,13 @@ class AuthPage extends GetView<AuthLogic> {
SizedBox(height: 26),
CaptchaWidget(controller: controller.captchaOtpController),
CaptchaWidget(),
SizedBox(height: 23),
RElevated(
text: 'ارسال رمز یکبار مصرف',
onPressed: () {
if (data.value.currentState?.validate() == true &&
controller.captchaOtpController.validate()) {
if (data.value.currentState?.validate() == true) {
controller.otpStatus.value = OtpStatus.sent;
controller.startTimer();
}
@@ -473,7 +469,6 @@ class AuthPage extends GetView<AuthLogic> {
TapGestureRecognizer()
..onTap = () {
controller.otpStatus.value = OtpStatus.init;
controller.captchaOtpController.clear();
},
text: ' ویرایش',
style: AppFonts.yekan14.copyWith(
@@ -489,8 +484,7 @@ class AuthPage extends GetView<AuthLogic> {
text: 'ورود',
onPressed: () {
if (controller.formKeyOtp.value.currentState?.validate() ==
true &&
controller.captchaOtpController.validate()) {}
true) {}
},
width: Get.width,
height: 48,
@@ -514,11 +508,4 @@ class AuthPage extends GetView<AuthLogic> {
],
);
}
Widget clearButton(VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Icon(CupertinoIcons.multiply_circle, size: 24),
);
}
}

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:rasadyar_core/infrastructure/remote/interfaces/i_form_data.dart';
@@ -15,7 +16,9 @@ class DioRemote implements IHttpClient {
@override
Future<void> init() async {
final dio = Dio(BaseOptions(baseUrl: baseUrl));
dio.interceptors.add(PrettyDioLogger());
if (kDebugMode) {
dio.interceptors.add(PrettyDioLogger());
}
_dio = dio;
}
@@ -40,6 +43,7 @@ class DioRemote implements IHttpClient {
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
T Function(Map<String, dynamic> json)? fromJson,
Map<String, String>? headers,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
@@ -52,6 +56,15 @@ class DioRemote implements IHttpClient {
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
if (fromJson != null) {
final rawData = response.data;
final parsedData =
rawData is Map<String, dynamic> ? fromJson(rawData) : null;
response.data = parsedData;
return DioResponse<T>(response);
}
return DioResponse<T>(response);
}

View File

@@ -1,266 +0,0 @@
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:rasadyar_core/presentation/common/app_color.dart';
import 'package:rasadyar_core/presentation/common/app_fonts.dart';
class CaptchaController {
int? captchaCode;
TextEditingController textController = TextEditingController();
GlobalKey<FormState> formKey = GlobalKey<FormState>();
late Function() refreshCaptcha;
bool validate() {
if (formKey.currentState?.validate() == true) {
return true;
}
return false;
}
String get enteredText => textController.text;
bool isCorrect() {
return textController.text == captchaCode.toString();
}
void clear() {
textController.clear();
}
}
class RandomLinePainter extends CustomPainter {
final Random random = Random();
final Paint linePaint;
final List<Offset> points;
RandomLinePainter({
required this.points,
required Color lineColor,
double strokeWidth = 2.0,
}) : linePaint =
Paint()
..color = lineColor
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
@override
void paint(Canvas canvas, Size size) {
final path = Path();
if (points.isNotEmpty) {
path.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
canvas.drawPath(path, linePaint);
}
}
@override
bool shouldRepaint(RandomLinePainter oldDelegate) => true;
}
class CaptchaWidget extends StatefulWidget {
final CaptchaController controller;
final bool autoValidateMode;
const CaptchaWidget({
required this.controller,
this.autoValidateMode = false,
super.key,
});
@override
_CaptchaWidgetState createState() => _CaptchaWidgetState();
}
class _CaptchaWidgetState extends State<CaptchaWidget> {
late List<Offset> points;
late List<Offset> points1;
late List<Offset> points2;
bool isOnError = false;
@override
void initState() {
super.initState();
generateLines();
getRandomSixDigitNumber();
// Set the refresh function in the controller
widget.controller.refreshCaptcha = () {
getRandomSixDigitNumber();
generateLines();
setState(() {});
};
}
void generateLines() {
points = generateRandomLine();
points1 = generateRandomLine();
points2 = generateRandomLine();
setState(() {});
}
List<Offset> generateRandomLine() {
final random = Random();
int pointCount = random.nextInt(10) + 5;
List<Offset> points = [];
double previousY = 0;
for (int i = 0; i < pointCount; i++) {
double x = (i / (pointCount - 1)) * 135;
if (i == 0) {
previousY = 24;
} else {
double change = (random.nextDouble() * 20) - 10;
previousY = max(5, min(43, previousY + change));
}
points.add(Offset(x, previousY));
}
return points;
}
void getRandomSixDigitNumber() {
final random = Random();
widget.controller.captchaCode = random.nextInt(900000) + 100000;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 135,
height: 48,
decoration: BoxDecoration(
color: AppColor.whiteNormalHover,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: Stack(
alignment: AlignmentDirectional.center,
children: [
CustomPaint(
painter: RandomLinePainter(
points: points,
lineColor: Colors.blue,
strokeWidth: 1.0,
),
size: const Size(double.infinity, double.infinity),
),
CustomPaint(
painter: RandomLinePainter(
points: points1,
lineColor: Colors.green,
strokeWidth: 1.0,
),
size: const Size(double.infinity, double.infinity),
),
CustomPaint(
painter: RandomLinePainter(
points: points2,
lineColor: Colors.red,
strokeWidth: 1.0,
),
size: const Size(double.infinity, double.infinity),
),
Text(
widget.controller.captchaCode.toString(),
style: AppFonts.yekan24,
),
],
),
),
const SizedBox(height: 20),
IconButton(
padding: EdgeInsets.zero,
onPressed: widget.controller.refreshCaptcha,
icon: Icon(CupertinoIcons.refresh, size: 16),
),
Expanded(
child: Form(
key: widget.controller.formKey,
autovalidateMode:
widget.autoValidateMode
? AutovalidateMode.onUserInteraction
: AutovalidateMode.disabled,
child: TextFormField(
controller: widget.controller.textController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
gapPadding: 11,
),
labelText: 'کد امنیتی',
labelStyle: AppFonts.yekan13,
errorStyle: AppFonts.yekan10.copyWith(
color: AppColor.redNormal,
fontSize: 8,
),
suffixIconConstraints: BoxConstraints(
maxHeight: 24,
minHeight: 24,
maxWidth: 24,
minWidth: 24,
),
suffix:
widget.controller.textController.text.trim().isNotEmpty
? clearButton(() {
widget.controller.textController.clear();
setState(() {});
})
: null,
counterText: '',
),
keyboardType: TextInputType.numberWithOptions(
decimal: false,
signed: false,
),
maxLines: 1,
maxLength: 6,
onChanged: (value) {
if (isOnError) {
isOnError = !isOnError;
widget.controller.formKey.currentState?.reset();
widget.controller.textController.text = value;
}
setState(() {});
},
validator: (value) {
if (value == null || value.isEmpty) {
isOnError = true;
return 'کد امنیتی را وارد کنید';
}
if (value != widget.controller.captchaCode.toString()) {
isOnError = true;
return '⚠️کد امنیتی وارد شده اشتباه است';
}
return null;
},
style: AppFonts.yekan13,
),
),
),
],
);
}
Widget clearButton(VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Icon(CupertinoIcons.multiply_circle, size: 18),
);
}
}

View File

@@ -5,7 +5,6 @@ export 'buttons/elevated.dart';
export 'buttons/outline_elevated.dart';
export 'buttons/outline_elevated_icon.dart';
export 'buttons/text_button.dart';
export 'captcha/captcha_widget.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet.dart';
export 'draggable_bottom_sheet/draggable_bottom_sheet_controller.dart';
export 'draggable_bottom_sheet/bottom_sheet_manger.dart';