From 59df5bdce4b0ef3e24e34ac2bf9a72511ddce6e7 Mon Sep 17 00:00:00 2001 From: Aidaiym Date: Thu, 18 Jul 2024 14:13:27 +0600 Subject: [PATCH 1/2] add mock data source to app --- app/lib/app/data/data.dart | 10 +- .../data/source/auth_local_data_source.dart | 57 +---- .../data/source/auth_remote_data_source.dart | 143 +----------- .../app_local_data_source_impl.dart} | 0 .../local/auth_local_data_source_impl.dart | 59 +++++ .../local/theme_local_data_source_impl.dart | 34 +++ .../mock/auth_local_data_source_mock.dart | 33 +++ .../mock/auth_remote_data_source_mock.dart | 96 ++++++++ .../mock/theme_local_data_source_mock.dart | 19 ++ .../remote/auth_remote_data_source_impl.dart | 208 ++++++++++++++++++ .../data/source/theme_local_data_source.dart | 31 +-- app/lib/app/presentation/view/app_view.dart | 31 ++- app/lib/config/app_config.dart | 2 + app/test/widget_test.dart | 6 +- 14 files changed, 504 insertions(+), 225 deletions(-) rename app/lib/app/data/source/{app_local_data_source.dart => local/app_local_data_source_impl.dart} (100%) create mode 100644 app/lib/app/data/source/local/auth_local_data_source_impl.dart create mode 100644 app/lib/app/data/source/local/theme_local_data_source_impl.dart create mode 100644 app/lib/app/data/source/mock/auth_local_data_source_mock.dart create mode 100644 app/lib/app/data/source/mock/auth_remote_data_source_mock.dart create mode 100644 app/lib/app/data/source/mock/theme_local_data_source_mock.dart create mode 100644 app/lib/app/data/source/remote/auth_remote_data_source_impl.dart diff --git a/app/lib/app/data/data.dart b/app/lib/app/data/data.dart index 1619b9d1..797b28b2 100644 --- a/app/lib/app/data/data.dart +++ b/app/lib/app/data/data.dart @@ -1,10 +1,16 @@ export 'repository/app_repositoty_impl.dart'; export 'repository/theme_repository_impl.dart'; export 'repository/auth_repositoty_impl.dart'; -export 'source/app_local_data_source.dart'; -export 'source/theme_local_data_source.dart'; +export 'source/local/app_local_data_source_impl.dart'; +export 'source/local/theme_local_data_source_impl.dart'; +export 'source/local/auth_local_data_source_impl.dart'; +export 'source/remote/auth_remote_data_source_impl.dart'; export 'source/auth_local_data_source.dart'; export 'source/auth_remote_data_source.dart'; +export 'source/theme_local_data_source.dart'; +export 'source/mock/auth_local_data_source_mock.dart'; +export 'source/mock/auth_remote_data_source_mock.dart'; +export 'source/mock/theme_local_data_source_mock.dart'; export 'models/user_model.dart'; export 'models/token_response.dart'; export 'models/user_data_response.dart'; diff --git a/app/lib/app/data/source/auth_local_data_source.dart b/app/lib/app/data/source/auth_local_data_source.dart index faf8dccc..b3a4986a 100644 --- a/app/lib/app/data/source/auth_local_data_source.dart +++ b/app/lib/app/data/source/auth_local_data_source.dart @@ -1,53 +1,10 @@ -import 'package:meta/meta.dart'; -import 'package:mq_storage/mq_storage.dart'; import 'package:my_quran/app/app.dart'; -import 'package:my_quran/constants/contants.dart'; -@immutable -final class AuthLocalDataSource { - const AuthLocalDataSource(this.storage); - - final PreferencesStorage storage; - - UserEntity? get init { - final userToken = storage.readString(key: StorageKeys.tokenKey); - final userGender = storage.readString(key: StorageKeys.genderKey); - final username = storage.readString(key: StorageKeys.usernameKey); - final localeCode = storage.readString(key: StorageKeys.localeKey); - if (userToken == null && userGender == null && username == null) return null; - return UserEntity( - accessToken: userToken!, - username: username!, - gender: userGender == Gender.male.name ? Gender.male : Gender.female, - localeCode: localeCode ?? 'en', - ); - } - - String? getToken() => storage.readString(key: StorageKeys.tokenKey); - - Future saveUserData(UserEntity userEntity) async { - await Future.wait([ - storage.writeString(key: StorageKeys.localeKey, value: userEntity.localeCode), - storage.writeString(key: StorageKeys.genderKey, value: userEntity.gender.name), - storage.writeString(key: StorageKeys.usernameKey, value: userEntity.username), - ]); - } - - Future saveGender(Gender gender) { - return storage.writeString( - key: StorageKeys.genderKey, - value: gender.name, - ); - } - - Future saveLocaleCode(String localeCode) { - return storage.writeString( - key: StorageKeys.localeKey, - value: localeCode, - ); - } - - Future logoutLocal() async { - await storage.clear(); - } +abstract class AuthLocalDataSource { + UserEntity? get init; + String? getToken(); + Future saveUserData(UserEntity userEntity); + Future saveGender(Gender gender); + Future saveLocaleCode(String localeCode); + Future logoutLocal(); } 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 9ea94695..e5a57563 100644 --- a/app/lib/app/data/source/auth_remote_data_source.dart +++ b/app/lib/app/data/source/auth_remote_data_source.dart @@ -1,155 +1,36 @@ -import 'package:meta/meta.dart'; -import 'package:mq_storage/mq_storage.dart'; import 'package:my_quran/app/app.dart'; -import 'package:my_quran/config/config.dart'; -import 'package:my_quran/constants/contants.dart'; import 'package:my_quran/core/core.dart'; -@immutable -final class AuthRemoteDataSource { - const AuthRemoteDataSource({ - required this.client, - required this.storage, - required this.soccialAuth, - required this.isIntegrationTest, - }); +abstract class AuthRemoteDataSource { + Future loginWithEmail(String email); - final MqDio client; - final PreferencesStorage storage; - final SoccialAuth soccialAuth; - final bool isIntegrationTest; + Future> fetchSmsCode({ + required String code, + required String languageCode, + required Gender gender, + }); Future> signInWithGoogle( String languageCode, Gender gender, - ) async { - final googleAuth = await _getGoogleAuth(); - - final token = await client.postType( - apiConst.loginWithGoogle, - fromJson: TokenResponse.fromJson, - body: {'access_token': googleAuth.accessToken}, - ); - - return token.fold(Left.new, (r) async { - final user = UserModelResponse( - accessToken: r.key, - username: googleAuth.name, - gender: gender, - localeCode: languageCode, - ); - - await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); - - return Right(user); - }); - } - - Future<_UserReqParam> _getGoogleAuth() async { - if (isIntegrationTest) { - return const _UserReqParam( - name: 'Test User', - accessToken: r'myquran_te$t_t0ken', - ); - } else { - final googleAuth = await soccialAuth.signInWithGoogle(); - final accessToken = googleAuth.credential?.accessToken ?? ''; - final username = googleAuth.user?.displayName ?? ''; - return _UserReqParam( - name: username, - accessToken: accessToken, - ); - } - } + ); Future> signInWithApple( String languageCode, Gender gender, - ) async { - final appleAuth = await _getAppleAuth(); - - final token = await client.postType( - apiConst.loginWithApple, - fromJson: TokenResponse.fromJson, - body: {'access_token': appleAuth.accessToken}, - ); + ); - return token.fold(Left.new, (r) async { - final user = UserModelResponse( - accessToken: r.key, - username: appleAuth.name, - gender: gender, - localeCode: languageCode, - ); - - await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); - - return Right(user); - }); - } - - Future<_UserReqParam> _getAppleAuth() async { - if (isIntegrationTest) { - return const _UserReqParam( - name: 'Test User', - accessToken: r'myquran_te$t_t0ken', - ); - } else { - final appleAuth = await soccialAuth.signInWithApple(); - final accessToken = appleAuth.credential?.accessToken ?? ''; - final username = appleAuth.user?.displayName ?? ''; - return _UserReqParam( - name: username, - accessToken: accessToken, - ); - } - } - - Future> saveUserData(UserEntity userEntity) { - return client.putType( - apiConst.putProfile(userEntity.accessToken), - fromJson: UserDataResponse.fromJson, - body: { - 'gender': userEntity.gender.name.toUpperCase(), - 'language': userEntity.localeCode.toUpperCase(), - }, - ); - } + Future> saveUserData(UserEntity userEntity); Future> pathGender({ required String userId, required Gender gender, - }) { - return client.patchType( - apiConst.putProfile(userId), - fromJson: UserDataResponse.fromJson, - body: {'gender': gender.name.toUpperCase()}, - ); - } + }); Future> pathLocaleCode({ required String userId, required String localeCode, - }) { - return client.patchType( - apiConst.putProfile(userId), - fromJson: UserDataResponse.fromJson, - body: {'language': localeCode.toUpperCase()}, - ); - } - - Future logoutRemote() async { - await soccialAuth.logOut(); - } -} - -@immutable -final class _UserReqParam { - const _UserReqParam({ - required this.name, - required this.accessToken, }); - final String name; - final String accessToken; + Future logoutRemote(); } diff --git a/app/lib/app/data/source/app_local_data_source.dart b/app/lib/app/data/source/local/app_local_data_source_impl.dart similarity index 100% rename from app/lib/app/data/source/app_local_data_source.dart rename to app/lib/app/data/source/local/app_local_data_source_impl.dart diff --git a/app/lib/app/data/source/local/auth_local_data_source_impl.dart b/app/lib/app/data/source/local/auth_local_data_source_impl.dart new file mode 100644 index 00000000..8728965a --- /dev/null +++ b/app/lib/app/data/source/local/auth_local_data_source_impl.dart @@ -0,0 +1,59 @@ +import 'package:meta/meta.dart'; +import 'package:mq_storage/mq_storage.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/constants/contants.dart'; + +@immutable +final class AuthLocalDataSourceImpl implements AuthLocalDataSource { + const AuthLocalDataSourceImpl(this.storage); + + final PreferencesStorage storage; + + @override + UserEntity? get init { + final userToken = storage.readString(key: StorageKeys.tokenKey); + final userGender = storage.readString(key: StorageKeys.genderKey); + final username = storage.readString(key: StorageKeys.usernameKey); + final localeCode = storage.readString(key: StorageKeys.localeKey); + if (userToken == null && userGender == null && username == null) return null; + return UserEntity( + accessToken: userToken!, + username: username!, + gender: userGender == Gender.male.name ? Gender.male : Gender.female, + localeCode: localeCode ?? 'en', + ); + } + + @override + String? getToken() => storage.readString(key: StorageKeys.tokenKey); + + @override + Future saveUserData(UserEntity userEntity) async { + await Future.wait([ + storage.writeString(key: StorageKeys.localeKey, value: userEntity.localeCode), + storage.writeString(key: StorageKeys.genderKey, value: userEntity.gender.name), + storage.writeString(key: StorageKeys.usernameKey, value: userEntity.username), + ]); + } + + @override + Future saveGender(Gender gender) { + return storage.writeString( + key: StorageKeys.genderKey, + value: gender.name, + ); + } + + @override + Future saveLocaleCode(String localeCode) { + return storage.writeString( + key: StorageKeys.localeKey, + value: localeCode, + ); + } + + @override + Future logoutLocal() async { + await storage.clear(); + } +} diff --git a/app/lib/app/data/source/local/theme_local_data_source_impl.dart b/app/lib/app/data/source/local/theme_local_data_source_impl.dart new file mode 100644 index 00000000..c940617a --- /dev/null +++ b/app/lib/app/data/source/local/theme_local_data_source_impl.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/constants/contants.dart'; +import 'package:mq_storage/mq_storage.dart'; +import 'package:my_quran/theme/theme.dart'; + +@immutable +final class ThemeLocalDataSourceImpl implements ThemeLocalDataSource { + const ThemeLocalDataSourceImpl(this.storage); + final PreferencesStorage storage; + + @override + CustomTheme get initialTheme { + final isDark = storage.readBool(key: StorageKeys.modeKey); + final cachedColorIndex = storage.readInt(key: StorageKeys.colorKey); + final brightness = isDark != null ? (isDark ? Brightness.dark : Brightness.light) : Brightness.light; + final targetColor = _getColor(cachedColorIndex); + return CustomTheme(brightness, targetColor); + } + + @override + Future saveThemePrimaryColor(int index) async { + await storage.writeInt(key: StorageKeys.colorKey, value: index); + } + + @override + Future saveThemeMode({required bool isDark}) async { + await storage.writeBool(key: StorageKeys.modeKey, value: isDark); + } + + Color _getColor(int? cacheColor) { + return TargetColor.fromIndex(cacheColor ?? 0).color; + } +} diff --git a/app/lib/app/data/source/mock/auth_local_data_source_mock.dart b/app/lib/app/data/source/mock/auth_local_data_source_mock.dart new file mode 100644 index 00000000..12839ce0 --- /dev/null +++ b/app/lib/app/data/source/mock/auth_local_data_source_mock.dart @@ -0,0 +1,33 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; + +@immutable +final class AuthLocalDataSourceMock implements AuthLocalDataSource { + const AuthLocalDataSourceMock(); + + @override + UserEntity? get init { + return null; + } + + @override + String? getToken() => ''; + + @override + Future saveUserData(UserEntity userEntity) async {} + + @override + Future saveGender(Gender gender) { + return Future.value(); + } + + @override + Future saveLocaleCode(String localeCode) { + return Future.value(); + } + + @override + Future logoutLocal() async { + return Future.value(); + } +} diff --git a/app/lib/app/data/source/mock/auth_remote_data_source_mock.dart b/app/lib/app/data/source/mock/auth_remote_data_source_mock.dart new file mode 100644 index 00000000..0a9b7c45 --- /dev/null +++ b/app/lib/app/data/source/mock/auth_remote_data_source_mock.dart @@ -0,0 +1,96 @@ +import 'package:meta/meta.dart'; +import 'package:my_quran/app/app.dart'; + +import 'package:my_quran/core/core.dart'; + +@immutable +final class AuthRemoteDataSourceMock implements AuthRemoteDataSource { + const AuthRemoteDataSourceMock(); + + @override + Future loginWithEmail(String email) async { + return; + } + + @override + Future> fetchSmsCode({ + required String code, + required String languageCode, + required Gender gender, + }) async { + final user = UserModelResponse( + accessToken: r'myquran_te$t_t0ken', + username: 'Test User', + gender: gender, + localeCode: languageCode, + ); + + return Right(user); + } + + @override + Future> signInWithGoogle( + String languageCode, + Gender gender, + ) async { + final user = UserModelResponse( + accessToken: 'mock_token_google', + username: 'Mock Google User', + gender: gender, + localeCode: languageCode, + ); + return Right(user); + } + + @override + Future> signInWithApple( + String languageCode, + Gender gender, + ) async { + final user = UserModelResponse( + accessToken: 'mock_token_apple', + username: 'Mock Apple User', + gender: gender, + localeCode: languageCode, + ); + return Right(user); + } + + @override + Future> saveUserData(UserEntity userEntity) async { + final response = UserDataResponse( + gender: userEntity.gender.toString(), + language: userEntity.localeCode, + ); + return Right(response); + } + + @override + Future> pathGender({ + required String userId, + required Gender gender, + }) async { + final response = UserDataResponse( + gender: Gender.male.toString(), + language: 'en', + ); + return Right(response); + } + + @override + Future> pathLocaleCode({ + required String userId, + required String localeCode, + }) async { + final response = UserDataResponse( + gender: Gender.male.toString(), + language: 'en', + ); + return Right(response); + } + + @override + Future logoutRemote() async { + return; + } +} diff --git a/app/lib/app/data/source/mock/theme_local_data_source_mock.dart b/app/lib/app/data/source/mock/theme_local_data_source_mock.dart new file mode 100644 index 00000000..630660db --- /dev/null +++ b/app/lib/app/data/source/mock/theme_local_data_source_mock.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/theme/theme.dart'; + +@immutable +final class ThemeLocalDataSourceMock implements ThemeLocalDataSource { + const ThemeLocalDataSourceMock(); + + @override + CustomTheme get initialTheme { + return const CustomTheme(Brightness.light, Colors.red); + } + + @override + Future saveThemePrimaryColor(int index) async {} + + @override + Future saveThemeMode({required bool isDark}) async {} +} diff --git a/app/lib/app/data/source/remote/auth_remote_data_source_impl.dart b/app/lib/app/data/source/remote/auth_remote_data_source_impl.dart new file mode 100644 index 00000000..3d4615b7 --- /dev/null +++ b/app/lib/app/data/source/remote/auth_remote_data_source_impl.dart @@ -0,0 +1,208 @@ +import 'package:meta/meta.dart'; +import 'package:mq_storage/mq_storage.dart'; +import 'package:my_quran/app/app.dart'; +import 'package:my_quran/config/config.dart'; +import 'package:my_quran/constants/contants.dart'; +import 'package:my_quran/core/core.dart'; + +@immutable +final class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + const AuthRemoteDataSourceImpl({ + required this.client, + required this.storage, + required this.soccialAuth, + required this.isIntegrationTest, + }); + + final MqDio client; + final PreferencesStorage storage; + final SoccialAuth soccialAuth; + final bool isIntegrationTest; + + @override + Future> signInWithGoogle( + String languageCode, + Gender gender, + ) async { + final googleAuth = await _getGoogleAuth(); + + final token = await client.postType( + apiConst.loginWithGoogle, + fromJson: TokenResponse.fromJson, + body: {'access_token': googleAuth.accessToken}, + ); + + return token.fold(Left.new, (r) async { + final user = UserModelResponse( + accessToken: r.key, + username: googleAuth.name, + gender: gender, + localeCode: languageCode, + ); + + await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); + + return Right(user); + }); + } + + Future<_UserReqParam> _getGoogleAuth() async { + if (isIntegrationTest) { + return const _UserReqParam( + name: 'Test User', + accessToken: r'myquran_te$t_t0ken', + ); + } else { + final googleAuth = await soccialAuth.signInWithGoogle(); + final accessToken = googleAuth.credential?.accessToken ?? ''; + final username = googleAuth.user?.displayName ?? ''; + return _UserReqParam( + name: username, + accessToken: accessToken, + ); + } + } + + @override + Future> signInWithApple( + String languageCode, + Gender gender, + ) async { + final appleAuth = await _getAppleAuth(); + + final token = await client.postType( + apiConst.loginWithApple, + fromJson: TokenResponse.fromJson, + body: {'access_token': appleAuth.accessToken}, + ); + + return token.fold(Left.new, (r) async { + final user = UserModelResponse( + accessToken: r.key, + username: appleAuth.name, + gender: gender, + localeCode: languageCode, + ); + + await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); + + return Right(user); + }); + } + + Future<_UserReqParam> _getAppleAuth() async { + if (isIntegrationTest) { + return const _UserReqParam( + name: 'Test User', + accessToken: r'myquran_te$t_t0ken', + ); + } else { + final appleAuth = await soccialAuth.signInWithApple(); + final accessToken = appleAuth.credential?.accessToken ?? ''; + final username = appleAuth.user?.displayName ?? ''; + return _UserReqParam( + name: username, + accessToken: accessToken, + ); + } + } + + @override + Future> saveUserData(UserEntity userEntity) { + return client.putType( + apiConst.putProfile(userEntity.accessToken), + fromJson: UserDataResponse.fromJson, + body: { + 'gender': userEntity.gender.name.toUpperCase(), + 'language': userEntity.localeCode.toUpperCase(), + }, + ); + } + + @override + Future> pathGender({ + required String userId, + required Gender gender, + }) { + return client.patchType( + apiConst.putProfile(userId), + fromJson: UserDataResponse.fromJson, + body: {'gender': gender.name.toUpperCase()}, + ); + } + + @override + Future> pathLocaleCode({ + required String userId, + required String localeCode, + }) { + return client.patchType( + apiConst.putProfile(userId), + fromJson: UserDataResponse.fromJson, + body: {'language': localeCode.toUpperCase()}, + ); + } + + @override + Future logoutRemote() async { + await soccialAuth.logOut(); + } + + @override + Future> fetchSmsCode({ + required String code, + required String languageCode, + required Gender gender, + }) async { + try { + final token = await client.post( + 'apiConst.fetchSmsCode', + fromJson: TokenResponse.fromJson, + body: {'code': code}, + ); + return token.fold( + (error) { + return Left(error); + }, + (r) async { + final user = UserModelResponse( + accessToken: r.key, + username: '', + gender: gender, + localeCode: languageCode, + ); + + await storage.writeString(key: StorageKeys.tokenKey, value: user.accessToken); + + return Right(user); + }, + ); + } catch (e) { + return Left(Exception('Error fetching SMS code: $e')); + } + } + + @override + Future loginWithEmail(String email) async { + try { + await client.post( + 'apiConst.loginWithEmail', + fromJson: TokenResponse.fromJson, + body: {'email': email}, + ); + } catch (e) { + throw Exception('Error during login: $e'); + } + } +} + +@immutable +final class _UserReqParam { + const _UserReqParam({ + required this.name, + required this.accessToken, + }); + + final String name; + final String accessToken; +} diff --git a/app/lib/app/data/source/theme_local_data_source.dart b/app/lib/app/data/source/theme_local_data_source.dart index 3c33c02e..0ead7590 100644 --- a/app/lib/app/data/source/theme_local_data_source.dart +++ b/app/lib/app/data/source/theme_local_data_source.dart @@ -1,30 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:my_quran/constants/contants.dart'; -import 'package:mq_storage/mq_storage.dart'; import 'package:my_quran/theme/theme.dart'; -@immutable -final class ThemeLocalDataSource { - const ThemeLocalDataSource(this.storage); - final PreferencesStorage storage; - - CustomTheme get initialTheme { - final isDark = storage.readBool(key: StorageKeys.modeKey); - final cachedColorIndex = storage.readInt(key: StorageKeys.colorKey); - final brightness = isDark != null ? (isDark ? Brightness.dark : Brightness.light) : Brightness.light; - final targetColor = _getColor(cachedColorIndex); - return CustomTheme(brightness, targetColor); - } - - Future saveThemePrimaryColor(int index) async { - await storage.writeInt(key: StorageKeys.colorKey, value: index); - } - - Future saveThemeMode({required bool isDark}) async { - await storage.writeBool(key: StorageKeys.modeKey, value: isDark); - } - - Color _getColor(int? cacheColor) { - return TargetColor.fromIndex(cacheColor ?? 0).color; - } +abstract class ThemeLocalDataSource { + CustomTheme get initialTheme; + Future saveThemePrimaryColor(int index); + Future saveThemeMode({required bool isDark}); } diff --git a/app/lib/app/presentation/view/app_view.dart b/app/lib/app/presentation/view/app_view.dart index e2522852..b89ba96c 100644 --- a/app/lib/app/presentation/view/app_view.dart +++ b/app/lib/app/presentation/view/app_view.dart @@ -17,6 +17,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final isMockData = context.read().isMockData; return MultiBlocProvider( providers: [ RepositoryProvider( @@ -28,7 +29,9 @@ class MyApp extends StatelessWidget { ), RepositoryProvider( create: (context) => ThemeRepositoryImpl( - ThemeLocalDataSource(context.read()), + isMockData + ? const ThemeLocalDataSourceMock() + : ThemeLocalDataSourceImpl(context.read()), ), ), BlocProvider( @@ -40,17 +43,21 @@ class MyApp extends StatelessWidget { ), ), RepositoryProvider( - create: (context) => AuthRepositoryImpl( - localDataSource: AuthLocalDataSource( - context.read(), - ), - remoteDataSource: AuthRemoteDataSource( - client: context.read(), - storage: context.read(), - soccialAuth: context.read(), - isIntegrationTest: context.read().isIntegrationTest, - ), - ), + create: (context) { + return AuthRepositoryImpl( + localDataSource: isMockData + ? const AuthLocalDataSourceMock() + : AuthLocalDataSourceImpl(context.read()), + remoteDataSource: isMockData + ? const AuthRemoteDataSourceMock() + : AuthRemoteDataSourceImpl( + client: context.read(), + storage: context.read(), + soccialAuth: context.read(), + isIntegrationTest: context.read().isIntegrationTest, + ), + ); + }, ), BlocProvider( create: (context) => AuthCubit( diff --git a/app/lib/config/app_config.dart b/app/lib/config/app_config.dart index 81c66e0d..3b335e3a 100644 --- a/app/lib/config/app_config.dart +++ b/app/lib/config/app_config.dart @@ -7,9 +7,11 @@ final class AppConfig { const AppConfig({ this.storage, this.isIntegrationTest = false, + this.isMockData = false, }); final bool isIntegrationTest; + final bool isMockData; final PreferencesStorage? storage; diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 7b154882..97ab9912 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -19,10 +19,10 @@ void main() { final homeRepo = MockHomeRepositoryImpl(); final appLocalDataSource = AppLocalDataSource(packageInfo: packageInfo); final appRepository = AppRepositoryImpl(appLocalDataSource); - final themeRepository = ThemeRepositoryImpl(ThemeLocalDataSource(storage)); + final themeRepository = ThemeRepositoryImpl(ThemeLocalDataSourceImpl(storage)); final authRepository = AuthRepositoryImpl( - localDataSource: AuthLocalDataSource(storage), - remoteDataSource: AuthRemoteDataSource( + localDataSource: AuthLocalDataSourceImpl(storage), + remoteDataSource: AuthRemoteDataSourceImpl( client: remoteClient, storage: storage, soccialAuth: MockSccialAuth(), From 174b355887f3e69414942a3f41aa757136c4b638 Mon Sep 17 00:00:00 2001 From: Aidaiym Date: Sat, 20 Jul 2024 12:28:21 +0600 Subject: [PATCH 2/2] Add email auth --- .../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/app_config.dart | 2 +- 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 | 90 +++++++++++ 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 + 27 files changed, 397 insertions(+), 78 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 fab4875c..99d501dd 100644 --- a/app/lib/app/data/repository/auth_repositoty_impl.dart +++ b/app/lib/app/data/repository/auth_repositoty_impl.dart @@ -19,6 +19,44 @@ final class AuthRepositoryImpl implements AuthRepository { return localDataSource.init; } + @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 setUserData(UserEntity userEntity) async { try { diff --git a/app/lib/app/domain/domain.dart b/app/lib/app/domain/domain.dart index a0ad0e83..3f5926fb 100644 --- a/app/lib/app/domain/domain.dart +++ b/app/lib/app/domain/domain.dart @@ -11,6 +11,8 @@ export 'usecase/apple_sign_in_use_case.dart'; 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/email_login_usecase.dart'; +export 'usecase/fetch_sms_code_use_case.dart'; export 'usecase/logout_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..73f2454b 100644 --- a/app/lib/app/presentation/cubit/auth_cubit.dart +++ b/app/lib/app/presentation/cubit/auth_cubit.dart @@ -12,6 +12,8 @@ class AuthCubit extends Cubit { AuthCubit( this.getInitialUserUseCase, this.googleSignIn, + this.loginUsecase, + this.fetchSmsCodeUseCase, this.appleSignIn, this.serUserDataUseCase, this.patchGenderUseCase, @@ -21,12 +23,35 @@ class AuthCubit extends Cubit { final GetInitialUserUseCase getInitialUserUseCase; final GoogleSignInUseCase googleSignIn; + final EmailLoginUseCase loginUsecase; + final FetchSmsCodeUseCase fetchSmsCodeUseCase; final AppleSignInUseCase appleSignIn; final SerUserDataUseCase serUserDataUseCase; final PatchGenderUseCase patchGenderUseCase; final PatchLocaleCodeUseCase patchLocaleCodeUseCase; final LogoutUseCase logoutUseCase; + 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( state.currentLocale.languageCode, diff --git a/app/lib/app/presentation/view/app_view.dart b/app/lib/app/presentation/view/app_view.dart index b89ba96c..bf62d722 100644 --- a/app/lib/app/presentation/view/app_view.dart +++ b/app/lib/app/presentation/view/app_view.dart @@ -63,6 +63,8 @@ class MyApp extends StatelessWidget { create: (context) => AuthCubit( GetInitialUserUseCase(context.read()), GoogleSignInUseCase(context.read()), + EmailLoginUseCase(context.read()), + FetchSmsCodeUseCase(context.read()), AppleSignInUseCase(context.read()), SerUserDataUseCase(context.read()), PatchGenderUseCase(context.read()), 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/app_config.dart b/app/lib/config/app_config.dart index 3b335e3a..d0661ddc 100644 --- a/app/lib/config/app_config.dart +++ b/app/lib/config/app_config.dart @@ -7,7 +7,7 @@ final class AppConfig { const AppConfig({ this.storage, this.isIntegrationTest = false, - this.isMockData = false, + this.isMockData = true, }); final bool isIntegrationTest; 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..4ba57e64 --- /dev/null +++ b/app/lib/modules/login/presentation/view/verification_code_view.dart @@ -0,0 +1,90 @@ +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'), + ), + 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..1cb504e5 100644 --- a/app/test/helpers/pump_app.dart +++ b/app/test/helpers/pump_app.dart @@ -15,6 +15,8 @@ extension PumpApp on WidgetTester { GetInitialUserUseCase getInitialUserUseCase, GetAppVersionUseCase getAppVersionUseCase, GoogleSignInUseCase googleSignInUseCase, + EmailLoginUseCase emailSignIn, + FetchSmsCodeUseCase fetchSmsCode, AppleSignInUseCase appleSignInUseCase, SerUserDataUseCase setUserDataUseCase, HomeRepository homeRepo, @@ -44,6 +46,8 @@ extension PumpApp on WidgetTester { create: (context) => AuthCubit( getInitialUserUseCase, googleSignInUseCase, + emailSignIn, + fetchSmsCode, appleSignInUseCase, setUserDataUseCase, patchGenderUseCase, diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 97ab9912..f7ae0a65 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); @@ -56,6 +58,8 @@ void main() { getInitialUserUseCase, getAppVersionUseCase, googleSignInUseCase, + emailSignIn, + fetchSmsCode, appleSignInUseCase, setUserDataUseCase, homeRepo,