Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test/sign up #27

Merged
merged 7 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/core/constants/constant.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
const VERIFICATION_LENGTH = 4;
const VERIFICATION_LENGTH = 6;
const VERIFICATION_CODE_EXPIRATION_TIME = 60 * 10; // in seconds
11 changes: 10 additions & 1 deletion lib/core/models/app_error.dart
Original file line number Diff line number Diff line change
@@ -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});
}
9 changes: 9 additions & 0 deletions lib/core/models/signup_result.dart
Original file line number Diff line number Diff line change
@@ -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});
}
28 changes: 14 additions & 14 deletions lib/core/routes/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,20 @@ class Routes {
static const String privacySettings = PrivacySettingsScreen.route;

static GoRouter appRouter(WidgetRef ref) => GoRouter(
initialLocation: Routes.home,
// redirect: (context, state) {
// final isAuthenticated =
// ref.read(authViewModelProvider.notifier).isAuthenticated();
// if (!isAuthenticated) {
// if (state.fullPath != Routes.logIn &&
// state.fullPath != Routes.signUp &&
// state.fullPath != Routes.verification &&
// state.fullPath != Routes.splash) {
// return Routes.logIn;
// }
// }
// return null;
// },
initialLocation: Routes.splash,
redirect: (context, state) {
final isAuthenticated =
ref.read(authViewModelProvider.notifier).isAuthenticated();
if (!isAuthenticated) {
if (state.fullPath != Routes.logIn &&
state.fullPath != Routes.signUp &&
state.fullPath != Routes.verification &&
state.fullPath != Routes.splash) {
return Routes.logIn;
}
}
return null;
},
routes: [
GoRoute(
path: Routes.splash,
Expand Down
2 changes: 1 addition & 1 deletion lib/features/auth/models/auth_response_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AuthResponseModel {
factory AuthResponseModel.fromMap(Map<String, dynamic> map) {
return AuthResponseModel(
user: UserModel.fromMap(map['user'] as Map<String, dynamic>),
token: map['accessToken'] as String,
token: map['sessionId'] as String,
);
}

Expand Down
50 changes: 34 additions & 16 deletions lib/features/auth/repository/auth_remote_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,28 @@ 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';
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);
Expand All @@ -54,27 +65,23 @@ class AuthRemoteRepository {
}

// todo: make sure of what does get from the back-end
Future<Either<AppError, AuthResponseModel>> verifyEmail(
Future<AppError?> verifyEmail(
{required String email, required String code}) async {
try {
final response = await _dio.post('/auth/verify',
data: {'email': email, 'verificationCode': code});

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<String, dynamic>);

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<AppError?> sendConfirmationCode({required String email}) async {
Expand Down Expand Up @@ -202,7 +209,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 ||
Expand All @@ -212,8 +220,18 @@ 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.');
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'],
);
}
}
111 changes: 91 additions & 20 deletions lib/features/auth/view/screens/sign_up_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -33,7 +34,28 @@ class SignUpScreen extends ConsumerStatefulWidget {
}

class _SignUpScreenState extends ConsumerState<SignUpScreen> {
final formKey = GlobalKey<FormState>();
//-------------------------------------- Keys -------------------------
final formKey = GlobalKey<FormState>(debugLabel: 'signup_form');
final emailKey = GlobalKey<FormFieldState>(debugLabel: 'signup_email_input');
final phoneKey = GlobalKey<FormFieldState>(debugLabel: 'signup_phone_input');
final passwordKey =
GlobalKey<FormFieldState>(debugLabel: 'signup_password_input');
final confirmPasswordKey =
GlobalKey<FormFieldState>(debugLabel: 'signup_confirm_password_input');
final alreadyHaveAccountKey =
GlobalKey<State>(debugLabel: 'signup_already_have_account_button');
final signUpSubmitKey = GlobalKey<State>(debugLabel: 'signup_submit_button');
final onConfirmationKey =
GlobalKey<State>(debugLabel: 'signup_on_confirmation_button');
final onCancellationKey =
GlobalKey<State>(debugLabel: 'signup_on_cancellation_button');

final emailShakeKey = GlobalKey<ShakeWidgetState>();
final phoneShakeKey = GlobalKey<ShakeWidgetState>();
final passwordShakeKey = GlobalKey<ShakeWidgetState>();
final confirmPasswordShakeKey = GlobalKey<ShakeWidgetState>();

//-------------------------------------- Focus nodes -------------------------
final FocusNode emailFocusNode = FocusNode();
final FocusNode phoneFocusNode = FocusNode();
final FocusNode passwordFocusNode = FocusNode();
Expand All @@ -43,25 +65,28 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
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();
late WebViewControllerPlus _controllerPlus;

final emailShakeKey = GlobalKey<ShakeWidgetState>();
final phoneShakeKey = GlobalKey<ShakeWidgetState>();
final passwordShakeKey = GlobalKey<ShakeWidgetState>();
final confirmPasswordShakeKey = GlobalKey<ShakeWidgetState>();
//-------------------------------------- Errors -------------------------
String? emailError;
String? phoneError;

String? passwordError;

String? confirmPasswordError;

final String siteKey = dotenv.env['RECAPTCHA_SITE_KEY'] ?? '';

String? captchaToken;

late WebViewControllerPlus _controllerPlus;
final double _height = 70;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -118,14 +143,31 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
});
}

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) {
vibrate();
return;
}
// phoneController.value.international gives the phone number in international format eg. +20123456789
AuthState signUpState =
SignupResult signUpResult =
await ref.read(authViewModelProvider.notifier).signUp(
email: emailController.text,
phone: phoneController.value.international,
Expand All @@ -134,19 +176,28 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
reCaptchaResponse: captchaToken!,
);

context.pop(); // to close the dialog
if (signUpState.type == AuthStateType.success) {
if (mounted) {
context.pop(); // to close the dialog
}
if (signUpResult.state.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);
}
} 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');
});
}
}

Expand All @@ -173,9 +224,20 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
if (someNotFilled) {
vibrate();
} else {
// todo show the dialog (marwan)
// showConfirmationDialog(
// context, emailController, _controllerPlus, signUp, onEdit);
// todo make recaptcha better
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(),
onCancelButtonKey: onCancellationKey,
onConfirmButtonKey: onConfirmationKey,
);
}
}

Expand Down Expand Up @@ -216,6 +278,8 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
width: 250.0),
ShakeMyAuthInput(
name: 'Email',
errorText: emailError,
formKey: emailKey,
shakeKey: emailShakeKey,
isFocused: isEmailFocused,
focusNode: emailFocusNode,
Expand All @@ -224,13 +288,17 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
),
AuthPhoneNumber(
name: 'Phone Number',
errorText: phoneError,
formKey: phoneKey,
shakeKey: phoneShakeKey,
isFocused: isPhoneFocused,
focusNode: phoneFocusNode,
controller: phoneController,
),
ShakeMyAuthInput(
name: 'Password',
errorText: passwordError,
formKey: passwordKey,
shakeKey: passwordShakeKey,
isFocused: isPasswordFocused,
focusNode: passwordFocusNode,
Expand All @@ -240,6 +308,8 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
),
ShakeMyAuthInput(
name: 'Confirm Password',
errorText: confirmPasswordError,
formKey: confirmPasswordKey,
shakeKey: confirmPasswordShakeKey,
isFocused: isConfirmPasswordFocused,
focusNode: confirmPasswordFocusNode,
Expand All @@ -251,10 +321,11 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const TitleElement(
name: 'Already have an account? ',
name: 'Already have an account?',
color: Palette.primaryText,
fontSize: Sizes.infoText),
AuthSubTextButton(
buttonKey: alreadyHaveAccountKey,
onPressed: () {
context.pop();
},
Expand Down
Loading
Loading