From e4dd3d99af3eb8bc1bb5d56cfb39c82604e83683 Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:52:33 +0300 Subject: [PATCH 1/6] feat(auth): add recaptcha again. --- .../auth/view/screens/sign_up_screen.dart | 40 ++++++- .../auth/view/widget/confirmation_dialog.dart | 108 ++++++++++-------- 2 files changed, 94 insertions(+), 54 deletions(-) diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index f00aa7eb..e0de9a48 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -60,7 +60,6 @@ class _SignUpScreenState extends ConsumerState { String? captchaToken; late WebViewControllerPlus _controllerPlus; - final double _height = 70; @override void initState() { @@ -118,6 +117,24 @@ class _SignUpScreenState extends ConsumerState { }); } + Widget reCaptcha() { + return Transform.scale( + scale: 1.1, + child: Padding( + padding: const EdgeInsets.only( + left: 70, + ), + child: SizedBox( + height: 200, + width: 300, + child: WebViewWidget( + controller: _controllerPlus, + ), + ), + ), + ); + } + void signUp() async { // todo check which field make the error if found to tell the user. if (captchaToken == null || captchaToken!.isEmpty) { @@ -134,7 +151,9 @@ class _SignUpScreenState extends ConsumerState { reCaptchaResponse: captchaToken!, ); - context.pop(); // to close the dialog + if (mounted) { + context.pop(); // to close the dialog + } if (signUpState.type == AuthStateType.success) { ref .read(signUpEmailProvider.notifier) @@ -143,7 +162,9 @@ class _SignUpScreenState extends ConsumerState { .read(authViewModelProvider.notifier) .sendConfirmationCode(email: emailController.text); if (sendCodeState.type == AuthStateType.success) { - context.push(Routes.verification); + if (mounted) { + context.push(Routes.verification); + } } } else { //todo show error message to the user eg. email already exists / email not valid @@ -174,8 +195,17 @@ class _SignUpScreenState extends ConsumerState { vibrate(); } else { // todo show the dialog (marwan) - // showConfirmationDialog( - // context, emailController, _controllerPlus, signUp, onEdit); + showConfirmationDialog( + context: context, + title: 'is this the correct email?', + subtitle: emailController.text, + confirmText: 'Yes', + cancelText: 'Edit', + actionsAlignment: MainAxisAlignment.spaceBetween, + onConfirm: signUp, + onCancel: onEdit, + trailing: reCaptcha(), + ); } } diff --git a/lib/features/auth/view/widget/confirmation_dialog.dart b/lib/features/auth/view/widget/confirmation_dialog.dart index 5d1062ad..73b32a82 100644 --- a/lib/features/auth/view/widget/confirmation_dialog.dart +++ b/lib/features/auth/view/widget/confirmation_dialog.dart @@ -26,6 +26,7 @@ void showConfirmationDialog({ required VoidCallback onConfirm, required VoidCallback onCancel, MainAxisAlignment? actionsAlignment, + Widget? trailing, }) { showDialog( context: context, @@ -33,61 +34,70 @@ void showConfirmationDialog({ // Allow dismissing the dialog by tapping outside builder: (BuildContext context) { return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0), // Blur effect - child: AlertDialog( - backgroundColor: Palette.trinary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - actionsAlignment: MainAxisAlignment.spaceBetween, - contentPadding: const EdgeInsets.only(top: 16), - content: IntrinsicHeight( - // width: 1000, - // height: 80, + filter: ImageFilter.blur(sigmaX: 17.0, sigmaY: 17.0), + child: Center( + child: SingleChildScrollView( child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - TitleElement( - name: title, - color: titleColor ?? Palette.accentText, - fontSize: titleFontSize ?? Sizes.secondaryText, - fontWeight: titleFontWeight, - textAlign: TextAlign.left, - ), - TitleElement( - name: subtitle, - color: subtitleColor ?? Palette.primaryText, - fontSize: subtitleFontSize ?? 16, - fontWeight: subtitleFontWeight ?? FontWeight.bold, - textAlign: TextAlign.left, + AlertDialog( + backgroundColor: Palette.trinary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + actionsAlignment: MainAxisAlignment.spaceBetween, + contentPadding: const EdgeInsets.only(top: 16), + content: IntrinsicHeight( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TitleElement( + name: title, + color: titleColor ?? Palette.accentText, + fontSize: titleFontSize ?? Sizes.secondaryText, + fontWeight: titleFontWeight, + textAlign: TextAlign.left, + ), + TitleElement( + name: subtitle, + color: subtitleColor ?? Palette.primaryText, + fontSize: subtitleFontSize ?? 16, + fontWeight: subtitleFontWeight ?? FontWeight.bold, + textAlign: TextAlign.left, + ), + SizedBox(height: contentGap ?? 10), + ], + ), + ), + actions: [ + Row( + mainAxisAlignment: + actionsAlignment ?? MainAxisAlignment.spaceBetween, + children: [ + AuthSubTextButton( + onPressed: onCancel, + fontSize: Sizes.secondaryText, + label: cancelText, + color: cancelColor, + padding: cancelPadding ?? const EdgeInsets.all(0), + ), + AuthSubTextButton( + onPressed: onConfirm, + fontSize: Sizes.secondaryText, + label: confirmText, + color: confirmColor, + padding: confirmPadding ?? const EdgeInsets.all(0), + ), + ], + ) + ], ), - SizedBox(height: contentGap ?? 10), + trailing ?? Container(), ], ), ), - actions: [ - Row( - mainAxisAlignment: - actionsAlignment ?? MainAxisAlignment.spaceBetween, - children: [ - AuthSubTextButton( - onPressed: onCancel, - fontSize: Sizes.secondaryText, - label: cancelText, - color: cancelColor, - padding: cancelPadding ?? const EdgeInsets.all(0), - ), - AuthSubTextButton( - onPressed: onConfirm, - fontSize: Sizes.secondaryText, - label: confirmText, - color: confirmColor, - padding: confirmPadding ?? const EdgeInsets.all(0), - ), - ], - ) - ], ), ); }, From f7adc43bc0a5f3b4311dc46c968021c75ee40d36 Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:11:50 +0300 Subject: [PATCH 2/6] feat(auth): add signup keys. --- lib/core/constants/constant.dart | 3 +- .../auth/view/screens/sign_up_screen.dart | 36 +++++++++++++----- .../view/screens/verification_screen.dart | 37 ++++++++++++++----- .../auth/view/widget/confirmation_dialog.dart | 4 ++ 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/lib/core/constants/constant.dart b/lib/core/constants/constant.dart index 47d2cb2f..3f6f533b 100644 --- a/lib/core/constants/constant.dart +++ b/lib/core/constants/constant.dart @@ -1 +1,2 @@ -const VERIFICATION_LENGTH = 4; +const VERIFICATION_LENGTH = 6; +const VERIFICATION_CODE_EXPIRATION_TIME = 60 * 10; // in seconds diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index e0de9a48..137274fe 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -33,7 +33,21 @@ class SignUpScreen extends ConsumerStatefulWidget { } class _SignUpScreenState extends ConsumerState { - final formKey = GlobalKey(); + final formKey = GlobalKey(debugLabel: 'signup_form'); + final emailKey = GlobalKey(debugLabel: 'signup_email_input'); + final phoneKey = GlobalKey(debugLabel: 'signup_phone_input'); + final passwordKey = + GlobalKey(debugLabel: 'signup_password_input'); + final confirmPasswordKey = + GlobalKey(debugLabel: 'signup_confirm_password_input'); + final alreadyHaveAccountKey = + GlobalKey(debugLabel: 'signup_already_have_account_button'); + final signUpSubmitKey = GlobalKey(debugLabel: 'signup_submit_button'); + final onConfirmationKey = + GlobalKey(debugLabel: 'signup_on_confirmation_button'); + final onCancellationKey = + GlobalKey(debugLabel: 'signup_on_cancellation_button'); + final FocusNode emailFocusNode = FocusNode(); final FocusNode phoneFocusNode = FocusNode(); final FocusNode passwordFocusNode = FocusNode(); @@ -154,17 +168,12 @@ class _SignUpScreenState extends ConsumerState { if (mounted) { context.pop(); // to close the dialog } - if (signUpState.type == AuthStateType.success) { + if (signUpState.type == AuthStateType.unauthenticated) { ref .read(signUpEmailProvider.notifier) .update((_) => emailController.text); - AuthState sendCodeState = await ref - .read(authViewModelProvider.notifier) - .sendConfirmationCode(email: emailController.text); - if (sendCodeState.type == AuthStateType.success) { - if (mounted) { - context.push(Routes.verification); - } + if (mounted) { + context.push(Routes.verification); } } else { //todo show error message to the user eg. email already exists / email not valid @@ -194,7 +203,7 @@ class _SignUpScreenState extends ConsumerState { if (someNotFilled) { vibrate(); } else { - // todo show the dialog (marwan) + // todo make recaptcha better showConfirmationDialog( context: context, title: 'is this the correct email?', @@ -205,6 +214,8 @@ class _SignUpScreenState extends ConsumerState { onConfirm: signUp, onCancel: onEdit, trailing: reCaptcha(), + onCancelButtonKey: onCancellationKey, + onConfirmButtonKey: onConfirmationKey, ); } } @@ -246,6 +257,7 @@ class _SignUpScreenState extends ConsumerState { width: 250.0), ShakeMyAuthInput( name: 'Email', + formKey: emailKey, shakeKey: emailShakeKey, isFocused: isEmailFocused, focusNode: emailFocusNode, @@ -254,6 +266,7 @@ class _SignUpScreenState extends ConsumerState { ), AuthPhoneNumber( name: 'Phone Number', + formKey: phoneKey, shakeKey: phoneShakeKey, isFocused: isPhoneFocused, focusNode: phoneFocusNode, @@ -261,6 +274,7 @@ class _SignUpScreenState extends ConsumerState { ), ShakeMyAuthInput( name: 'Password', + formKey: passwordKey, shakeKey: passwordShakeKey, isFocused: isPasswordFocused, focusNode: passwordFocusNode, @@ -270,6 +284,7 @@ class _SignUpScreenState extends ConsumerState { ), ShakeMyAuthInput( name: 'Confirm Password', + formKey: confirmPasswordKey, shakeKey: confirmPasswordShakeKey, isFocused: isConfirmPasswordFocused, focusNode: confirmPasswordFocusNode, @@ -285,6 +300,7 @@ class _SignUpScreenState extends ConsumerState { color: Palette.primaryText, fontSize: Sizes.infoText), AuthSubTextButton( + buttonKey: alreadyHaveAccountKey, onPressed: () { context.pop(); }, diff --git a/lib/features/auth/view/screens/verification_screen.dart b/lib/features/auth/view/screens/verification_screen.dart index e8397136..6f9f966d 100644 --- a/lib/features/auth/view/screens/verification_screen.dart +++ b/lib/features/auth/view/screens/verification_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter_shakemywidget/flutter_shakemywidget.dart'; +import 'package:go_router/go_router.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:flutter/material.dart'; import 'package:telware_cross_platform/core/view/widget/responsive.dart'; @@ -14,6 +15,7 @@ import 'package:flutter_verification_code/flutter_verification_code.dart'; import 'package:telware_cross_platform/core/constants/constant.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_state.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; +import 'package:telware_cross_platform/core/routes/routes.dart'; class VerificationScreen extends ConsumerStatefulWidget { static const String route = '/verify'; @@ -25,12 +27,18 @@ class VerificationScreen extends ConsumerStatefulWidget { } class _VerificationScreen extends ConsumerState { + final verificationCodeKey = + GlobalKey(debugLabel: 'verificationCode_input'); + final resendCodeKey = + GlobalKey(debugLabel: 'verification_resendCode_button'); + final submitKey = GlobalKey(debugLabel: 'verification_submit_button'); + final shakeKey = GlobalKey(); + String _code = ''; - bool _onEditing = true; bool codeNotMatched = false; - int remainingTime = 60; // Total seconds for countdown + int remainingTime = + VERIFICATION_CODE_EXPIRATION_TIME; // Total seconds for countdown Timer? _timer; - final shakeKey = GlobalKey(); late String email; final TextStyle digitStyle = const TextStyle( @@ -42,7 +50,8 @@ class _VerificationScreen extends ConsumerState { @override void initState() { super.initState(); - email = ref.read(signUpEmailProvider); + // email = ref.read(signUpEmailProvider); + email = 'hmmm@gmail.com'; startTimer(); // Start the countdown timer } @@ -56,9 +65,10 @@ class _VerificationScreen extends ConsumerState { AuthState sendCodeState = await ref .read(authViewModelProvider.notifier) .sendConfirmationCode(email: email); + debugPrint('sendCodeState: ${sendCodeState.type}'); if (sendCodeState.type == AuthStateType.success) { setState(() { - remainingTime = 60; + remainingTime = VERIFICATION_CODE_EXPIRATION_TIME; }); startTimer(); } @@ -90,15 +100,19 @@ class _VerificationScreen extends ConsumerState { void onEditing(bool value) { setState(() { - _onEditing = value; - codeNotMatched = false; + codeNotMatched = + false; // waiting for the user to enter the code to show an error message if the code is not correct }); if (value) { setState(() { + // if the user completed the code then edited it + // again but did not complete it, then submit the code will contains the old one, + // so we have to set it to empty to avoid this issue _code = ''; }); + } else { + FocusScope.of(context).unfocus(); } - if (!value) FocusScope.of(context).unfocus(); } void onSubmit() async { @@ -110,10 +124,12 @@ class _VerificationScreen extends ConsumerState { ); if (state.type == AuthStateType.authenticated) { // todo: Navigate to the home screen + if (mounted) { + context.push(Routes.home); + } } else { setState(() { codeNotMatched = true; - remainingTime = 0; }); } } else { @@ -179,6 +195,7 @@ class _VerificationScreen extends ConsumerState { shakeOffset: 10, shakeDuration: const Duration(milliseconds: 500), child: VerificationCode( + key: verificationCodeKey, textStyle: digitStyle, keyboardType: TextInputType.number, underlineColor: Palette.accentText, @@ -194,6 +211,7 @@ class _VerificationScreen extends ConsumerState { ), remainingTime == 0 ? AuthSubTextButton( + buttonKey: resendCodeKey, onPressed: resendCode, label: 'Resend code', padding: const EdgeInsets.only(top: 20, right: 5), @@ -223,6 +241,7 @@ class _VerificationScreen extends ConsumerState { ), ), floatingActionButton: AuthFloatingActionButton( + buttonKey: submitKey, onSubmit: onSubmit, ), ); diff --git a/lib/features/auth/view/widget/confirmation_dialog.dart b/lib/features/auth/view/widget/confirmation_dialog.dart index 73b32a82..2a789dc7 100644 --- a/lib/features/auth/view/widget/confirmation_dialog.dart +++ b/lib/features/auth/view/widget/confirmation_dialog.dart @@ -25,6 +25,8 @@ void showConfirmationDialog({ Color? cancelColor, required VoidCallback onConfirm, required VoidCallback onCancel, + GlobalKey? onConfirmButtonKey, + GlobalKey? onCancelButtonKey, MainAxisAlignment? actionsAlignment, Widget? trailing, }) { @@ -77,6 +79,7 @@ void showConfirmationDialog({ actionsAlignment ?? MainAxisAlignment.spaceBetween, children: [ AuthSubTextButton( + buttonKey: onCancelButtonKey, onPressed: onCancel, fontSize: Sizes.secondaryText, label: cancelText, @@ -84,6 +87,7 @@ void showConfirmationDialog({ padding: cancelPadding ?? const EdgeInsets.all(0), ), AuthSubTextButton( + buttonKey: onConfirmButtonKey, onPressed: onConfirm, fontSize: Sizes.secondaryText, label: confirmText, From f80575febbd4f3c536c7fff45025717884d36225 Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:12:31 +0300 Subject: [PATCH 3/6] feat(auth): add signup and verification keys. --- lib/features/auth/view/widget/auth_phone_number.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/auth/view/widget/auth_phone_number.dart b/lib/features/auth/view/widget/auth_phone_number.dart index ddbad50d..ee774f0d 100644 --- a/lib/features/auth/view/widget/auth_phone_number.dart +++ b/lib/features/auth/view/widget/auth_phone_number.dart @@ -11,6 +11,7 @@ class AuthPhoneNumber extends StatelessWidget { required this.shakeKey, required this.focusNode, required this.isFocused, + this.formKey, this.padding = const EdgeInsets.only( bottom: Dimensions.inputPaddingBottom, left: Dimensions.inputPaddingLeft, @@ -20,6 +21,7 @@ class AuthPhoneNumber extends StatelessWidget { }); final GlobalKey shakeKey; + final GlobalKey? formKey; final String name; final EdgeInsetsGeometry padding; final FocusNode focusNode; @@ -36,6 +38,7 @@ class AuthPhoneNumber extends StatelessWidget { child: Padding( padding: padding, child: PhoneFormField( + key: formKey, focusNode: focusNode, decoration: InputDecoration( hintText: isFocused ? '' : name, From 02139271fb60b92c84b6217981b78173ded8d9bb Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:17:26 +0300 Subject: [PATCH 4/6] feat(auth): modify signup body. --- .../auth/repository/auth_remote_repository.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/features/auth/repository/auth_remote_repository.dart b/lib/features/auth/repository/auth_remote_repository.dart index c851f7aa..391c236d 100644 --- a/lib/features/auth/repository/auth_remote_repository.dart +++ b/lib/features/auth/repository/auth_remote_repository.dart @@ -32,13 +32,15 @@ class AuthRemoteRepository { required String reCaptchaResponse, }) async { try { - final response = await _dio.post('/auth/sign-up', data: { + final response = await _dio.post('/auth/signup', data: { 'email': email, - 'phone': phone, + 'phoneNumber': phone, 'password': password, - 'confirmPassword': confirmPassword, + 'passwordConfirm': confirmPassword, 'reCaptchaResponse': reCaptchaResponse, }); + debugPrint( + '---------------------------Sign Up response: ${response.data}'); if (response.statusCode! >= 400) { final String message = response.data?['message'] ?? 'Unexpected Error'; @@ -60,6 +62,9 @@ class AuthRemoteRepository { final response = await _dio.post('/auth/verify', data: {'email': email, 'verificationCode': code}); + debugPrint( + '---------------------------Verification response: ${response.data}'); + if (response.statusCode! >= 400) { final String message = response.data?['message'] ?? 'Unexpected Error'; return Left(AppError(message)); @@ -202,7 +207,8 @@ class AuthRemoteRepository { AppError handleDioException(DioException dioException) { String? message; if (dioException.response != null) { - message = (dioException.response!.data)['data']['message']; + message = + dioException.response!.data?['message'] ?? 'Unexpected dio Error'; debugPrint(message); } else if (dioException.type == DioExceptionType.connectionTimeout || dioException.type == DioExceptionType.connectionError || @@ -212,7 +218,7 @@ class AuthRemoteRepository { } else { message = 'Something wrong happened. Please, try again later.'; debugPrint(message); - debugPrint('here Unhandled Dio Exception'); + debugPrint('Unhandled Dio Exception'); } return AppError(message ?? 'Unexpected server error.'); } From 3fe09e8dff477840b173af95ee4a2b3cc0a5ad21 Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Fri, 1 Nov 2024 01:03:14 +0200 Subject: [PATCH 5/6] feat(auth): integration with BE. --- lib/core/models/app_error.dart | 11 ++- lib/core/models/signup_result.dart | 9 +++ lib/core/routes/routes.dart | 10 ++- .../auth/models/auth_response_model.dart | 2 +- .../repository/auth_remote_repository.dart | 40 ++++++---- .../auth/view/screens/sign_up_screen.dart | 80 ++++++++++++------- .../view/screens/verification_screen.dart | 6 +- .../auth/view/widget/auth_input_field.dart | 3 + .../auth/view/widget/auth_phone_number.dart | 3 + .../auth/view/widget/shake_my_auth_input.dart | 3 + .../auth/view_model/auth_view_model.dart | 32 +++----- 11 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 lib/core/models/signup_result.dart diff --git a/lib/core/models/app_error.dart b/lib/core/models/app_error.dart index 92ae8310..93440b02 100644 --- a/lib/core/models/app_error.dart +++ b/lib/core/models/app_error.dart @@ -1,6 +1,15 @@ class AppError { final String error; + final String? emailError; + final String? phoneNumberError; + final String? passwordError; + final String? confirmPasswordError; final int? code; - AppError(this.error, {this.code}); + AppError(this.error, + {this.code, + this.emailError, + this.phoneNumberError, + this.passwordError, + this.confirmPasswordError}); } diff --git a/lib/core/models/signup_result.dart b/lib/core/models/signup_result.dart new file mode 100644 index 00000000..168d1688 --- /dev/null +++ b/lib/core/models/signup_result.dart @@ -0,0 +1,9 @@ +import 'package:telware_cross_platform/core/models/app_error.dart'; +import 'package:telware_cross_platform/features/auth/view_model/auth_state.dart'; + +class SignupResult { + final AuthState state; + final AppError? error; + + SignupResult({required this.state, this.error}); +} diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index f29322e3..a67403c4 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -44,11 +44,11 @@ class Routes { static const String userProfile = UserProfileScreen.route; static const String privacySettings = PrivacySettingsScreen.route; - static GoRouter appRouter(WidgetRef ref) => GoRouter( initialLocation: Routes.splash, redirect: (context, state) { - final isAuthenticated = ref.read(authViewModelProvider.notifier).isAuthenticated(); + final isAuthenticated = + ref.read(authViewModelProvider.notifier).isAuthenticated(); if (!isAuthenticated) { if (state.fullPath != Routes.logIn && state.fullPath != Routes.signUp && @@ -124,8 +124,10 @@ class Routes { pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: StoryScreen( - userId: (state.extra as Map)['userId'] as String, - showSeens: (state.extra as Map)['showSeens'] as bool, + userId: + (state.extra as Map)['userId'] as String, + showSeens: + (state.extra as Map)['showSeens'] as bool, ), transitionsBuilder: _slideRightTransitionBuilder, ), diff --git a/lib/features/auth/models/auth_response_model.dart b/lib/features/auth/models/auth_response_model.dart index 1c967f44..3b6c490d 100644 --- a/lib/features/auth/models/auth_response_model.dart +++ b/lib/features/auth/models/auth_response_model.dart @@ -12,7 +12,7 @@ class AuthResponseModel { factory AuthResponseModel.fromMap(Map map) { return AuthResponseModel( user: UserModel.fromMap(map['user'] as Map), - token: map['accessToken'] as String, + token: map['sessionId'] as String, ); } diff --git a/lib/features/auth/repository/auth_remote_repository.dart b/lib/features/auth/repository/auth_remote_repository.dart index 391c236d..356dd305 100644 --- a/lib/features/auth/repository/auth_remote_repository.dart +++ b/lib/features/auth/repository/auth_remote_repository.dart @@ -44,7 +44,16 @@ class AuthRemoteRepository { if (response.statusCode! >= 400) { final String message = response.data?['message'] ?? 'Unexpected Error'; - return AppError(message); + return AppError( + message, + emailError: response.data?['error']?['errors']?['email']?['message'], + phoneNumberError: response.data?['error']?['errors']?['phoneNumber'] + ?['message'], + passwordError: response.data?['error']?['errors']?['password'] + ?['message'], + confirmPasswordError: response.data?['error']?['errors'] + ?['passwordConfirm']?['message'], + ); } } on DioException catch (dioException) { return handleDioException(dioException); @@ -56,30 +65,23 @@ class AuthRemoteRepository { } // todo: make sure of what does get from the back-end - Future> verifyEmail( + Future verifyEmail( {required String email, required String code}) async { try { final response = await _dio.post('/auth/verify', data: {'email': email, 'verificationCode': code}); - debugPrint( - '---------------------------Verification response: ${response.data}'); - if (response.statusCode! >= 400) { final String message = response.data?['message'] ?? 'Unexpected Error'; - return Left(AppError(message)); + return AppError(message); } - final AuthResponseModel verificationResponse = AuthResponseModel.fromMap( - (response.data['data']) as Map); - - return Right(verificationResponse); } on DioException catch (dioException) { - return Left(handleDioException(dioException)); + return handleDioException(dioException); } catch (error) { debugPrint('Verify Email error:\n${error.toString()}'); - return Left( - AppError("Couldn't verify email now. Please, try again later.")); + return AppError("Couldn't verify email now. Please, try again later."); } + return null; } Future sendConfirmationCode({required String email}) async { @@ -220,6 +222,16 @@ class AuthRemoteRepository { debugPrint(message); debugPrint('Unhandled Dio Exception'); } - return AppError(message ?? 'Unexpected server error.'); + return AppError( + message ?? 'Unexpected server error.', + emailError: dioException.response?.data?['error']?['errors']?['email'] + ?['message'], + phoneNumberError: dioException.response?.data?['error']?['errors'] + ?['phoneNumber']?['message'], + passwordError: dioException.response?.data?['error']?['errors'] + ?['password']?['message'], + confirmPasswordError: dioException.response?.data?['error']?['errors'] + ?['passwordConfirm']?['message'], + ); } } diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index 137274fe..909bc6d4 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_shakemywidget/flutter_shakemywidget.dart'; import 'package:go_router/go_router.dart'; import 'package:phone_form_field/phone_form_field.dart'; +import 'package:telware_cross_platform/core/models/signup_result.dart'; import 'package:vibration/vibration.dart'; import 'package:webview_flutter_plus/webview_flutter_plus.dart'; @@ -33,21 +34,28 @@ class SignUpScreen extends ConsumerStatefulWidget { } class _SignUpScreenState extends ConsumerState { + //-------------------------------------- Keys ------------------------- final formKey = GlobalKey(debugLabel: 'signup_form'); final emailKey = GlobalKey(debugLabel: 'signup_email_input'); final phoneKey = GlobalKey(debugLabel: 'signup_phone_input'); final passwordKey = - GlobalKey(debugLabel: 'signup_password_input'); + GlobalKey(debugLabel: 'signup_password_input'); final confirmPasswordKey = - GlobalKey(debugLabel: 'signup_confirm_password_input'); + GlobalKey(debugLabel: 'signup_confirm_password_input'); final alreadyHaveAccountKey = - GlobalKey(debugLabel: 'signup_already_have_account_button'); + GlobalKey(debugLabel: 'signup_already_have_account_button'); final signUpSubmitKey = GlobalKey(debugLabel: 'signup_submit_button'); final onConfirmationKey = - GlobalKey(debugLabel: 'signup_on_confirmation_button'); + GlobalKey(debugLabel: 'signup_on_confirmation_button'); final onCancellationKey = - GlobalKey(debugLabel: 'signup_on_cancellation_button'); + GlobalKey(debugLabel: 'signup_on_cancellation_button'); + final emailShakeKey = GlobalKey(); + final phoneShakeKey = GlobalKey(); + final passwordShakeKey = GlobalKey(); + final confirmPasswordShakeKey = GlobalKey(); + + //-------------------------------------- Focus nodes ------------------------- final FocusNode emailFocusNode = FocusNode(); final FocusNode phoneFocusNode = FocusNode(); final FocusNode passwordFocusNode = FocusNode(); @@ -57,24 +65,29 @@ class _SignUpScreenState extends ConsumerState { bool isPasswordFocused = false; bool isConfirmPasswordFocused = false; + //-------------------------------------- Controllers ------------------------- + final TextEditingController emailController = TextEditingController(); final PhoneController phoneController = PhoneController( initialValue: const PhoneNumber(isoCode: IsoCode.EG, nsn: '')); final TextEditingController passwordController = TextEditingController(); final TextEditingController confirmPasswordController = - TextEditingController(); + TextEditingController(); + late WebViewControllerPlus _controllerPlus; + RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller(); - final emailShakeKey = GlobalKey(); - final phoneShakeKey = GlobalKey(); - final passwordShakeKey = GlobalKey(); - final confirmPasswordShakeKey = GlobalKey(); + //-------------------------------------- Errors ------------------------- + String? emailError; + String? phoneError; + + String? passwordError; + + String? confirmPasswordError; final String siteKey = dotenv.env['RECAPTCHA_SITE_KEY'] ?? ''; String? captchaToken; - late WebViewControllerPlus _controllerPlus; - @override void initState() { super.initState(); @@ -101,9 +114,9 @@ class _SignUpScreenState extends ConsumerState { _controllerPlus = WebViewControllerPlus() ..addJavaScriptChannel('onCaptchaCompleted', onMessageReceived: (JavaScriptMessage message) { - captchaToken = message.message; - debugPrint('Captcha token: $captchaToken'); - }) + captchaToken = message.message; + debugPrint('Captcha token: $captchaToken'); + }) ..loadFlutterAssetServer('assets/webpages/captcha.html') ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)); @@ -150,25 +163,24 @@ class _SignUpScreenState extends ConsumerState { } void signUp() async { - // todo check which field make the error if found to tell the user. if (captchaToken == null || captchaToken!.isEmpty) { vibrate(); return; } // phoneController.value.international gives the phone number in international format eg. +20123456789 - AuthState signUpState = - await ref.read(authViewModelProvider.notifier).signUp( - email: emailController.text, - phone: phoneController.value.international, - password: passwordController.text, - confirmPassword: confirmPasswordController.text, - reCaptchaResponse: captchaToken!, - ); + SignupResult signUpResult = + await ref.read(authViewModelProvider.notifier).signUp( + email: emailController.text, + phone: phoneController.value.international, + password: passwordController.text, + confirmPassword: confirmPasswordController.text, + reCaptchaResponse: captchaToken!, + ); if (mounted) { context.pop(); // to close the dialog } - if (signUpState.type == AuthStateType.unauthenticated) { + if (signUpResult.state.type == AuthStateType.unauthenticated) { ref .read(signUpEmailProvider.notifier) .update((_) => emailController.text); @@ -177,6 +189,16 @@ class _SignUpScreenState extends ConsumerState { } } else { //todo show error message to the user eg. email already exists / email not valid + setState(() { + emailError = signUpResult.error?.emailError; + phoneError = signUpResult.error?.phoneNumberError; + passwordError = signUpResult.error?.passwordError; + confirmPasswordError = signUpResult.error?.confirmPasswordError; + debugPrint('emailError: $emailError'); + debugPrint('phoneError: $phoneError'); + debugPrint('passwordError: $passwordError'); + debugPrint('confirmPasswordError: $confirmPasswordError'); + }); } } @@ -250,13 +272,14 @@ class _SignUpScreenState extends ConsumerState { ), const TitleElement( name: - 'Please confirm your email address and enter your password.', + 'Please confirm your email address and enter your password.', color: Palette.accentText, fontSize: Sizes.secondaryText, padding: EdgeInsets.only(bottom: 30), width: 250.0), ShakeMyAuthInput( name: 'Email', + errorText: emailError, formKey: emailKey, shakeKey: emailShakeKey, isFocused: isEmailFocused, @@ -266,6 +289,7 @@ class _SignUpScreenState extends ConsumerState { ), AuthPhoneNumber( name: 'Phone Number', + errorText: phoneError, formKey: phoneKey, shakeKey: phoneShakeKey, isFocused: isPhoneFocused, @@ -274,6 +298,7 @@ class _SignUpScreenState extends ConsumerState { ), ShakeMyAuthInput( name: 'Password', + errorText: passwordError, formKey: passwordKey, shakeKey: passwordShakeKey, isFocused: isPasswordFocused, @@ -284,6 +309,7 @@ class _SignUpScreenState extends ConsumerState { ), ShakeMyAuthInput( name: 'Confirm Password', + errorText: confirmPasswordError, formKey: confirmPasswordKey, shakeKey: confirmPasswordShakeKey, isFocused: isConfirmPasswordFocused, @@ -296,7 +322,7 @@ class _SignUpScreenState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, children: [ const TitleElement( - name: 'Already have an account? ', + name: 'Already have an account?', color: Palette.primaryText, fontSize: Sizes.infoText), AuthSubTextButton( diff --git a/lib/features/auth/view/screens/verification_screen.dart b/lib/features/auth/view/screens/verification_screen.dart index 6f9f966d..b8bf87de 100644 --- a/lib/features/auth/view/screens/verification_screen.dart +++ b/lib/features/auth/view/screens/verification_screen.dart @@ -50,8 +50,7 @@ class _VerificationScreen extends ConsumerState { @override void initState() { super.initState(); - // email = ref.read(signUpEmailProvider); - email = 'hmmm@gmail.com'; + email = ref.read(signUpEmailProvider); startTimer(); // Start the countdown timer } @@ -65,7 +64,6 @@ class _VerificationScreen extends ConsumerState { AuthState sendCodeState = await ref .read(authViewModelProvider.notifier) .sendConfirmationCode(email: email); - debugPrint('sendCodeState: ${sendCodeState.type}'); if (sendCodeState.type == AuthStateType.success) { setState(() { remainingTime = VERIFICATION_CODE_EXPIRATION_TIME; @@ -125,7 +123,7 @@ class _VerificationScreen extends ConsumerState { if (state.type == AuthStateType.authenticated) { // todo: Navigate to the home screen if (mounted) { - context.push(Routes.home); + context.push(Routes.logIn); } } else { setState(() { diff --git a/lib/features/auth/view/widget/auth_input_field.dart b/lib/features/auth/view/widget/auth_input_field.dart index 4ab995ae..4e561530 100644 --- a/lib/features/auth/view/widget/auth_input_field.dart +++ b/lib/features/auth/view/widget/auth_input_field.dart @@ -9,6 +9,7 @@ class AuthInputField extends StatefulWidget { final bool obscure; final FocusNode focusNode; final FormFieldValidator? validator; + final String? errorText; final TextEditingController? controller; final Key? visibilityKey; @@ -21,6 +22,7 @@ class AuthInputField extends StatefulWidget { required this.focusNode, this.obscure = false, this.validator, + this.errorText, this.controller, this.visibilityKey, }); @@ -73,6 +75,7 @@ class AuthInputFieldState extends State { decoration: InputDecoration( hintText: widget.isFocused ? '' : widget.name, labelText: !widget.isFocused ? '' : widget.name, + errorText: widget.errorText, hintStyle: const TextStyle( color: Palette.accentText, fontWeight: FontWeight.normal), labelStyle: const TextStyle( diff --git a/lib/features/auth/view/widget/auth_phone_number.dart b/lib/features/auth/view/widget/auth_phone_number.dart index ee774f0d..cf055779 100644 --- a/lib/features/auth/view/widget/auth_phone_number.dart +++ b/lib/features/auth/view/widget/auth_phone_number.dart @@ -12,6 +12,7 @@ class AuthPhoneNumber extends StatelessWidget { required this.focusNode, required this.isFocused, this.formKey, + this.errorText, this.padding = const EdgeInsets.only( bottom: Dimensions.inputPaddingBottom, left: Dimensions.inputPaddingLeft, @@ -23,6 +24,7 @@ class AuthPhoneNumber extends StatelessWidget { final GlobalKey shakeKey; final GlobalKey? formKey; final String name; + final String? errorText; final EdgeInsetsGeometry padding; final FocusNode focusNode; final bool isFocused; @@ -43,6 +45,7 @@ class AuthPhoneNumber extends StatelessWidget { decoration: InputDecoration( hintText: isFocused ? '' : name, labelText: !(isFocused) ? '' : name, + errorText: errorText, hintStyle: const TextStyle( color: Palette.accentText, fontWeight: FontWeight.normal, diff --git a/lib/features/auth/view/widget/shake_my_auth_input.dart b/lib/features/auth/view/widget/shake_my_auth_input.dart index 7361861c..ef55a222 100644 --- a/lib/features/auth/view/widget/shake_my_auth_input.dart +++ b/lib/features/auth/view/widget/shake_my_auth_input.dart @@ -7,6 +7,7 @@ class ShakeMyAuthInput extends StatelessWidget { const ShakeMyAuthInput({ super.key, required this.name, + this.errorText, this.formKey, required this.shakeKey, required this.isFocused, @@ -25,6 +26,7 @@ class ShakeMyAuthInput extends StatelessWidget { final GlobalKey shakeKey; final GlobalKey? formKey; final String name; + final String? errorText; final EdgeInsetsGeometry padding; final bool obscure; final bool isFocused; @@ -43,6 +45,7 @@ class ShakeMyAuthInput extends StatelessWidget { child: AuthInputField( name: name, padding: padding, + errorText: errorText, formFieldKey: formKey, isFocused: isFocused, focusNode: focusNode, diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart index f0e6fbc0..c3f19973 100644 --- a/lib/features/auth/view_model/auth_view_model.dart +++ b/lib/features/auth/view_model/auth_view_model.dart @@ -5,6 +5,7 @@ import 'package:telware_cross_platform/core/mock/constants_mock.dart'; import 'package:telware_cross_platform/core/mock/token_mock.dart'; import 'package:telware_cross_platform/core/mock/user_mock.dart'; import 'package:flutter/material.dart'; +import 'package:telware_cross_platform/core/models/signup_result.dart'; import 'package:telware_cross_platform/core/providers/token_provider.dart'; import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/features/auth/repository/auth_local_repository.dart'; @@ -40,11 +41,10 @@ class AuthViewModel extends _$AuthViewModel { state = AuthState.authorized; return; } - + // try getting updated user data - final response = - await ref.read(authRemoteRepositoryProvider).getMe(token); - + final response = await ref.read(authRemoteRepositoryProvider).getMe(token); + response.match((appError) { state = AuthState.fail(appError.error); // getting user data from local as remote failed @@ -67,7 +67,7 @@ class AuthViewModel extends _$AuthViewModel { return token != null && token.isNotEmpty; } - Future signUp({ + Future signUp({ required String email, required String phone, required String password, @@ -79,7 +79,7 @@ class AuthViewModel extends _$AuthViewModel { // if we are using mock data, we will not send the request to the server if (USE_MOCK_DATA) { state = AuthState.unauthenticated; - return state; + return SignupResult(state: state); } final AppError? response = await ref @@ -96,7 +96,7 @@ class AuthViewModel extends _$AuthViewModel { state = AuthState .unauthenticated; // user is not authenticated yet, he needs to verify his email } - return state; + return SignupResult(state: state, error: response); } Future verifyEmail({ @@ -119,20 +119,12 @@ class AuthViewModel extends _$AuthViewModel { .read(authRemoteRepositoryProvider) .verifyEmail(email: email, code: code); - response.match((appError) { - state = AuthState.unauthenticated; - }, (verificationResponse) { - ref.read(authLocalRepositoryProvider).setUser(verificationResponse.user); - ref.read(userProvider.notifier).update((_) => verificationResponse.user); - - ref - .read(authLocalRepositoryProvider) - .setToken(verificationResponse.token); - ref - .read(tokenProvider.notifier) - .update((_) => verificationResponse.token); + if (response != null) { + state = AuthState.fail(response.error); + return state; + } else { state = AuthState.authenticated; - }); + } return state; } From fde7a29188613188ba7b95b735b3aab20db6bf3b Mon Sep 17 00:00:00 2001 From: marwan2232004 <118024824+marwan2232004@users.noreply.github.com> Date: Fri, 1 Nov 2024 01:56:29 +0200 Subject: [PATCH 6/6] fix(auth): remove unwanted code. --- .../auth/view/screens/sign_up_screen.dart | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/features/auth/view/screens/sign_up_screen.dart b/lib/features/auth/view/screens/sign_up_screen.dart index 909bc6d4..3088b99b 100644 --- a/lib/features/auth/view/screens/sign_up_screen.dart +++ b/lib/features/auth/view/screens/sign_up_screen.dart @@ -39,16 +39,16 @@ class _SignUpScreenState extends ConsumerState { final emailKey = GlobalKey(debugLabel: 'signup_email_input'); final phoneKey = GlobalKey(debugLabel: 'signup_phone_input'); final passwordKey = - GlobalKey(debugLabel: 'signup_password_input'); + GlobalKey(debugLabel: 'signup_password_input'); final confirmPasswordKey = - GlobalKey(debugLabel: 'signup_confirm_password_input'); + GlobalKey(debugLabel: 'signup_confirm_password_input'); final alreadyHaveAccountKey = - GlobalKey(debugLabel: 'signup_already_have_account_button'); + GlobalKey(debugLabel: 'signup_already_have_account_button'); final signUpSubmitKey = GlobalKey(debugLabel: 'signup_submit_button'); final onConfirmationKey = - GlobalKey(debugLabel: 'signup_on_confirmation_button'); + GlobalKey(debugLabel: 'signup_on_confirmation_button'); final onCancellationKey = - GlobalKey(debugLabel: 'signup_on_cancellation_button'); + GlobalKey(debugLabel: 'signup_on_cancellation_button'); final emailShakeKey = GlobalKey(); final phoneShakeKey = GlobalKey(); @@ -72,9 +72,8 @@ class _SignUpScreenState extends ConsumerState { initialValue: const PhoneNumber(isoCode: IsoCode.EG, nsn: '')); final TextEditingController passwordController = TextEditingController(); final TextEditingController confirmPasswordController = - TextEditingController(); + TextEditingController(); late WebViewControllerPlus _controllerPlus; - RecaptchaV2Controller recaptchaV2Controller = RecaptchaV2Controller(); //-------------------------------------- Errors ------------------------- String? emailError; @@ -114,9 +113,9 @@ class _SignUpScreenState extends ConsumerState { _controllerPlus = WebViewControllerPlus() ..addJavaScriptChannel('onCaptchaCompleted', onMessageReceived: (JavaScriptMessage message) { - captchaToken = message.message; - debugPrint('Captcha token: $captchaToken'); - }) + captchaToken = message.message; + debugPrint('Captcha token: $captchaToken'); + }) ..loadFlutterAssetServer('assets/webpages/captcha.html') ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(const Color(0x00000000)); @@ -169,13 +168,13 @@ class _SignUpScreenState extends ConsumerState { } // phoneController.value.international gives the phone number in international format eg. +20123456789 SignupResult signUpResult = - await ref.read(authViewModelProvider.notifier).signUp( - email: emailController.text, - phone: phoneController.value.international, - password: passwordController.text, - confirmPassword: confirmPasswordController.text, - reCaptchaResponse: captchaToken!, - ); + await ref.read(authViewModelProvider.notifier).signUp( + email: emailController.text, + phone: phoneController.value.international, + password: passwordController.text, + confirmPassword: confirmPasswordController.text, + reCaptchaResponse: captchaToken!, + ); if (mounted) { context.pop(); // to close the dialog @@ -272,7 +271,7 @@ class _SignUpScreenState extends ConsumerState { ), const TitleElement( name: - 'Please confirm your email address and enter your password.', + 'Please confirm your email address and enter your password.', color: Palette.accentText, fontSize: Sizes.secondaryText, padding: EdgeInsets.only(bottom: 30),