diff --git a/app/lib/app/data/repository/auth_repositoty_impl.dart b/app/lib/app/data/repository/auth_repositoty_impl.dart index fab4875c..2cf23781 100644 --- a/app/lib/app/data/repository/auth_repositoty_impl.dart +++ b/app/lib/app/data/repository/auth_repositoty_impl.dart @@ -30,6 +30,72 @@ final class AuthRepositoryImpl implements AuthRepository { } } + @override + Future> signUpWithEmail({ + required String email, + required String password, + required String username, + required String languageCode, + required Gender gender, + }) async { + try { + final res = await remoteDataSource.signUpWithEmail( + email: email, + password: password, + username: username, + languageCode: languageCode, + gender: gender, + ); + return res.fold( + (l) => Left(AuthenticationExc(message: l.toString())), + (r) => Right( + UserEntity( + accessToken: r.accessToken, + username: r.username, + gender: r.gender, + localeCode: r.localeCode, + ), + ), + ); + } catch (e, s) { + MqCrashlytics.report(e, s); + log('signUpWithEmail: error: $e\n$s'); + return Left(AuthenticationExc(message: e.toString())); + } + } + + @override + Future> signInWithEmail({ + required String email, + required String password, + required String languageCode, + required Gender gender, + }) async { + try { + final res = await remoteDataSource.signInWithEmail( + email: email, + password: password, + languageCode: languageCode, + gender: gender, + ); + return res.fold( + Left.new, + (r) => Right( + UserEntity( + accessToken: r.accessToken, + username: r.username, + gender: r.gender, + localeCode: r.localeCode, + ), + ), + ); + } catch (e, s) { + MqCrashlytics.report(e, s); + log('signWithEmail: error: $e\n$s'); + return Left(AuthenticationExc(message: e.toString())); + } + } + @override Future> signWithGoogle( String languageCode, @@ -106,6 +172,16 @@ final class AuthRepositoryImpl implements AuthRepository { }); } + @override + Future forgotPassword(String email) async { + try { + await remoteDataSource.forgotPasswordRemote(email); + } catch (e, s) { + MqCrashlytics.report(e, s); + log('Forgot Password error: $e\n$s'); + } + } + @override Future logout() async { try { diff --git a/app/lib/app/data/source/auth_remote_data_source.dart b/app/lib/app/data/source/auth_remote_data_source.dart index 7feb63fe..95f5a6fd 100644 --- a/app/lib/app/data/source/auth_remote_data_source.dart +++ b/app/lib/app/data/source/auth_remote_data_source.dart @@ -19,6 +19,72 @@ final class AuthRemoteDataSource { final SoccialAuth soccialAuth; final bool isIntegrationTest; + Future> signUpWithEmail({ + required String email, + required String password, + required String username, + required String languageCode, + required Gender gender, + }) async { + final signUpAuth = await soccialAuth.signUpWithEmail(email, password); + final accessToken = signUpAuth.credential?.accessToken ?? ''; + final token = await client.post( + apiConst.registerWithEmail, + fromJson: TokenResponse.fromJson, + body: { + 'access_token': accessToken, + 'username': username, + }, + ); + + return token.fold( + (l) => Left(Exception('Failed to sign up with email')), + (r) async { + final user = UserModelResponse( + accessToken: r.key, + username: username, + gender: gender, + localeCode: languageCode, + ); + + await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); + + return Right(user); + }, + ); + } + + Future> signInWithEmail({ + required String email, + required String password, + required String languageCode, + required Gender gender, + }) async { + final signInAuth = await soccialAuth.signInWithEmail(email, password); + final accessToken = signInAuth.credential?.accessToken ?? ''; + + final token = await client.post( + apiConst.loginWithEmail, + fromJson: TokenResponse.fromJson, + body: { + 'access_token': accessToken, + }, + ); + + return token.fold(Left.new, (r) async { + final user = UserModelResponse( + accessToken: r.key, + username: 'token.username', + gender: gender, + localeCode: languageCode, + ); + + await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); + + return Right(user); + }); + } + Future> signInWithGoogle( String languageCode, Gender gender, @@ -141,6 +207,10 @@ final class AuthRemoteDataSource { ); } + Future forgotPasswordRemote(String email) async { + await soccialAuth.forgotPassword(email); + } + Future logoutRemote() async { await soccialAuth.logOut(); } diff --git a/app/lib/app/domain/domain.dart b/app/lib/app/domain/domain.dart index a0ad0e83..e3098e60 100644 --- a/app/lib/app/domain/domain.dart +++ b/app/lib/app/domain/domain.dart @@ -12,5 +12,8 @@ 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_sign_up_usecase.dart'; +export 'usecase/email_sign_in_usecase.dart'; +export 'usecase/forgot_password_usecase.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..51a9e71d 100644 --- a/app/lib/app/domain/repository/auth_repository.dart +++ b/app/lib/app/domain/repository/auth_repository.dart @@ -4,6 +4,21 @@ import 'package:my_quran/core/core.dart'; abstract class AuthRepository { UserEntity? get init; + Future> signUpWithEmail({ + required String email, + required String password, + required String username, + required String languageCode, + required Gender gender, + }); + + Future> signInWithEmail({ + required String email, + required String password, + required String languageCode, + required Gender gender, + }); + Future> signWithGoogle( String languageCode, Gender gender, @@ -26,5 +41,7 @@ abstract class AuthRepository { required String localeCode, }); + Future forgotPassword(String email); + Future logout() async {} } diff --git a/app/lib/app/domain/usecase/email_sign_in_usecase.dart b/app/lib/app/domain/usecase/email_sign_in_usecase.dart new file mode 100644 index 00000000..1da33103 --- /dev/null +++ b/app/lib/app/domain/usecase/email_sign_in_usecase.dart @@ -0,0 +1,24 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/core/core.dart'; + +@immutable +final class EmailSignInUseCase { + const EmailSignInUseCase(this.repository); + + final AuthRepository repository; + + Future> call({ + required String email, + required String password, + required String languageCode, + required Gender gender, + }) { + return repository.signInWithEmail( + email: email, + password: password, + languageCode: languageCode, + gender: gender, + ); + } +} diff --git a/app/lib/app/domain/usecase/email_sign_up_usecase.dart b/app/lib/app/domain/usecase/email_sign_up_usecase.dart new file mode 100644 index 00000000..2e36cea9 --- /dev/null +++ b/app/lib/app/domain/usecase/email_sign_up_usecase.dart @@ -0,0 +1,26 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/core/core.dart'; + +@immutable +final class EmailSignUpUseCase { + const EmailSignUpUseCase(this.repository); + + final AuthRepository repository; + + Future> call({ + required String email, + required String password, + required String username, + required String languageCode, + required Gender gender, + }) { + return repository.signUpWithEmail( + email: email, + password: password, + username: username, + languageCode: languageCode, + gender: gender, + ); + } +} diff --git a/app/lib/app/domain/usecase/forgot_password_usecase.dart b/app/lib/app/domain/usecase/forgot_password_usecase.dart new file mode 100644 index 00000000..696938e2 --- /dev/null +++ b/app/lib/app/domain/usecase/forgot_password_usecase.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; + +@immutable +final class ForgotPasswordUseCase { + const ForgotPasswordUseCase(this.repository); + + final AuthRepository repository; + + Future call(String email) { + return repository.forgotPassword(email); + } +} diff --git a/app/lib/app/presentation/cubit/auth_cubit.dart b/app/lib/app/presentation/cubit/auth_cubit.dart index 2e4c27c7..bf443b31 100644 --- a/app/lib/app/presentation/cubit/auth_cubit.dart +++ b/app/lib/app/presentation/cubit/auth_cubit.dart @@ -13,19 +13,63 @@ class AuthCubit extends Cubit { this.getInitialUserUseCase, this.googleSignIn, this.appleSignIn, + this.emailSignIn, + this.emailSignUp, this.serUserDataUseCase, this.patchGenderUseCase, this.patchLocaleCodeUseCase, this.logoutUseCase, + this.forgotPasswordUseCase, ) : super(AuthState(user: getInitialUserUseCase.call)); final GetInitialUserUseCase getInitialUserUseCase; final GoogleSignInUseCase googleSignIn; final AppleSignInUseCase appleSignIn; + final EmailSignInUseCase emailSignIn; + final EmailSignUpUseCase emailSignUp; + final ForgotPasswordUseCase forgotPasswordUseCase; final SerUserDataUseCase serUserDataUseCase; final PatchGenderUseCase patchGenderUseCase; final PatchLocaleCodeUseCase patchLocaleCodeUseCase; final LogoutUseCase logoutUseCase; + bool passwordVisible = false; + + Future signUpWithEmail({ + required String email, + required String password, + required String username, + }) async { + final user = await emailSignUp( + email: email, + password: password, + username: username, + languageCode: state.currentLocale.languageCode, + gender: state.gender, + ); + + user.fold( + (l) => emit(state.copyWith(exception: l)), + (r) => emit(state.copyWith(user: r)), + ); + + return state; + } + + Future signInWithEmail(String email, String password) async { + final user = await emailSignIn( + email: email, + password: password, + 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( @@ -95,6 +139,14 @@ class AuthCubit extends Cubit { } } + Future forgotPassword(String email) async { + try { + await forgotPasswordUseCase(email); + } catch (e) { + emit(state.copyWith(exception: Exception(e))); + } + } + Future logout() async { try { await logoutUseCase.call(); diff --git a/app/lib/app/presentation/view/app_view.dart b/app/lib/app/presentation/view/app_view.dart index f1d8d123..0a5e22e2 100644 --- a/app/lib/app/presentation/view/app_view.dart +++ b/app/lib/app/presentation/view/app_view.dart @@ -56,10 +56,13 @@ class MyApp extends StatelessWidget { GetInitialUserUseCase(context.read()), GoogleSignInUseCase(context.read()), AppleSignInUseCase(context.read()), + EmailSignInUseCase(context.read()), + EmailSignUpUseCase(context.read()), SerUserDataUseCase(context.read()), PatchGenderUseCase(context.read()), PatchLocaleCodeUseCase(context.read()), LogoutUseCase(context.read()), + ForgotPasswordUseCase(context.read()), ), ), BlocProvider( diff --git a/app/lib/components/components.dart b/app/lib/components/components.dart index 113de7f5..aa129948 100644 --- a/app/lib/components/components.dart +++ b/app/lib/components/components.dart @@ -8,3 +8,6 @@ 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'; +export 'forms/forgot_password_form.dart'; +export 'forms/hide_password.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/components/forms/forgot_password_form.dart b/app/lib/components/forms/forgot_password_form.dart new file mode 100644 index 00000000..abac7d8e --- /dev/null +++ b/app/lib/components/forms/forgot_password_form.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_quran/components/components.dart'; +import 'package:my_quran/core/core.dart'; +import 'package:my_quran/l10n/l10.dart'; + +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/theme/theme.dart'; +import 'package:my_quran/utils/urils.dart'; + +class ForgotPasswordForm extends StatelessWidget { + ForgotPasswordForm({required this.controller, super.key}); + + final TextEditingController controller; + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + child: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + context.l10n.email, + textAlign: TextAlign.center, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + const SizedBox(height: 16), + CustomTextFormField( + controller: controller, + 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: 20), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + MqAnalytic.track(AnalyticKey.tapForgotPassword); + context.read().forgotPassword(controller.text); + Navigator.pop(context); + } + }, + child: Text(context.l10n.resetPassword), + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/components/forms/hide_password.dart b/app/lib/components/forms/hide_password.dart new file mode 100644 index 00000000..e14c5b87 --- /dev/null +++ b/app/lib/components/forms/hide_password.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:my_quran/components/components.dart'; +import 'package:my_quran/l10n/l10.dart'; +import 'package:my_quran/utils/urils.dart'; + +class PasswordFormFiled extends StatefulWidget { + const PasswordFormFiled({ + this.controller, + this.validator, + super.key, + }); + final TextEditingController? controller; + final FormFieldValidator? validator; + + @override + State createState() => _PasswordFormFiledState(); +} + +class _PasswordFormFiledState extends State { + bool isPasswordHidden = true; + + @override + Widget build(BuildContext context) { + return CustomTextFormField( + controller: widget.controller, + labelText: context.l10n.password, + obscureText: isPasswordHidden, + suffixIcon: IconButton( + icon: Icon(isPasswordHidden ? Icons.visibility : Icons.visibility_off), + onPressed: () { + setState(() { + isPasswordHidden = !isPasswordHidden; + }); + }, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return context.l10n.passwordRequired; + } + if (!AppRegExp.password.hasMatch(value)) { + return context.l10n.passwordInvalid; + } + if (widget.validator != null) { + return widget.validator!(value); + } + return null; + }, + ); + } +} diff --git a/app/lib/config/router/app_router.dart b/app/lib/config/router/app_router.dart index bbaee742..11a1652a 100644 --- a/app/lib/config/router/app_router.dart +++ b/app/lib/config/router/app_router.dart @@ -25,6 +25,7 @@ final class AppRouter { static const read = 'read'; static const hatimRead = 'hatim-read'; static const login = 'login'; + static const register = 'register'; static const loginWihtSoccial = 'login-with-soccial'; static const settingsPage = 'settings'; @@ -52,10 +53,15 @@ final class AppRouter { name: login, builder: (context, state) => const LoginView(), ), + GoRoute( + path: '/$register', + name: register, + builder: (context, state) => SignUpView(), + ), GoRoute( path: '/$loginWihtSoccial', name: loginWihtSoccial, - builder: (context, state) => const SignInView(), + builder: (context, state) => SignInView(), ), 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(register)) { return '/$login'; } } diff --git a/app/lib/constants/api/api_const.dart b/app/lib/constants/api/api_const.dart index b022bb62..91206a76 100644 --- a/app/lib/constants/api/api_const.dart +++ b/app/lib/constants/api/api_const.dart @@ -11,6 +11,8 @@ class ApiConst { String get socketBase => 'wss://myquran.life/ws'; String get loginWithGoogle => '$_getDomain/api/v1/accounts/google/'; + String get loginWithEmail => '$_getDomain/api/v1/accounts/loginEmail/'; + String get registerWithEmail => '$_getDomain/api/v1/accounts/registerEmail/'; String get loginWithApple => '$_getDomain/api/v1/accounts/apple/'; String putProfile(String userId) => '$_getDomain/api/v1/accounts/profile/$userId/'; String get hatimDashBoard => '$_getDomain/api/v1/hatim/dashboard'; diff --git a/app/lib/core/analytics/mq_analytic_keys.dart b/app/lib/core/analytics/mq_analytic_keys.dart index f88831e9..9160a7af 100644 --- a/app/lib/core/analytics/mq_analytic_keys.dart +++ b/app/lib/core/analytics/mq_analytic_keys.dart @@ -15,6 +15,7 @@ final class AnalyticKey { static const goHatimReadPage = 'go_hatim_read_page'; static const goQuranReadByJuz = 'go_quran_read_by_juz'; static const goQuranReadBySurah = 'go_quran_read_by_surah'; + static const goRegister = 'go_register'; // select - unselect static const selectLanguage = 'select_language'; @@ -29,7 +30,9 @@ final class AnalyticKey { // action static const tapLogout = 'tap_logout'; - static const tapLoginWithSoccial = 'tap_login_with_soccial'; + static const tapLogin = 'tap_login'; + static const tapSignUp = 'tap_signup'; + static const tapForgotPassword = 'tap_forgot_password'; static const tapPrivacyPolicy = 'tap_privacy_policy'; static const tapQuranReadSettings = 'tap_quran_read_settings'; static const showAmin = 'show_amin'; diff --git a/app/lib/core/auth/soccial_auth.dart b/app/lib/core/auth/soccial_auth.dart index cc91ed8b..28471f15 100644 --- a/app/lib/core/auth/soccial_auth.dart +++ b/app/lib/core/auth/soccial_auth.dart @@ -10,6 +10,32 @@ class SoccialAuth { final GoogleSignIn _googleSignIn; + Future signInWithEmail(String email, String password) async { + try { + final userCredential = await FirebaseAuth.instance.signInWithEmailAndPassword( + email: email, + password: password, + ); + return userCredential; + } catch (e, s) { + MqCrashlytics.report(e, s); + rethrow; + } + } + + Future signUpWithEmail(String email, String password) async { + try { + final userCredential = await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: password, + ); + return userCredential; + } catch (e, s) { + MqCrashlytics.report(e, s); + rethrow; + } + } + Future signInWithGoogle() async { try { final googleUser = await _googleSignIn.signIn(); @@ -47,6 +73,15 @@ class SoccialAuth { } } + Future forgotPassword(String email) async { + try { + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + } catch (e, s) { + MqCrashlytics.report(e, s); + rethrow; + } + } + Future logOut() async { try { await FirebaseAuth.instance.signOut(); diff --git a/app/lib/l10n/arb/app_ar.arb b/app/lib/l10n/arb/app_ar.arb index 5e975fe2..bad47296 100644 --- a/app/lib/l10n/arb/app_ar.arb +++ b/app/lib/l10n/arb/app_ar.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "يرجى تحديد لغة.", "male": "ذكر", "next": "التالي", - "signInWith": "---تسجيل الدخول ب---", "pleaseWait": "يرجى الانتظار", "privacyPolicy": " سياسة الخصوصية ", "profileChangeTheme": "تثبيت موضوع", @@ -74,12 +73,31 @@ "start": "بداية", "surah": "سورة", "surahs": "السور", - "welcome": " أهلاً وسهلاً ", "version": "إصدار", "salam": "السلام عليكم", "logout": "تسجيل الخروج", "signOutContext": "هل أنت متأكد أنك تريد تسجيل الخروج؟", "yes": "نعم", "no": "لا", - "appleSignInNotAvailable": "تسجيل الدخول عبر Apple غير متاح على أجهزة Android." + "appleSignInNotAvailable": "تسجيل الدخول عبر Apple غير متاح على أجهزة Android.", + "welcome": "مرحبًا بعودتك", + "signIn": "تسجيل الدخول", + "signUp": "اشتراك", + "dontHaveAccount": "ليس لديك حساب", + "orContinueWith": "أو تابع مع", + "email": "البريد الإلكتروني", + "password": "كلمة المرور", + "alreadyHaveAccount": "هل لديك حساب بالفعل؟", + "confirmPassword": "تأكيد كلمة المرور", + "passwordMismatchError": "كلمة المرور غير متطابقة", + "forgotPassword": "نسيت كلمة المرور؟", + "passwordReset": "إعادة تعيين كلمة المرور", + "passwordResetInstructions": "يرجى التحقق من بريدك الإلكتروني للحصول على تعليمات حول كيفية إعادة تعيين كلمة المرور الخاصة بك.", + "emailRequired": "البريد الإلكتروني مطلوب.", + "invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح.", + "passwordRequired": "كلمة المرور مطلوبة.", + "passwordInvalid": "يجب أن يكون كلمة المرور على الأقل 8 أحرف ، ويجب أن تحتوي على حرف واحد على الأقل ورقم واحد على الأقل.", + "resetPassword": "إعادة تعيين كلمة المرور", + "username": "اسم المستخدم", + "usernameRequired": "اسم المستخدم مطلوب" } \ 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 d8293876..012fb0ad 100644 --- a/app/lib/l10n/arb/app_en.arb +++ b/app/lib/l10n/arb/app_en.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Please select a language.", "male": "Male", "next": "Next", - "signInWith": "---Sign in with---", "pleaseWait": "Please waiting", "privacyPolicy": "Privacy Policy", "profileChangeTheme": "Theme Installation", @@ -74,12 +73,31 @@ "start": "Start", "surah": "Surah", "surahs": "Surahs", - "welcome": "Welcome", "version": "Version", "salam": "Assalamu alaykum", "logout": "Logout", "signOutContext": "Are you sure you want to logout?", "yes": "Yes", "no": "No", - "appleSignInNotAvailable": "Apple Sign-In is not available on Android devices." + "appleSignInNotAvailable": "Apple Sign-In is not available on Android devices.", + "welcome": "Welcome Back", + "signIn": "Sign in", + "signUp": "Sign up", + "dontHaveAccount": "Don't have an account", + "orContinueWith": "Or continue with", + "email": "Email", + "password":"Password", + "alreadyHaveAccount": "Already have an account?", + "confirmPassword": "Confirm Password", + "passwordMismatchError": "Passwords do not match", + "forgotPassword":"Forgot password?", + "passwordReset": "Password Reset", + "passwordResetInstructions":"Please check your email for instructions on how to reset your password.", + "emailRequired": "Email is required.", + "invalidEmail": "Please enter a valid email address.", + "passwordRequired": "Password is required.", + "passwordInvalid": "Password should be at least 8 characters long, must contain at least one letter and one number", + "resetPassword": "Reset your password", + "username": "Username", + "usernameRequired": "Username is required" } \ 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 65b9381c..dfe92e5d 100644 --- a/app/lib/l10n/arb/app_id.arb +++ b/app/lib/l10n/arb/app_id.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Pilih bahasa.", "male": "Pria", "next": "Lanjut", - "signInWith": "---Masuk dengan---", "pleaseWait": "Silakan tunggu", "privacyPolicy": " Kebijakan Privasi", "profileChangeTheme": "Instalasi Tema", @@ -74,12 +73,31 @@ "start": "Awal", "surah": "Surat", "surahs": "Surat", - "welcome": "Selamat datang", "version": "Versi", "salam": "Assalamu alaikum", "logout": "Keluar", "signOutContext": "Apakah Anda yakin ingin keluar?", "yes": "Ya", "no": "Tidak", - "appleSignInNotAvailable": "Masuk dengan Apple tidak tersedia di perangkat Android." + "appleSignInNotAvailable": "Masuk dengan Apple tidak tersedia di perangkat Android.", + "welcome": "Selamat Datang Kembali", + "signIn": "Masuk", + "signUp": "Daftar", + "dontHaveAccount": "Tidak punya akun", + "orContinueWith": "Atau lanjutkan dengan", + "email": "Email", + "password": "Kata Sandi", + "alreadyHaveAccount": "Sudah memiliki akun?", + "confirmPassword": "Konfirmasi Kata Sandi", + "passwordMismatchError": "Kata sandi tidak cocok", + "forgotPassword": "Lupa kata sandi?", + "passwordReset": "Atur Ulang Kata Sandi", + "passwordResetInstructions":"Silakan periksa email Anda untuk petunjuk tentang cara mengatur ulang kata sandi Anda.", + "emailRequired": "Email diperlukan.", + "invalidEmail": "Silakan masukkan alamat email yang valid.", + "passwordRequired": "Kata sandi diperlukan.", + "passwordInvalid": "Password harus setidaknya 8 karakter, harus mengandung setidaknya satu huruf dan satu angka.", + "resetPassword": "Atur ulang kata sandi Anda", + "username": "Nama Pengguna", + "usernameRequired": "Nama Pengguna diperlukan" } \ 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 ad2c7e06..128bcc5b 100644 --- a/app/lib/l10n/arb/app_kk.arb +++ b/app/lib/l10n/arb/app_kk.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Тілді таңдаңыз", "male": "Eркек", "next": "Келесі", - "signInWith": " ---Жүйеге кіру---", "pleaseWait": "Өтінемін, Күте тұрыңыз", "privacyPolicy": " Құпиялылық саясаты", "profileChangeTheme": "Тақырыпты орнату", @@ -74,12 +73,31 @@ "start": "Баста", "surah": "Cүре", "surahs": "Cүре", - "welcome": "Қош келдіңіз", "version": "Нұсқа", "salam": "Ассаламу алейкум", "logout": "Шығу", "signOutContext": "Шығыңыз келетініне сенімдісіз бе?", "yes": "Иә", "no": "Жоқ", - "appleSignInNotAvailable": "Apple жүйесіне кіру Android құрылғыларында қол жетімді емес." + "appleSignInNotAvailable": "Apple жүйесіне кіру Android құрылғыларында қол жетімді емес.", + "welcome": "Қош келдіңіз", + "signIn": "Кіру", + "signUp": "Тіркелу", + "dontHaveAccount": "Есептік жазбаңыз жоқ па", + "orContinueWith": "Немесе жалғастыру", + "email": "Электрондық пошта", + "password": "Құпия сөз", + "alreadyHaveAccount": "Аккаунтыңыз бар ма?", + "confirmPassword": "Құпия сөзді растау", + "passwordMismatchError": "Құпия сөздер сәйкес келмейді", + "forgotPassword": "Парольді ұмыттыңыз ба?", + "passwordReset": "Құпия сөзді қалпына келтіру", + "passwordResetInstructions":"Құпия сөзіңізді қалпына келтіру жайлы нұсқаулықтар үшін электрондық поштаны тексеріңіз.", + "emailRequired": "Электрондық пошта қажет.", + "invalidEmail": "Жарамды электрондық пошта мекенжайын енгізіңіз.", + "passwordRequired": "Құпия сөз қажет.", + "passwordInvalid": "Құпия сөз қамтиамы 8 тауып, кем дегенде бір әріп және бір сан міндетті.", + "resetPassword": "Парольді қайта орнату", + "username": "Сіздің атыңыз", + "usernameRequired": "Қолданушы аты міндетті" } \ 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 4bb862a8..0655befe 100644 --- a/app/lib/l10n/arb/app_ky.arb +++ b/app/lib/l10n/arb/app_ky.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Сураныч тилди тандаңыз.", "male": "Эркек", "next": "Кийинки", - "signInWith": "---Кирүү---", "pleaseWait": "Сураныч, күтө туруңуз", "privacyPolicy": "Конфиденциалдык саясат", "profileChangeTheme": "Теманы орнотуу", @@ -74,12 +73,31 @@ "start": "Баштоо", "surah": "Сүрөө", "surahs": "Сүрөөлөр", - "welcome": "Кош келиңиз", "version": "Версия", "salam": "Ассаламу алейкум", "logout": "Чыгуу", "signOutContext": "Чын эле чыгып кеткиңиз келеби?", "yes": "Ооба", "no": "Жок", - "appleSignInNotAvailable": "Apple аркылуу кирүү Android системаларында жеткиликтүү эмес." + "appleSignInNotAvailable": "Apple аркылуу кирүү Android системаларында жеткиликтүү эмес.", + "welcome": "Кош келиңиз", + "signIn": "Кирүү", + "signUp": "Катталуу", + "dontHaveAccount": "Аккаунтуңуз жокпу?", + "orContinueWith": "Же", + "email": "Электрондук почта", + "password": "Сыр сөз", + "alreadyHaveAccount": "Аккаунтунуз барбы?", + "confirmPassword": "Сырсөздү ырастоо", + "passwordMismatchError": "Сырсөздөр дал келген жок", + "forgotPassword": "Сыр сөзүңүздү унуттуңузбу?", + "passwordReset": "Сыр сөздү калыбына келтирүү", + "passwordResetInstructions":"Сыр сөздү кайра калыбына келтирүү тууралуу инструкция электрондук почтаңызга жөнөтүлдү.", + "emailRequired": "Электрондук почтаңызды жазыңыз.", + "invalidEmail": "Туура электрондук почта дарегин киргизиңиз.", + "passwordRequired": "Сырсөз жазыңыз.", + "passwordInvalid": "Сырсөз кеминде 8 символдуу жана бир тамга, бир санды камтыган болушу керек.", + "resetPassword":"Паролду орнотуу", + "username": "Аты-жөнүңүз", + "usernameRequired": "Аты-жөнүңүздү жазыңыз" } \ 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 20a57914..98ee0931 100644 --- a/app/lib/l10n/arb/app_ru.arb +++ b/app/lib/l10n/arb/app_ru.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Пожалуйста, выберите язык", "male": "Мужчина", "next": "Следующий", - "signInWith": "---Войти с помощью---", "pleaseWait": "Пожалуйста, подождите", "privacyPolicy": "Политика Конфиденциальности", "profileChangeTheme": "Установка темы", @@ -74,12 +73,31 @@ "start": "Начать", "surah": "Суры", "surahs": "Суры", - "welcome": "Добро пожаловать", "version": "Версия", "salam": "Ассаламу алейкум", "logout": "Выйти", "signOutContext": "Вы уверены, что хотите выйти?", "yes": "Да", "no": "Нет", - "appleSignInNotAvailable": "Вход с Apple недоступен на устройствах Android." + "appleSignInNotAvailable": "Вход с Apple недоступен на устройствах Android.", + "welcome": "Добро пожаловать", + "signIn": "Войти", + "signUp": "Зарегистрироваться", + "dontHaveAccount": "Нет учетной записи", + "orContinueWith": "Или продолжить с", + "email": "Эл. почта", + "password": "Пароль", + "alreadyHaveAccount": "Уже есть аккаунт?", + "confirmPassword": "Подтвердите пароль", + "passwordMismatchError": "Пароли не совпадают", + "forgotPassword": "Забыли пароль?", + "passwordReset": "Сброс пароля", + "passwordResetInstructions":"Пожалуйста, проверьте свою электронную почту для получения инструкций по сбросу пароля.", + "emailRequired": "Требуется электронная почта.", + "invalidEmail": "Пожалуйста, введите действующий адрес электронной почты.", + "passwordRequired": "Требуется пароль.", + "passwordInvalid": "Пароль должен быть не менее 8 символов длиной и содержать хотя бы одну букву и одну цифру.", + "resetPassword": "Сбросить пароль", + "username": "Имя пользователя", + "usernameRequired": "Имя пользователя обязательно" } \ 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 f6157833..b8c9d4b7 100644 --- a/app/lib/l10n/arb/app_tr.arb +++ b/app/lib/l10n/arb/app_tr.arb @@ -49,7 +49,6 @@ "loginPleaseSelectLang": "Lütfen bir dil seçin.", "male": "Erkek", "next": "Sonraki", - "signInWith": "---Şununla giriş yap---", "pleaseWait": "Lütfen bekleyin", "privacyPolicy": "Gizlilik Politikası", "profileChangeTheme": "Tema Kurulumu", @@ -74,12 +73,31 @@ "start": "Başla", "surah": "Sure", "surahs": "Sureler", - "welcome": "Hoş geldiniz", "version": "Sürüm", "salam": "Selamun aleyküm", "logout": "Çıkış yap", "signOutContext": "Çıkış yapmak istediğinizden emin misiniz?", "yes": "Evet", "no": "Hayır", - "appleSignInNotAvailable": "Apple ile giriş Android cihazlarda mevcut değil." + "appleSignInNotAvailable": "Apple ile giriş Android cihazlarda mevcut değil.", + "welcome": "Hoş geldiniz", + "signIn": "Giriş Yap", + "signUp": "Kaydol", + "dontHaveAccount": "Hesabınız yok mu", + "orContinueWith": "Veya ile devam et", + "email": "E-posta", + "password": "Şifre", + "alreadyHaveAccount": "Zaten bir hesabınız var mı?", + "confirmPassword": "Şifreyi Onayla", + "passwordMismatchError": "Şifreler eşleşmiyor", + "forgotPassword": "Şifremi unuttum?", + "passwordReset": "Şifre Sıfırlama", + "passwordResetInstructions":"Şifrenizi sıfırlama talimatları için e-postanızı kontrol edin.", + "emailRequired": "E-posta gereklidir.", + "invalidEmail": "Lütfen geçerli bir e-posta adresi girin.", + "passwordRequired": "Şifre gereklidir.", + "passwordInvalid": "Parola en az 8 karakter uzunluğunda olmalı, en az bir harf ve bir rakam içermelidir.", + "resetPassword": "Şifrenizi sıfırlayın", + "username": "Kullanıcı adı", + "usernameRequired": "Kullanıcı adı zorunlu" } \ 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..44603ab9 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/sign_up_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..d8d30277 100644 --- a/app/lib/modules/login/presentation/view/sign_in_view.dart +++ b/app/lib/modules/login/presentation/view/sign_in_view.dart @@ -6,7 +6,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'; import 'package:my_quran/core/core.dart'; @@ -15,7 +15,12 @@ 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 forgotPasswordEmailController = TextEditingController(); + final passwordController = TextEditingController(); + final formKey = GlobalKey(); @override Widget build(BuildContext context) { @@ -33,90 +38,151 @@ class SignInView extends StatelessWidget { ); } }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 70), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 80, vertical: 30), - child: Assets.images.splash.image(), - ), - const SizedBox(height: 32), - Text( - '${context.l10n.welcome}!', - style: context.titleLarge!.copyWith(color: context.colors.primary, fontSize: 30), - ), - const SizedBox(height: 33), - Text( - context.l10n.signInWith, - textAlign: TextAlign.center, - style: context.bodyLarge!.copyWith(color: context.colors.shadow, fontSize: 17), - ), - Padding( - padding: const EdgeInsets.all(16), - child: ElevatedButton( - key: Key(MqKeys.loginTypeName('google')), + child: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + const SizedBox(height: 100), + Align( + child: Text( + '${context.l10n.welcome}!', + style: context.titleLarge!.copyWith(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 20), + Text( + context.l10n.email, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + 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: 16), + Text( + context.l10n.password, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + const SizedBox(height: 8), + PasswordFormFiled(controller: passwordController), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + AppBottomSheet.showBottomSheet( + context, + initialChildSize: 0.4, + ForgotPasswordForm( + controller: forgotPasswordEmailController, + ), + ); + }, + child: Text( + context.l10n.forgotPassword, + style: context.labelMedium!.copyWith(color: context.colors.error), + ), + ), + ), + const SizedBox(height: 16), + CustomButton( + key: Key(MqKeys.loginTypeName('email')), + text: context.l10n.signIn, onPressed: () async { + if (formKey.currentState!.validate()) { + MqAnalytic.track( + AnalyticKey.tapLogin, + params: {'soccial': 'email'}, + ); + unawaited(AppAlert.showLoading(context)); + await context.read().signInWithEmail( + emailController.text, + passwordController.text, + ); + if (context.mounted) context.loaderOverlay.hide(); + } + }, + ), + 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.tapLoginWithSoccial, + AnalyticKey.tapLogin, params: {'soccial': 'google'}, ); 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( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black26), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Assets.icons.googleIcon.svg(height: 25), + const SizedBox(width: 10), + Text(context.l10n.google, style: context.bodyMedium), + ], ), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Assets.icons.googleIcon.svg(height: 25), - const SizedBox(width: 10), - Text(context.l10n.google, style: context.bodyMedium), - ], ), ), - ), - // 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), - TextButton( - onPressed: () { - MqAnalytic.track(AnalyticKey.tapPrivacyPolicy); - AppLaunch.launchURL(apiConst.provicyPolicy); - }, - child: Text( - context.l10n.privacyPolicy, - style: context.bodyLarge!.copyWith( - color: context.colors.primary, - decoration: TextDecoration.underline, + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(context.l10n.dontHaveAccount), + TextButton( + onPressed: () { + MqAnalytic.track(AnalyticKey.goRegister); + context.goNamed(AppRouter.register); + }, + child: Text(context.l10n.signUp), + ), + ], + ), + const SizedBox(height: 40), + TextButton( + onPressed: () { + MqAnalytic.track(AnalyticKey.tapPrivacyPolicy); + AppLaunch.launchURL(apiConst.provicyPolicy); + }, + child: Text( + context.l10n.privacyPolicy, + style: context.bodyLarge!.copyWith( + decoration: TextDecoration.underline, + ), ), ), - ), - const SizedBox(height: 20), - ], + const SizedBox(height: 10), + ], + ), ), ), ); diff --git a/app/lib/modules/login/presentation/view/sign_up_view.dart b/app/lib/modules/login/presentation/view/sign_up_view.dart new file mode 100644 index 00000000..224cc45a --- /dev/null +++ b/app/lib/modules/login/presentation/view/sign_up_view.dart @@ -0,0 +1,196 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +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'; +import 'package:my_quran/core/core.dart'; +import 'package:my_quran/l10n/l10.dart'; +import 'package:my_quran/theme/theme.dart'; +import 'package:my_quran/utils/urils.dart'; + +class SignUpView extends StatelessWidget { + SignUpView({super.key}); + + final usernameController = TextEditingController(); + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final confirmPasswordController = TextEditingController(); + final formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: const Key(MqKeys.registerView), + 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, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + const SizedBox(height: 100), + Align( + child: Text( + context.l10n.signUp, + textAlign: TextAlign.center, + style: context.titleLarge!.copyWith(fontSize: 24, fontWeight: FontWeight.bold), + ), + ), + const SizedBox(height: 40), + Text( + context.l10n.username, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + const SizedBox(height: 8), + CustomTextFormField( + controller: usernameController, + labelText: context.l10n.username, + validator: (value) { + if (value == null || value.isEmpty) { + return context.l10n.usernameRequired; + } + return null; + }, + ), + const SizedBox(height: 25), + Text( + context.l10n.email, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + 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: 25), + Text( + context.l10n.password, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + const SizedBox(height: 8), + PasswordFormFiled(controller: passwordController), + const SizedBox(height: 25), + Text( + context.l10n.confirmPassword, + style: context.bodyMedium!.copyWith(color: context.colors.secondary), + ), + const SizedBox(height: 8), + PasswordFormFiled( + controller: confirmPasswordController, + validator: (value) { + if (value == null || value.isEmpty) { + return context.l10n.passwordRequired; + } + if (!AppRegExp.password.hasMatch(value)) { + return context.l10n.passwordInvalid; + } + if (passwordController.text != confirmPasswordController.text) { + return context.l10n.passwordMismatchError; + } + return null; + }, + ), + const SizedBox(height: 50), + CustomButton( + key: Key(MqKeys.registerTypeName('email')), + text: context.l10n.signUp, + onPressed: () async { + if (formKey.currentState!.validate()) { + MqAnalytic.track( + AnalyticKey.tapSignUp, + params: {'method': 'email'}, + ); + unawaited(AppAlert.showLoading(context)); + await context.read().signUpWithEmail( + email: emailController.text, + password: passwordController.text, + username: usernameController.text, + ); + if (context.mounted) context.loaderOverlay.hide(); + } + }, + ), + 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, + children: [ + Assets.icons.googleIcon.svg(height: 25), + const SizedBox(width: 10), + Text(context.l10n.google, style: context.bodyMedium), + ], + ), + ), + ), + const SizedBox(height: 40), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(context.l10n.alreadyHaveAccount), + TextButton( + onPressed: () => context.goNamed(AppRouter.loginWihtSoccial), + child: Text(context.l10n.signIn), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/utils/reg_exp/app_reg_exp.dart b/app/lib/utils/reg_exp/app_reg_exp.dart index ee907091..5c7ceb3a 100644 --- a/app/lib/utils/reg_exp/app_reg_exp.dart +++ b/app/lib/utils/reg_exp/app_reg_exp.dart @@ -2,4 +2,6 @@ class AppRegExp { const AppRegExp._(); static final duration = RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$'); + static final email = RegExp(r'^[^@]+@[^@]+\.[^@]+$'); + static final password = RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$'); } diff --git a/app/lib/utils/show/alerts.dart b/app/lib/utils/show/alerts.dart index 7cc8d1c8..cd64ce0d 100644 --- a/app/lib/utils/show/alerts.dart +++ b/app/lib/utils/show/alerts.dart @@ -102,4 +102,26 @@ final class AppAlert { }, ); } + + static void showInfoDialog( + BuildContext context, { + required String title, + required String content, + }) { + showAdaptiveDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text(title), + content: Text(content), + actions: [ + CupertinoButton( + onPressed: () => Navigator.pop(context), + child: const Text('OK'), + ), + ], + ); + }, + ); + } } diff --git a/app/test/helpers/pump_app.dart b/app/test/helpers/pump_app.dart index 1bdda3b0..a6978d75 100644 --- a/app/test/helpers/pump_app.dart +++ b/app/test/helpers/pump_app.dart @@ -14,11 +14,14 @@ extension PumpApp on WidgetTester { GetAppVersionUseCase getAppVersionUseCase, GoogleSignInUseCase googleSignInUseCase, AppleSignInUseCase appleSignInUseCase, + EmailSignInUseCase emailSignIn, + EmailSignUpUseCase emailSignUp, SerUserDataUseCase setUserDataUseCase, HomeRepository homeRepo, PatchGenderUseCase patchGenderUseCase, PatchLocaleCodeUseCase patchLocaleCodeUseCase, LogoutUseCase logoutUseCase, + ForgotPasswordUseCase forgotPasswordUseCase, ) { return pumpWidget( RepositoryProvider( @@ -38,10 +41,13 @@ extension PumpApp on WidgetTester { getInitialUserUseCase, googleSignInUseCase, appleSignInUseCase, + emailSignIn, + emailSignUp, setUserDataUseCase, patchGenderUseCase, patchLocaleCodeUseCase, logoutUseCase, + forgotPasswordUseCase, ), ), BlocProvider( diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 38ca7330..5d645072 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -55,6 +55,8 @@ void main() { final googleSignInUseCase = GoogleSignInUseCase(authRepository); final appleSignInUseCase = AppleSignInUseCase(authRepository); + final emailSignIn = EmailSignInUseCase(authRepository); + final emailSignUp = EmailSignUpUseCase(authRepository); final setUserDataUseCase = SerUserDataUseCase(authRepository); final patchLocaleCodeUseCase = PatchLocaleCodeUseCase(authRepository); final pathGenderUseCase = PatchGenderUseCase(authRepository); @@ -62,6 +64,7 @@ void main() { final setColorUseCase = SetColorUseCase(themeRepository); final getAppVersionUseCase = GetAppVersionUseCase(appRepository); final logoutUseCase = LogoutUseCase(authRepository); + final forgotPasswordUseCase = ForgotPasswordUseCase(authRepository); when(() => storage.readString(key: StorageKeys.tokenKey)).thenReturn(null); when(() => storage.readString(key: StorageKeys.genderKey)).thenReturn(null); @@ -77,11 +80,14 @@ void main() { getAppVersionUseCase, googleSignInUseCase, appleSignInUseCase, + emailSignIn, + emailSignUp, setUserDataUseCase, homeRepo, pathGenderUseCase, patchLocaleCodeUseCase, logoutUseCase, + forgotPasswordUseCase, ); await tester.pumpAndSettle(); expect(find.byType(MaterialApp), findsOneWidget); diff --git a/app/test_driver/app/login/login_test.dart b/app/test_driver/app/login/login_test.dart index ee777b27..9e8222bb 100644 --- a/app/test_driver/app/login/login_test.dart +++ b/app/test_driver/app/login/login_test.dart @@ -19,10 +19,28 @@ Future selectGender(FlutterDriver driver) async { await driver.takeScreenshot(Screenshots.loginGenderMalePage); } -Future loginWithGoogleApple(FlutterDriver driver) async { +/* +These functions are placeholders for future implementation. + +Future registerWithEmail(FlutterDriver driver) async { + await driver.waitFor(find.byValueKey(MqKeys.registerView)); + await driver.tap(find.byValueKey(MqKeys.registerTypeName('email'))); + await driver.takeScreenshot(Screenshots.registerEmailPage); + await Future.delayed(const Duration(seconds: 1)); +} + +Future loginWithEmail(FlutterDriver driver) async { + await driver.waitFor(find.byValueKey(MqKeys.signInView)); + await driver.tap(find.byValueKey(MqKeys.loginTypeName('email'))); + await driver.takeScreenshot(Screenshots.loginEmailPage); + await Future.delayed(const Duration(seconds: 1)); +} +*/ + +Future loginWithGoogle(FlutterDriver driver) async { await driver.waitFor(find.byValueKey(MqKeys.signInView)); await driver.tap(find.byValueKey(MqKeys.loginTypeName('google'))); - await driver.takeScreenshot(Screenshots.loginApplePage); + await driver.takeScreenshot(Screenshots.loginGooglePage); await Future.delayed(const Duration(seconds: 1)); } diff --git a/app/test_driver/app_test.dart b/app/test_driver/app_test.dart index 8054f255..412b634c 100644 --- a/app/test_driver/app_test.dart +++ b/app/test_driver/app_test.dart @@ -33,7 +33,7 @@ void main() async { }); test('sign-in-with-google-account', () async { - await loginWithGoogleApple(driver); + await loginWithGoogle(driver); }); }); diff --git a/app/test_driver/extensions/screenshot_name.dart b/app/test_driver/extensions/screenshot_name.dart index 8e3e39c5..a2a55ce9 100644 --- a/app/test_driver/extensions/screenshot_name.dart +++ b/app/test_driver/extensions/screenshot_name.dart @@ -6,6 +6,8 @@ class Screenshots { static const loginGenderFemalePage = '004-login-gender-page-male'; static const loginGooglePage = '005-login-google-page'; static const loginApplePage = '006-login-apple-page'; + static const loginEmailPage = '007-login-email-page'; + static const registerEmailPage = '008-register-email-page'; /// 21-40 home static const homeInit = '021-home-init'; diff --git a/packages/mq_ci_keys/lib/src/mq_ci_keys_base.dart b/packages/mq_ci_keys/lib/src/mq_ci_keys_base.dart index 37af715a..4164faae 100644 --- a/packages/mq_ci_keys/lib/src/mq_ci_keys_base.dart +++ b/packages/mq_ci_keys/lib/src/mq_ci_keys_base.dart @@ -4,6 +4,7 @@ final class MqKeys { static const loginNext = 'login-next'; static const loginSelectGender = 'login-select-gender'; static const signInView = 'sign-in-view'; + static const registerView = 'register-view'; // home static const home = 'home'; @@ -73,6 +74,10 @@ final class MqKeys { static const loginType = 'login'; static String loginTypeName(String name) => 'loginType-$name'; + // register type + static const registerType = 'register'; + static String registerTypeName(String name) => 'registerType-$name'; + // language static const language = 'language'; static String languageCode(String localeCode) => 'language-$localeCode';