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

Password reset feature #2432

Open
wants to merge 8 commits into
base: staging
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion mobile-v3/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:io';

import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart';
import 'package:airqo/src/app/auth/bloc/auth_bloc.dart';
import 'package:airqo/src/app/auth/pages/welcome_screen.dart';
import 'package:airqo/src/app/auth/repository/auth_repository.dart';
Expand All @@ -16,12 +17,12 @@ import 'package:airqo/src/app/other/places/repository/google_places_repository.d
import 'package:airqo/src/app/other/theme/bloc/theme_bloc.dart';
import 'package:airqo/src/app/other/theme/repository/theme_repository.dart';
import 'package:airqo/src/app/profile/bloc/user_bloc.dart';
import 'package:airqo/src/app/profile/pages/guest_profile%20page.dart';
import 'package:airqo/src/app/profile/repository/user_repository.dart';
import 'package:airqo/src/app/shared/bloc/connectivity_bloc.dart';
import 'package:airqo/src/app/shared/pages/nav_page.dart';

import 'package:airqo/src/meta/utils/colors.dart';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand Down Expand Up @@ -117,6 +118,8 @@ class AirqoMobile extends StatelessWidget {
BlocProvider(
create: (context) => ConnectivityBloc(connectivity),
),
BlocProvider(create: (context) => PasswordResetBloc(authRepository: authRepository),
)
],
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repository/auth_repository.dart';
import 'forgot_password_event.dart';
import 'forgot_password_state.dart';


class PasswordResetBloc extends Bloc<PasswordResetEvent, PasswordResetState> {
final AuthRepository authRepository;

PasswordResetBloc({required this.authRepository}) : super(PasswordResetInitial()) {
on<RequestPasswordReset>((event, emit) async {
try {
emit(PasswordResetLoading(email: event.email));
await authRepository.requestPasswordReset(event.email);
emit(PasswordResetSuccess(message: ''));
} catch (e) {
emit(PasswordResetError(message: e.toString()));
}
});
Comment on lines +11 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling in RequestPasswordReset event handler.

The current implementation catches all errors generically. Consider handling specific error types and preserving the email in error state.

     on<RequestPasswordReset>((event, emit) async {
       try {
         emit(PasswordResetLoading(email: event.email));
         await authRepository.requestPasswordReset(event.email);
-        emit(PasswordResetSuccess(message: ''));
+        emit(PasswordResetSuccess(
+          email: event.email,
+          message: 'Reset instructions sent to ${event.email}',
+        ));
       } catch (e) {
-        emit(PasswordResetError(message: e.toString()));
+        emit(PasswordResetError(
+          email: event.email,
+          message: e.toString(),
+          errorType: e is NetworkException 
+            ? PasswordResetErrorType.networkError 
+            : PasswordResetErrorType.unknown,
+        ));
       }
     });

Committable suggestion skipped: line range outside the PR's diff.


on<UpdatePassword>((event, emit) async {
emit(PasswordResetLoading());
try {
final message = await authRepository.updatePassword(
confirmPassword: event.confirmPassword,
password: event.password,
token: event.token,
);
emit(PasswordResetSuccess(message: message));
} catch (error) {
emit(PasswordResetError(message: 'Failed to update password. \nPlease re-check the code you entered'));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:equatable/equatable.dart';

abstract class PasswordResetEvent extends Equatable {
@override
List<Object?> get props => [];
}

class RequestPasswordReset extends PasswordResetEvent {
final String email;

RequestPasswordReset(this.email);

@override
List<Object?> get props => [email];
}

class UpdatePassword extends PasswordResetEvent {
final String confirmPassword;
final String password;
final String token;

UpdatePassword({
required this.confirmPassword,
required this.password,
required this.token,
});

@override
List<Object?> get props => [confirmPassword, password, token];
}
Comment on lines +17 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add password validation in UpdatePassword event.

The password and confirmPassword fields should be validated for matching values and minimum security requirements.

 class UpdatePassword extends PasswordResetEvent {
   final String confirmPassword;
   final String password;
   final String token;
 
   UpdatePassword({
     required this.confirmPassword,
     required this.password,
     required this.token,
-  });
+  }) {
+    assert(password == confirmPassword, 'Passwords do not match');
+    assert(password.length >= 8, 'Password must be at least 8 characters');
+    assert(token.isNotEmpty, 'Token cannot be empty');
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class UpdatePassword extends PasswordResetEvent {
final String confirmPassword;
final String password;
final String token;
UpdatePassword({
required this.confirmPassword,
required this.password,
required this.token,
});
@override
List<Object?> get props => [confirmPassword, password, token];
}
class UpdatePassword extends PasswordResetEvent {
final String confirmPassword;
final String password;
final String token;
UpdatePassword({
required this.confirmPassword,
required this.password,
required this.token,
}) {
assert(password == confirmPassword, 'Passwords do not match');
assert(password.length >= 8, 'Password must be at least 8 characters');
assert(token.isNotEmpty, 'Token cannot be empty');
}
@override
List<Object?> get props => [confirmPassword, password, token];
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:equatable/equatable.dart';

abstract class PasswordResetState extends Equatable {
final String? email;
const PasswordResetState({this.email});

@override
List<Object?> get props => [email];
}

class PasswordResetInitial extends PasswordResetState {
const PasswordResetInitial() : super();
}

class PasswordResetLoading extends PasswordResetState {
const PasswordResetLoading({String? email}) : super(email: email); // Optional email
}

class PasswordResetSuccess extends PasswordResetState {
final String message;

const PasswordResetSuccess({String? email, required this.message}) : super(email: email);

@override
List<Object?> get props => [email, message];
}

class PasswordResetError extends PasswordResetState {
final String message;

const PasswordResetError({String? email, required this.message})
: super(email: email); // Optional email

@override
List<Object?> get props => [email, message]; // Nullable email
}

24 changes: 20 additions & 4 deletions mobile-v3/lib/src/app/auth/pages/login_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:airqo/src/app/auth/bloc/auth_bloc.dart';
import 'package:airqo/src/app/auth/pages/password_reset/forgot_password.dart';
import 'package:airqo/src/app/auth/pages/register_page.dart';
import 'package:airqo/src/app/shared/pages/nav_page.dart';
import 'package:airqo/src/app/shared/widgets/form_field.dart';
Expand Down Expand Up @@ -28,7 +29,7 @@ class _LoginPageState extends State<LoginPage> {
super.initState();
emailController = TextEditingController();
passwordController = TextEditingController();

try {
authBloc = context.read<AuthBloc>();

Expand Down Expand Up @@ -67,7 +68,7 @@ class _LoginPageState extends State<LoginPage> {
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.boldHeadlineColor),
color: Theme.of(context).textTheme.headlineLarge?.color),
),
centerTitle: true,
),
Expand Down Expand Up @@ -174,20 +175,35 @@ class _LoginPageState extends State<LoginPage> {
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Text("Don't have an account?",
style: TextStyle(
color: AppColors.boldHeadlineColor,
color: Theme.of(context).textTheme.headlineLarge?.color,
fontWeight: FontWeight.w500)),
InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CreateAccountScreen())),
child: Text(
"Create Account",
" Create Account",
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.primaryColor),
),
)
]),
SizedBox(height: 16),

Center(
child: InkWell(
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ForgotPasswordPage())),
child: Text(
"Forgot password?",
style: TextStyle(
fontWeight: FontWeight.w500,
color: AppColors.primaryColor),
),
),
)
],

),
),
),
Expand Down
Loading