From cde701f04dedfc57e973a31ab1dd0296e577e171 Mon Sep 17 00:00:00 2001 From: Aidaiym Date: Fri, 26 Jul 2024 14:17:53 +0600 Subject: [PATCH] auth with all mocks --- .../data/repository/auth_repositoty_impl.dart | 38 +++++ app/lib/app/domain/domain.dart | 2 + .../domain/repository/auth_repository.dart | 8 + .../domain/usecase/email_login_usecase.dart | 13 ++ .../usecase/fetch_sms_code_use_case.dart | 22 +++ .../app/presentation/cubit/auth_cubit.dart | 25 +++ app/lib/app/presentation/view/app_view.dart | 2 + app/lib/components/components.dart | 1 + .../components/forms/custom_text_field.dart | 33 ++++ app/lib/config/router/app_router.dart | 10 +- app/lib/core/analytics/mq_analytic_keys.dart | 2 +- app/lib/l10n/arb/app_ar.arb | 8 +- app/lib/l10n/arb/app_en.arb | 8 +- app/lib/l10n/arb/app_id.arb | 8 +- app/lib/l10n/arb/app_kk.arb | 8 +- app/lib/l10n/arb/app_ky.arb | 10 +- app/lib/l10n/arb/app_ru.arb | 8 +- app/lib/l10n/arb/app_tr.arb | 8 +- .../login/presentation/presentation.dart | 1 + .../login/presentation/view/sign_in_view.dart | 150 ++++++++++-------- .../view/verification_code_view.dart | 96 +++++++++++ app/lib/utils/reg_exp/app_reg_exp.dart | 1 + app/pubspec.lock | 8 + app/pubspec.yaml | 1 + app/test/helpers/pump_app.dart | 4 + app/test/widget_test.dart | 4 + 26 files changed, 402 insertions(+), 77 deletions(-) create mode 100644 app/lib/app/domain/usecase/email_login_usecase.dart create mode 100644 app/lib/app/domain/usecase/fetch_sms_code_use_case.dart create mode 100644 app/lib/components/forms/custom_text_field.dart create mode 100644 app/lib/modules/login/presentation/view/verification_code_view.dart diff --git a/app/lib/app/data/repository/auth_repositoty_impl.dart b/app/lib/app/data/repository/auth_repositoty_impl.dart index 7b4ac921..36d0f75a 100644 --- a/app/lib/app/data/repository/auth_repositoty_impl.dart +++ b/app/lib/app/data/repository/auth_repositoty_impl.dart @@ -28,6 +28,44 @@ final class AuthRepositoryImpl implements AuthRepository { } } + @override + Future loginWithEmail(String email) async { + try { + await remoteDataSource.loginWithEmail(email); + } catch (e, s) { + MqCrashlytics.report(e, s); + log('signWithEmail: error: $e\n$s'); + } + } + + @override + Future> fetchSmsCode({ + required String code, + required String languageCode, + required Gender gender, + }) async { + try { + final res = await remoteDataSource.fetchSmsCode(code: code, languageCode: languageCode, gender: gender); + + return res.fold( + Left.new, + (r) { + final userEntity = UserEntity( + accessToken: r.accessToken, + username: r.username, + gender: r.gender, + localeCode: r.localeCode, + ); + return Right(userEntity); + }, + ); + } catch (e, s) { + log('signWithemail: error: $e\n$s'); + MqCrashlytics.report(e, s); + return Left(AuthenticationExc(message: e.toString())); + } + } + @override Future> signWithGoogle( String languageCode, diff --git a/app/lib/app/domain/domain.dart b/app/lib/app/domain/domain.dart index a0ad0e83..b5d53180 100644 --- a/app/lib/app/domain/domain.dart +++ b/app/lib/app/domain/domain.dart @@ -12,5 +12,7 @@ export 'usecase/ser_user_data_user_case.dart'; export 'usecase/patch_gender_user_case.dart'; export 'usecase/patch_locale_code_use_case.dart'; export 'usecase/logout_use_case.dart'; +export 'usecase/email_login_usecase.dart'; +export 'usecase/fetch_sms_code_use_case.dart'; export 'entity/user_entity.dart'; export 'entity/user_data_entity.dart'; diff --git a/app/lib/app/domain/repository/auth_repository.dart b/app/lib/app/domain/repository/auth_repository.dart index 36e95964..dfcb60fe 100644 --- a/app/lib/app/domain/repository/auth_repository.dart +++ b/app/lib/app/domain/repository/auth_repository.dart @@ -4,6 +4,14 @@ import 'package:my_quran/core/core.dart'; abstract class AuthRepository { UserEntity? get init; + Future loginWithEmail(String email); + + Future> fetchSmsCode({ + required String code, + required String languageCode, + required Gender gender, + }); + Future> signWithGoogle( String languageCode, Gender gender, diff --git a/app/lib/app/domain/usecase/email_login_usecase.dart b/app/lib/app/domain/usecase/email_login_usecase.dart new file mode 100644 index 00000000..43a40de3 --- /dev/null +++ b/app/lib/app/domain/usecase/email_login_usecase.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; + +@immutable +final class EmailLoginUseCase { + const EmailLoginUseCase(this.repository); + + final AuthRepository repository; + + Future call(String email) { + return repository.loginWithEmail(email); + } +} diff --git a/app/lib/app/domain/usecase/fetch_sms_code_use_case.dart b/app/lib/app/domain/usecase/fetch_sms_code_use_case.dart new file mode 100644 index 00000000..3539e1b6 --- /dev/null +++ b/app/lib/app/domain/usecase/fetch_sms_code_use_case.dart @@ -0,0 +1,22 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/core/core.dart'; + +@immutable +final class FetchSmsCodeUseCase { + const FetchSmsCodeUseCase(this.repository); + + final AuthRepository repository; + + Future> call({ + required String code, + required String languageCode, + required Gender gender, + }) { + return repository.fetchSmsCode( + code: code, + languageCode: languageCode, + gender: gender, + ); + } +} diff --git a/app/lib/app/presentation/cubit/auth_cubit.dart b/app/lib/app/presentation/cubit/auth_cubit.dart index 2e4c27c7..dd37d3ec 100644 --- a/app/lib/app/presentation/cubit/auth_cubit.dart +++ b/app/lib/app/presentation/cubit/auth_cubit.dart @@ -17,6 +17,8 @@ class AuthCubit extends Cubit { this.patchGenderUseCase, this.patchLocaleCodeUseCase, this.logoutUseCase, + this.loginUsecase, + this.fetchSmsCodeUseCase, ) : super(AuthState(user: getInitialUserUseCase.call)); final GetInitialUserUseCase getInitialUserUseCase; @@ -26,6 +28,29 @@ class AuthCubit extends Cubit { final PatchGenderUseCase patchGenderUseCase; final PatchLocaleCodeUseCase patchLocaleCodeUseCase; final LogoutUseCase logoutUseCase; + final EmailLoginUseCase loginUsecase; + final FetchSmsCodeUseCase fetchSmsCodeUseCase; + + Future login(String email) async { + try { + await loginUsecase(email); + } catch (e) { + emit(state.copyWith(exception: Exception(e))); + } + } + + Future fetchSmsCode(String code) async { + final user = await fetchSmsCodeUseCase( + code: code, + languageCode: state.currentLocale.languageCode, + gender: state.gender, + ); + user.fold( + (l) => emit(state.copyWith(exception: l)), + (r) => emit(state.copyWith(user: r)), + ); + return state; + } Future signInWithGoogle() async { final user = await googleSignIn( diff --git a/app/lib/app/presentation/view/app_view.dart b/app/lib/app/presentation/view/app_view.dart index a76da2ef..d91cea1c 100644 --- a/app/lib/app/presentation/view/app_view.dart +++ b/app/lib/app/presentation/view/app_view.dart @@ -68,6 +68,8 @@ class MyApp extends StatelessWidget { PatchGenderUseCase(context.read()), PatchLocaleCodeUseCase(context.read()), LogoutUseCase(context.read()), + EmailLoginUseCase(context.read()), + FetchSmsCodeUseCase(context.read()), ), ), BlocProvider( diff --git a/app/lib/components/components.dart b/app/lib/components/components.dart index 113de7f5..d55d5934 100644 --- a/app/lib/components/components.dart +++ b/app/lib/components/components.dart @@ -8,3 +8,4 @@ export 'indicators/dot_indicator.dart'; export 'audio/seek_bar.dart'; export 'audio/audio_center_button.dart'; export 'alert/confirmation_dialog.dart'; +export 'forms/custom_text_field.dart'; diff --git a/app/lib/components/forms/custom_text_field.dart b/app/lib/components/forms/custom_text_field.dart new file mode 100644 index 00000000..bf9ab7eb --- /dev/null +++ b/app/lib/components/forms/custom_text_field.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class CustomTextFormField extends StatelessWidget { + const CustomTextFormField({ + this.controller, + this.labelText, + this.obscureText = false, + this.suffixIcon, + this.validator, + super.key, + }); + + final bool obscureText; + final TextEditingController? controller; + final String? labelText; + final Widget? suffixIcon; + final String? Function(String?)? validator; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: labelText, + border: const OutlineInputBorder(), + suffixIcon: suffixIcon, + errorMaxLines: 3, + ), + obscureText: obscureText, + validator: validator, + ); + } +} diff --git a/app/lib/config/router/app_router.dart b/app/lib/config/router/app_router.dart index c14ac9d5..2c924b1c 100644 --- a/app/lib/config/router/app_router.dart +++ b/app/lib/config/router/app_router.dart @@ -26,6 +26,7 @@ final class AppRouter { static const hatimRead = 'hatim-read'; static const login = 'login'; static const loginWihtSoccial = 'login-with-soccial'; + static const verificationCode = 'verification-code'; static const settingsPage = 'settings'; static const langSettings = 'lang-settings'; @@ -55,7 +56,12 @@ final class AppRouter { GoRoute( path: '/$loginWihtSoccial', name: loginWihtSoccial, - builder: (context, state) => const SignInView(), + builder: (context, state) => SignInView(), + ), + GoRoute( + path: '/$verificationCode', + name: verificationCode, + builder: (context, state) => VerificationCodeView(), ), GoRoute( path: '/$devModeView', @@ -114,7 +120,7 @@ final class AppRouter { redirect: (context, state) { final path = state.matchedLocation; if (!context.read().isAuthedticated) { - if (!path.contains(devModeView) && !path.contains(loginWihtSoccial)) { + if (!path.contains(devModeView) && !path.contains(loginWihtSoccial) && !path.contains(verificationCode)) { return '/$login'; } } diff --git a/app/lib/core/analytics/mq_analytic_keys.dart b/app/lib/core/analytics/mq_analytic_keys.dart index f88831e9..81926785 100644 --- a/app/lib/core/analytics/mq_analytic_keys.dart +++ b/app/lib/core/analytics/mq_analytic_keys.dart @@ -29,7 +29,7 @@ final class AnalyticKey { // action static const tapLogout = 'tap_logout'; - static const tapLoginWithSoccial = 'tap_login_with_soccial'; + static const tapLogin = 'tap_login'; static const tapPrivacyPolicy = 'tap_privacy_policy'; static const tapQuranReadSettings = 'tap_quran_read_settings'; static const showAmin = 'show_amin'; diff --git a/app/lib/l10n/arb/app_ar.arb b/app/lib/l10n/arb/app_ar.arb index 2ce8ffe0..147957a9 100644 --- a/app/lib/l10n/arb/app_ar.arb +++ b/app/lib/l10n/arb/app_ar.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "نسخة جديدة من التطبيق متاحة.", "remindMeLater": "ذكرني لاحقاً", "updateNow": "تحديث الآن", - "hatimNotAvailable": "خاتم غير متاح" + "hatimNotAvailable": "خاتم غير متاح", + "enterEmailToLogin": "الرجاء إدخال بريدك الإلكتروني لتسجيل الدخول إلى التطبيق الخاص بنا.", + "email": "البريد الإلكتروني", + "emailRequired": "البريد الإلكتروني مطلوب.", + "invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح.", + "signIn": "تسجيل الدخول", + "orContinueWith": "أو تابع مع" } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_en.arb b/app/lib/l10n/arb/app_en.arb index d81ead11..05e5517c 100644 --- a/app/lib/l10n/arb/app_en.arb +++ b/app/lib/l10n/arb/app_en.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "A new version of the app is available.", "remindMeLater": "Remind me later", "updateNow": "Update now", - "hatimNotAvailable": "Hatim is not available." + "hatimNotAvailable": "Hatim is not available.", + "enterEmailToLogin": "Please enter your email to log in to our application.", + "email": "Email", + "emailRequired": "Email is required.", + "invalidEmail": "Please enter a valid email address.", + "signIn": "Sign in", + "orContinueWith": "Or continue with" } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_id.arb b/app/lib/l10n/arb/app_id.arb index 6871a509..9d173cd2 100644 --- a/app/lib/l10n/arb/app_id.arb +++ b/app/lib/l10n/arb/app_id.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "Versi baru dari aplikasi tersedia.", "remindMeLater": "Ingatkan saya nanti", "updateNow": "Perbarui sekarang", - "hatimNotAvailable": "Hatim tidak tersedia." + "hatimNotAvailable": "Hatim tidak tersedia.", + "enterEmailToLogin": "Silakan masukkan email Anda untuk masuk ke aplikasi kami.", + "email": "Email", + "emailRequired": "Email diperlukan.", + "invalidEmail": "Silakan masukkan alamat email yang valid.", + "signIn": "Masuk", + "orContinueWith": "Atau lanjutkan dengan" } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_kk.arb b/app/lib/l10n/arb/app_kk.arb index 63c30cd4..9264163d 100644 --- a/app/lib/l10n/arb/app_kk.arb +++ b/app/lib/l10n/arb/app_kk.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "Қолданбаның жаңа нұсқасы қолайлы.", "remindMeLater": "Кейін еске салу", "updateNow": "Қазір жаңарту", - "hatimNotAvailable": "Хатым жетімді емес." + "hatimNotAvailable": "Хатым жетімді емес.", + "enterEmailToLogin": "Біздің қолданбамызға кіру үшін emailңізді енгізіңіз.", + "email": "Электрондық пошта", + "emailRequired": "Электрондық пошта қажет.", + "invalidEmail": "Жарамды электрондық пошта мекенжайын енгізіңіз.", + "signIn": "Кіру", + "orContinueWith": "Немесе жалғастыру" } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_ky.arb b/app/lib/l10n/arb/app_ky.arb index 47cd19e7..b9895140 100644 --- a/app/lib/l10n/arb/app_ky.arb +++ b/app/lib/l10n/arb/app_ky.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "Колдонмонун жаңы версиясы жеткиликтүү", "remindMeLater": "Кийинчерээк эстетип коюу", "updateNow": "Азыр жаңыртуу", - "hatimNotAvailable": "Хатымга катышуу азырынча жеткиликтүү эмес." - } \ No newline at end of file + "hatimNotAvailable": "Хатымга катышуу азырынча жеткиликтүү эмес.", + "enterEmailToLogin": "Тиркемеге кирүү үчүн электрондук почтаңызды жазыңыз.", + "email": "Электрондук почта", + "emailRequired": "Электрондук почтаңызды жазыңыз.", + "invalidEmail": "Туура электрондук почта дарегин киргизиңиз.", + "signIn": "Кирүү", + "orContinueWith": "Же" +} \ No newline at end of file diff --git a/app/lib/l10n/arb/app_ru.arb b/app/lib/l10n/arb/app_ru.arb index 46afede8..c657302b 100644 --- a/app/lib/l10n/arb/app_ru.arb +++ b/app/lib/l10n/arb/app_ru.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "Доступна новая версия приложения.", "remindMeLater": "Напомнить позже", "updateNow": "Обновить сейчас", - "hatimNotAvailable": "Хатим недоступен." + "hatimNotAvailable": "Хатим недоступен.", + "enterEmailToLogin": "Введите ваш email для входа в приложение.", + "email": "Эл. почта", + "emailRequired": "Требуется электронная почта.", + "invalidEmail": "Пожалуйста, введите действующий адрес электронной почты.", + "signIn": "Войти", + "orContinueWith": "Или продолжить с" } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_tr.arb b/app/lib/l10n/arb/app_tr.arb index 17d2ce90..5cbc7478 100644 --- a/app/lib/l10n/arb/app_tr.arb +++ b/app/lib/l10n/arb/app_tr.arb @@ -87,5 +87,11 @@ "recommendedVersionDescription": "Uygulamanın yeni bir sürümü mevcut.", "remindMeLater": "Daha sonra hatırlat", "updateNow": "Şimdi güncelle", - "hatimNotAvailable": "Hatim mevcut değil." + "hatimNotAvailable": "Hatim mevcut değil.", + "enterEmailToLogin": "Uygulamamıza giriş yapmak için lütfen e-posta adresinizi girin.", + "email": "E-posta", + "emailRequired": "E-posta gereklidir.", + "invalidEmail": "Lütfen geçerli bir e-posta adresi girin.", + "signIn": "Giriş Yap", + "orContinueWith": "Veya ile devam et" } \ No newline at end of file diff --git a/app/lib/modules/login/presentation/presentation.dart b/app/lib/modules/login/presentation/presentation.dart index 2c43ebb8..f357cd34 100644 --- a/app/lib/modules/login/presentation/presentation.dart +++ b/app/lib/modules/login/presentation/presentation.dart @@ -1,5 +1,6 @@ export 'cubit/login_cubit.dart'; export 'view/login_view.dart'; export 'view/sign_in_view.dart'; +export 'view/verification_code_view.dart'; export 'widgets/select_gender.dart'; export 'widgets/select_lang.dart'; diff --git a/app/lib/modules/login/presentation/view/sign_in_view.dart b/app/lib/modules/login/presentation/view/sign_in_view.dart index 61cb2c79..646106e3 100644 --- a/app/lib/modules/login/presentation/view/sign_in_view.dart +++ b/app/lib/modules/login/presentation/view/sign_in_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,6 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:loader_overlay/loader_overlay.dart'; import 'package:mq_ci_keys/mq_ci_keys.dart'; import 'package:my_quran/app/app.dart'; +import 'package:my_quran/components/components.dart'; import 'package:my_quran/config/config.dart'; import 'package:my_quran/constants/contants.dart'; @@ -15,63 +17,98 @@ import 'package:my_quran/theme/theme.dart'; import 'package:my_quran/utils/urils.dart'; class SignInView extends StatelessWidget { - const SignInView({super.key}); + SignInView({super.key}); + + final emailController = TextEditingController(); + final formKey = GlobalKey(); @override Widget build(BuildContext context) { return Scaffold( key: const Key(MqKeys.signInView), - body: BlocListener( - listener: (context, state) { - if (state.user != null) { - context.read().setUserData(state.user!); - context.goNamed(AppRouter.home); - } else if (state.exception != null) { - AppAlert.showErrorDialog( - context, - errorText: state.exception.toString(), - ); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + body: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: ListView( + padding: const EdgeInsets.all(16), children: [ - const SizedBox(height: 70), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 80, vertical: 30), - child: Assets.images.splash.image(), + const SizedBox(height: 100), + Align( + child: Text( + '${context.l10n.welcome}!', + style: context.titleLarge!.copyWith(fontSize: 24, fontWeight: FontWeight.bold), + ), ), - const SizedBox(height: 32), - Text( - '${context.l10n.welcome}!', - style: context.titleLarge!.copyWith(color: context.colors.primary, fontSize: 30), + const SizedBox(height: 16), + Align( + child: Text(context.l10n.enterEmailToLogin), ), - const SizedBox(height: 33), + const SizedBox(height: 40), Text( - context.l10n.signInWith, - textAlign: TextAlign.center, - style: context.bodyLarge!.copyWith(color: context.colors.shadow, fontSize: 17), + context.l10n.email, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), ), - Padding( - padding: const EdgeInsets.all(16), - child: ElevatedButton( - key: Key(MqKeys.loginTypeName('google')), - onPressed: () async { + const SizedBox(height: 8), + CustomTextFormField( + controller: emailController, + labelText: context.l10n.email, + validator: (value) { + if (value == null || value.isEmpty) { + return context.l10n.emailRequired; + } + if (!AppRegExp.email.hasMatch(value)) { + return context.l10n.invalidEmail; + } + return null; + }, + ), + const SizedBox(height: 30), + CustomButton( + key: Key(MqKeys.loginTypeName('email')), + text: context.l10n.signIn, + onPressed: () { + if (formKey.currentState!.validate()) { MqAnalytic.track( - AnalyticKey.tapLoginWithSoccial, - params: {'soccial': 'google'}, + AnalyticKey.tapLogin, + params: {'soccial': 'email'}, ); - unawaited(AppAlert.showLoading(context)); - await context.read().signInWithGoogle(); - if (context.mounted) context.loaderOverlay.hide(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + try { + context.read().login(emailController.text); + context.goNamed(AppRouter.verificationCode); + } catch (e) { + log(e.toString()); + } + } + }, + ), + const SizedBox(height: 30), + Row( + children: [ + const Expanded(child: Divider()), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(context.l10n.orContinueWith), + ), + const Expanded(child: Divider()), + ], + ), + const SizedBox(height: 16), + GestureDetector( + key: Key(MqKeys.loginTypeName('google')), + onTap: () async { + MqAnalytic.track( + AnalyticKey.tapLogin, + params: {'soccial': 'google'}, + ); + unawaited(AppAlert.showLoading(context)); + await context.read().signInWithGoogle(); + if (context.mounted) context.loaderOverlay.hide(); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black26), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -83,25 +120,7 @@ class SignInView extends StatelessWidget { ), ), ), - // const SizedBox(height: 30), - // Padding( - // padding: const EdgeInsets.all(16), - // child: SignInWithAppleButton( - // key: Key(MqKeys.loginTypeName('apple')), - // onPressed: () async { - // if (Theme.of(context).platform == TargetPlatform.android) { - // AppSnackbar.showSnackbar(context, context.l10n.appleSignInNotAvailable); - // } else { - // unawaited(AppAlert.showLoading(context)); - // await context.read().signInWithApple(); - // if (context.mounted) context.loaderOverlay.hide(); - // } - // }, - // text: context.l10n.apple, - // ), - // ), - // const Spacer(), - const SizedBox(height: 30), + const SizedBox(height: 40), TextButton( onPressed: () { MqAnalytic.track(AnalyticKey.tapPrivacyPolicy); @@ -110,12 +129,11 @@ class SignInView extends StatelessWidget { child: Text( context.l10n.privacyPolicy, style: context.bodyLarge!.copyWith( - color: context.colors.primary, decoration: TextDecoration.underline, ), ), ), - const SizedBox(height: 20), + const SizedBox(height: 10), ], ), ), diff --git a/app/lib/modules/login/presentation/view/verification_code_view.dart b/app/lib/modules/login/presentation/view/verification_code_view.dart new file mode 100644 index 00000000..c85f9313 --- /dev/null +++ b/app/lib/modules/login/presentation/view/verification_code_view.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:my_quran/app/presentation/presenation.dart'; +import 'package:my_quran/components/components.dart'; +import 'package:my_quran/config/config.dart'; +import 'package:my_quran/theme/custom/typography/typography_theme.dart'; +import 'package:my_quran/utils/urils.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; + +class VerificationCodeView extends StatelessWidget { + VerificationCodeView({super.key}); + + final TextEditingController verificationCodeController = TextEditingController(); + final GlobalKey formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Enter a verification Code'), + leading: IconButton( + icon: const Icon(Icons.navigate_before), + onPressed: () { + context.goNamed(AppRouter.loginWihtSoccial); + }, + ), + ), + body: BlocListener( + listener: (context, state) { + if (state.user != null) { + context.read().setUserData(state.user!); + context.goNamed(AppRouter.home); + } else if (state.exception != null) { + AppAlert.showErrorDialog( + context, + errorText: state.exception.toString(), + ); + } + }, + child: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Please enter the 4-digit code sent to your email.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 26), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 50, + ), + child: PinCodeTextField( + appContext: context, + length: 4, + controller: verificationCodeController, + keyboardType: TextInputType.number, + cursorColor: Colors.black, + pinTheme: PinTheme( + shape: PinCodeFieldShape.box, + borderRadius: BorderRadius.circular(5), + inactiveColor: Colors.blue, + selectedColor: Colors.green, + errorBorderColor: context.colors.error, + ), + validator: (value) { + if (value == null || value.length < 4) { + return 'Please enter the full verification code.'; + } + return null; + }, + onChanged: (value) {}, + ), + ), + const SizedBox(height: 30), + CustomButton( + onPressed: () { + if (formKey.currentState!.validate()) { + context.read().fetchSmsCode(verificationCodeController.text); + } + }, + text: 'Verify', + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/utils/reg_exp/app_reg_exp.dart b/app/lib/utils/reg_exp/app_reg_exp.dart index ee907091..abc1cabf 100644 --- a/app/lib/utils/reg_exp/app_reg_exp.dart +++ b/app/lib/utils/reg_exp/app_reg_exp.dart @@ -2,4 +2,5 @@ class AppRegExp { const AppRegExp._(); static final duration = RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$'); + static final email = RegExp(r'^[^@]+@[^@]+\.[^@]+$'); } diff --git a/app/pubspec.lock b/app/pubspec.lock index e00f2683..cc51c405 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -949,6 +949,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + pin_code_fields: + dependency: "direct main" + description: + name: pin_code_fields + sha256: "4c0db7fbc889e622e7c71ea54b9ee624bb70c7365b532abea0271b17ea75b729" + url: "https://pub.dev" + source: hosted + version: "8.0.1" platform: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 9766ff7c..f835d1aa 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: path: ../packages/mq_storage/ mq_ci_keys: path: ../packages/mq_ci_keys/ + pin_code_fields: ^8.0.1 # flutter_native_splash: ^2.4.0 dev_dependencies: diff --git a/app/test/helpers/pump_app.dart b/app/test/helpers/pump_app.dart index 9f55d757..0709564b 100644 --- a/app/test/helpers/pump_app.dart +++ b/app/test/helpers/pump_app.dart @@ -23,6 +23,8 @@ extension PumpApp on WidgetTester { LogoutUseCase logoutUseCase, MqRemoteConfig remoteConfig, PackageInfo packageInfo, + EmailLoginUseCase emailSignIn, + FetchSmsCodeUseCase fetchSmsCode, ) { return pumpWidget( MultiRepositoryProvider( @@ -49,6 +51,8 @@ extension PumpApp on WidgetTester { patchGenderUseCase, patchLocaleCodeUseCase, logoutUseCase, + emailSignIn, + fetchSmsCode, ), ), BlocProvider( diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 0b864e79..04ece58e 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -42,6 +42,8 @@ void main() { final setColorUseCase = SetColorUseCase(themeRepository); final getAppVersionUseCase = GetAppVersionUseCase(appRepository); final logoutUseCase = LogoutUseCase(authRepository); + final emailSignIn = EmailLoginUseCase(authRepository); + final fetchSmsCode = FetchSmsCodeUseCase(authRepository); when(() => storage.readString(key: StorageKeys.tokenKey)).thenReturn(null); when(() => storage.readString(key: StorageKeys.genderKey)).thenReturn(null); @@ -64,6 +66,8 @@ void main() { logoutUseCase, remoteConfig, packageInfo, + emailSignIn, + fetchSmsCode, ); await tester.pumpAndSettle(); expect(find.byType(MaterialApp), findsOneWidget);