diff --git a/mobile-v3/lib/main.dart b/mobile-v3/lib/main.dart index bb412fb5f1..71126e16a4 100644 --- a/mobile-v3/lib/main.dart +++ b/mobile-v3/lib/main.dart @@ -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'; @@ -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'; @@ -117,6 +118,8 @@ class AirqoMobile extends StatelessWidget { BlocProvider( create: (context) => ConnectivityBloc(connectivity), ), + BlocProvider(create: (context) => PasswordResetBloc(authRepository: authRepository), + ) ], child: BlocBuilder( builder: (context, state) { diff --git a/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart new file mode 100644 index 0000000000..28e8f795e4 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart @@ -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 { + final AuthRepository authRepository; + + PasswordResetBloc({required this.authRepository}) : super(PasswordResetInitial()) { + on((event, emit) async { + try { + emit(PasswordResetLoading(email: event.email)); + await authRepository.requestPasswordReset(event.email); + emit(PasswordResetSuccess(message: '')); + } catch (e) { + emit(PasswordResetError(message: e.toString())); + } + }); + + on((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')); + } + }); + } +} diff --git a/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart new file mode 100644 index 0000000000..538268e3f0 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; + +abstract class PasswordResetEvent extends Equatable { + @override + List get props => []; +} + +class RequestPasswordReset extends PasswordResetEvent { + final String email; + + RequestPasswordReset(this.email); + + @override + List 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 get props => [confirmPassword, password, token]; +} diff --git a/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_state.dart b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_state.dart new file mode 100644 index 0000000000..02941d0cb7 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; + +abstract class PasswordResetState extends Equatable { + final String? email; + const PasswordResetState({this.email}); + + @override + List 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 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 get props => [email, message]; // Nullable email +} + diff --git a/mobile-v3/lib/src/app/auth/pages/login_page.dart b/mobile-v3/lib/src/app/auth/pages/login_page.dart index d71ab24f04..3099d39cf3 100644 --- a/mobile-v3/lib/src/app/auth/pages/login_page.dart +++ b/mobile-v3/lib/src/app/auth/pages/login_page.dart @@ -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'; @@ -28,7 +29,7 @@ class _LoginPageState extends State { super.initState(); emailController = TextEditingController(); passwordController = TextEditingController(); - + try { authBloc = context.read(); @@ -67,7 +68,7 @@ class _LoginPageState extends State { style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor), + color: Theme.of(context).textTheme.headlineLarge?.color), ), centerTitle: true, ), @@ -174,20 +175,35 @@ class _LoginPageState extends State { 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), + ), + ), + ) ], + ), ), ), diff --git a/mobile-v3/lib/src/app/auth/pages/password_reset/forgot_password.dart b/mobile-v3/lib/src/app/auth/pages/password_reset/forgot_password.dart new file mode 100644 index 0000000000..2c8172e7b7 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/pages/password_reset/forgot_password.dart @@ -0,0 +1,210 @@ +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart'; +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart'; +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_state.dart'; +import 'package:airqo/src/app/auth/bloc/auth_bloc.dart'; +import 'package:airqo/src/app/auth/pages/login_page.dart'; +import 'package:airqo/src/app/auth/pages/password_reset/reset_link_sent.dart'; + +import 'package:airqo/src/app/shared/pages/nav_page.dart'; +import 'package:airqo/src/app/shared/widgets/form_field.dart'; +import 'package:airqo/src/app/shared/widgets/spinner.dart'; +import 'package:airqo/src/meta/utils/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:loggy/loggy.dart'; + +class ForgotPasswordPage extends StatefulWidget { + const ForgotPasswordPage({super.key}); + + @override + State createState() => _ForgotPasswordPage(); +} + +class _ForgotPasswordPage extends State { + String? error; + late PasswordResetBloc passwordResetBloc; + late TextEditingController emailController = TextEditingController(); + + late GlobalKey formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + emailController = TextEditingController(); + + + try { + passwordResetBloc = context.read(); + + } catch (e) { + logError('Failed to initialize PasswordResetBloc: $e'); + } + } + + @override + void dispose() { + emailController.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is PasswordResetSuccess) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ResetLinkSentPage())); + } else if (state is PasswordResetError) { + setState(() { + error = state.message.replaceAll("Exception: ", ""); + }); + } + }, + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Forgot Password", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.headlineLarge?.color), + ), + centerTitle: true, + + + ), + + body: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Padding( + padding: const EdgeInsets.only(left: 32, right: 32, top: 8), + child: SizedBox( + child: Column( + children: [ + Text("Forgot Your Password?", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + SizedBox( + height: 12, + ), + Text("Enter your email address and we will send you a code to reset your password.", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + SizedBox(height: 32), + FormFieldWidget( + prefixIcon: Container( + padding: const EdgeInsets.all(13.5), + child: SvgPicture.asset( + "assets/icons/email-icon.svg", + height: 10, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "This field cannot be blank."; + } + return null; + }, + hintText: "Enter your email", + label: "Email*", + controller: emailController), + SizedBox(height: 18), + + ], + ), + ), + ), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(left: 32.0, top: 8), + child: Text(error!, style: TextStyle(color: Colors.red)), + ), + SizedBox(height: 18), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: BlocBuilder( + builder: (context, state) { + bool loading = state is PasswordResetLoading; + + return InkWell( + onTap: loading + ? null + : () { + final currentForm = formKey.currentState; + if (currentForm != null && + currentForm.validate()) { + passwordResetBloc.add(RequestPasswordReset( + emailController.text.trim() + )); + + emailController.clear(); + + + } + }, + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child: loading + ? Spinner() + : Text( + "Submit", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ); + }, + ), + ), + SizedBox( + height: 18 + ), + + InkWell( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => LoginPage())), + child: Center( + child: Text( + "Login", + style: TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.primaryColor), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile-v3/lib/src/app/auth/pages/password_reset/password_reset.dart b/mobile-v3/lib/src/app/auth/pages/password_reset/password_reset.dart new file mode 100644 index 0000000000..77ba51018a --- /dev/null +++ b/mobile-v3/lib/src/app/auth/pages/password_reset/password_reset.dart @@ -0,0 +1,221 @@ +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart'; + +import 'package:airqo/src/app/auth/pages/password_reset/reset_success.dart'; +import 'package:airqo/src/app/shared/widgets/form_field.dart'; +import 'package:airqo/src/app/shared/widgets/spinner.dart'; +import 'package:airqo/src/meta/utils/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:loggy/loggy.dart'; + +import '../../bloc/ForgotPasswordBloc/forgot_password_bloc.dart'; +import '../../bloc/ForgotPasswordBloc/forgot_password_state.dart'; + +class PasswordResetPage extends StatefulWidget { + final String token; + const PasswordResetPage({super.key, required this.token}); + + @override + State createState() => _PasswordResetPage(); +} + +class _PasswordResetPage extends State { + + String? error; + late PasswordResetBloc passwordResetBloc; + late TextEditingController passwordConfirmController = TextEditingController(); + late TextEditingController passwordController = TextEditingController(); + late TextEditingController resetController= TextEditingController(); + late GlobalKey formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + passwordConfirmController = TextEditingController(); + passwordController = TextEditingController(); + resetController= TextEditingController(); + + try { + passwordResetBloc = context.read(); + + } catch (e) { + logError('Failed to initialize PasswordResetBloc: $e'); + } + } + + @override + void dispose() { + passwordConfirmController.dispose(); + passwordController.dispose(); + resetController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is PasswordResetSuccess) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ResetSuccessPage())); + } else if (state is PasswordResetError) { + setState(() { + error = state.message.replaceAll("Exception: ", ""); + }); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text( + "Reset Password", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.headlineLarge?.color + ), + ), + centerTitle: true, + ), + body: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Padding( + padding: const EdgeInsets.only(left: 32, right: 32, top: 8), + child: SizedBox( + child: Column( + children: [ + + Text("Reset your password", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + SizedBox( + height: 20, + ), + Text("Please enter your new password below. Make sure it's something secure that you can remember.", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + + SizedBox(height: 16), + FormFieldWidget( + prefixIcon: Container( + padding: const EdgeInsets.all(13.5), + child: SvgPicture.asset( + "assets/icons/password.svg", + height: 10, + ), + ), + validator: (value) { + + if (value == null || value.isEmpty) { + return "This field cannot be blank."; + } + return null; + }, + hintText: "Enter your password", + label: "Password", + isPassword: true, + controller: passwordController), + SizedBox(height: 16), + FormFieldWidget( + prefixIcon: Container( + padding: const EdgeInsets.all(13.5), + child: SvgPicture.asset( + "assets/icons/password.svg", + height: 10, + ), + ), + validator: (value) { + if(value != passwordController.text){ + return "Passwords do not match"; + } + + if (value == null || value.isEmpty) { + return "This field cannot be blank."; + } + return null; + }, + hintText: "Re-enter your new password", + label: "Confirm Password", + isPassword: true, + controller: passwordConfirmController), + SizedBox(height: 16), + ], + ), + ), + ), + ), + if (error != null) + Padding( + padding: const EdgeInsets.only(left: 32.0, top: 8), + child: Text(error!, style: TextStyle(color: Colors.red)), + ), + SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32.0), + child: BlocBuilder( + builder: (context, state) { + bool loading = state is PasswordResetLoading; + + return InkWell( + onTap: loading + ? null + : () { + + + + final currentForm = formKey.currentState; + if (currentForm != null && + currentForm.validate()) { + passwordResetBloc.add(UpdatePassword( + confirmPassword: passwordConfirmController.text.trim(), + token: widget.token, + password:passwordController.text.trim())); + + } + resetController.clear(); + passwordConfirmController.clear(); + passwordController.clear(); + }, + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child: loading + ? Spinner() + : Text( + "Reset Password", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ); + }, + ), + ), + SizedBox(height: 16), + + ], + ), + ), + ), + ); + } +} diff --git a/mobile-v3/lib/src/app/auth/pages/password_reset/reset_link_sent.dart b/mobile-v3/lib/src/app/auth/pages/password_reset/reset_link_sent.dart new file mode 100644 index 0000000000..37eef372b9 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/pages/password_reset/reset_link_sent.dart @@ -0,0 +1,193 @@ +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart'; +import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_event.dart'; +import 'package:airqo/src/app/auth/pages/password_reset/password_reset.dart'; + +import 'package:airqo/src/meta/utils/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; + +class ResetLinkSentPage extends StatefulWidget { + const ResetLinkSentPage({super.key}); + + @override + _ResetLinkSentPageState createState() => _ResetLinkSentPageState(); +} + +class _ResetLinkSentPageState extends State { + final TextEditingController _pinController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Forgot Password", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.headlineLarge?.color), + ), + + centerTitle: true, + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(left: 32, right: 32, top: 8), + child: Column( + children: [ + Text( + "We just sent you a Password Reset Code to your email", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + SizedBox(height: 20), + Text( + "Enter the verification code sent to ha ******@gmail.com", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + + // PIN Code Input + SizedBox(height: 20), + PinCodeTextField( + appContext: context, + length: 5, // Adjust length if needed + controller: _pinController, + keyboardType: TextInputType.number, + animationType: AnimationType.fade, + hintCharacter: "0", + //backgroundColor: AppColors.boldHeadlineColor4, + + + textStyle: TextStyle( + fontSize: 28, + fontWeight: FontWeight.w600, + color: AppColors.boldHeadlineColor3 + + ), + pinTheme: PinTheme( + + shape: PinCodeFieldShape.box, + borderRadius: BorderRadius.circular(4), + + fieldHeight: 64, + fieldWidth: 60, + activeFillColor: Theme.of(context).highlightColor, + inactiveFillColor:Theme.of(context).highlightColor , + selectedFillColor: Theme.of(context).appBarTheme.backgroundColor, + activeColor: Theme.of(context).highlightColor, + inactiveColor: Theme.of(context).highlightColor, + selectedColor: AppColors.primaryColor, + fieldOuterPadding: EdgeInsets.symmetric(horizontal: 4), + + + ), + enableActiveFill: true, + onChanged: (value) {}, + ), + + SizedBox(height: 18), + InkWell( + onTap: () { + final pin = _pinController.text.trim(); + + + if (RegExp(r'^\d{5}$').hasMatch(pin)) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => PasswordResetPage(token: pin), + ), + ); + } else { + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please enter a valid 5-digit number.', + style: TextStyle( + color: Colors.white + ),), + backgroundColor: AppColors.primaryColor, + ), + ); + } + _pinController.clear(); + }, + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child: Text( + "Continue", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ), + + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Didn't receive the code?", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.headlineLarge?.color + ), + ), + InkWell( + onTap: () { + final email = context.read().state.email; + if (email != null && email.isNotEmpty) { + context.read().add( + RequestPasswordReset(email), + ); + print("Resend requested for email: $email"); + } else { + print("Email is null or empty. Cannot resend."); + } + }, + child: Center( + child: Text( + " Resend", + style: TextStyle( + fontWeight: FontWeight.w500, + color: AppColors.primaryColor), + ), + ), + ), + ], + ), + SizedBox(height: 20), + ], + ), + ), + SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/mobile-v3/lib/src/app/auth/pages/password_reset/reset_success.dart b/mobile-v3/lib/src/app/auth/pages/password_reset/reset_success.dart new file mode 100644 index 0000000000..03260953c5 --- /dev/null +++ b/mobile-v3/lib/src/app/auth/pages/password_reset/reset_success.dart @@ -0,0 +1,100 @@ + +import 'package:airqo/src/meta/utils/colors.dart'; +import 'package:flutter/material.dart'; + + +import '../login_page.dart'; + + +class ResetSuccessPage extends StatelessWidget { + const ResetSuccessPage({super.key}); + + @override + Widget build(BuildContext context) { + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Reset Password", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: AppColors.boldHeadlineColor2), + ), + centerTitle: true, + + + ), + + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Padding( + padding: const EdgeInsets.only(left: 32, right: 32, top: 8), + child: SizedBox( + child: Column( + children: [ + Text("Your password has been reset successfully!", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.titleMedium?.color + + ), + ), + SizedBox( + height: 20, + ), + Text("You can now log in to your account using your new password.", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.titleMedium?.color + ), + ), + + SizedBox( + height: 20, + ), + + InkWell( + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => LoginPage())), + child: Container( + height: 56, + decoration: BoxDecoration( + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4)), + child: Center( + child:Text( + "Login", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ), + ), + + ], + ), + ), + ), + ), + + + + + ], + ), + ); + } +} diff --git a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart index b19faad3f0..98b011098d 100644 --- a/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart +++ b/mobile-v3/lib/src/app/auth/pages/welcome_screen.dart @@ -1,4 +1,5 @@ import 'package:airqo/src/app/auth/pages/login_page.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/auth/widgets/breathe_clean.dart'; import 'package:airqo/src/app/auth/widgets/know_your_air.dart'; @@ -25,7 +26,7 @@ class _WelcomeScreenState extends State { @override void initState() { - controller = new PageController(); + controller = PageController(); super.initState(); } @@ -45,67 +46,69 @@ class _WelcomeScreenState extends State { Widget build(BuildContext context) { return BlocListener( listener: (context, state) { - if (state is GuestUser) { - Future.microtask(() => Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => NavPage()), - )); - } + if (state is GuestUser) { + Future.microtask(() => Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => NavPage()), + )); + } }, child: Scaffold( - body: Column( - children: [ - Stack( - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.6, - child: PageView( - controller: controller, - onPageChanged: changeIndex, - children: [ - WelcomeWidget(), - BreatheClean(), - KnowYourAir(), - ], + body: Column( + children: [ + Stack( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: PageView( + controller: controller, + onPageChanged: changeIndex, + children: [ + WelcomeWidget(), + BreatheClean(), + KnowYourAir(), + ], + ), ), - ), - Container( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.6, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AnimatedSmoothIndicator( - activeIndex: currentIndex, - count: 3, - effect: ExpandingDotsEffect( + Container( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.6, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AnimatedSmoothIndicator( + activeIndex: currentIndex, + count: 3, + effect: ExpandingDotsEffect( dotWidth: 7, radius: 7, dotColor: Color(0xff60646C), - // activeDotColor: Color(0xffF6F6F7), activeDotColor: AppColors.primaryColor, - dotHeight: 7), - ), - SizedBox(height: 16) - ], + dotHeight: 7, + ), + ), + SizedBox(height: 16), + ], + ), ), - ), - ], - ), - SizedBox( + ], + ), + SizedBox( height: MediaQuery.of(context).size.height * 0.4, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16 * 2), + padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ InkWell( - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (context) => CreateAccountScreen())), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => CreateAccountScreen()), + ), child: Container( height: 56, decoration: BoxDecoration( - color: AppColors.primaryColor, - borderRadius: BorderRadius.circular(4)), + color: AppColors.primaryColor, + borderRadius: BorderRadius.circular(4), + ), child: Center( child: Text( "Create Account", @@ -120,49 +123,58 @@ class _WelcomeScreenState extends State { SizedBox(height: 18), InkWell( onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (context) => LoginPage())), + MaterialPageRoute(builder: (context) => LoginPage()), + ), child: Container( height: 56, decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Theme.of(context).highlightColor, - borderRadius: BorderRadius.circular(4)), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Theme.of(context).highlightColor, + borderRadius: BorderRadius.circular(4), + ), child: Center( child: Text( "Login Here", style: TextStyle( - fontWeight: FontWeight.w500, color: Colors.black), + fontWeight: FontWeight.w500, + color: Colors.black, + ), ), ), ), ), SizedBox(height: 18), - InkWell( - onTap: () =>context.read().add(UseAsGuest()), + onTap: () => context.read().add(UseAsGuest()), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Continue as guest ", - style: TextStyle( - color: AppColors.boldHeadlineColor2, - fontWeight: FontWeight.w500)), - SvgPicture.asset('assets/icons/chevron-right.svg', - height: 16.0, - width: 16.0, - color: AppColors.boldHeadlineColor2,) + Text( + "Continue as guest ", + style: TextStyle( + color: Theme.of(context).textTheme.headlineLarge?.color, + fontWeight: FontWeight.w500, + ), + ), + SvgPicture.asset( + 'assets/icons/chevron-right.svg', + height: 16.0, + width: 16.0, + color: Theme.of(context).textTheme.headlineLarge?.color, + ), ], ), ), ), - ], ), - )) - ], - )), + ), + ), + ], + ), + ), ); } } diff --git a/mobile-v3/lib/src/app/auth/repository/auth_repository.dart b/mobile-v3/lib/src/app/auth/repository/auth_repository.dart index 2ea4a92d0f..8625a5dd41 100644 --- a/mobile-v3/lib/src/app/auth/repository/auth_repository.dart +++ b/mobile-v3/lib/src/app/auth/repository/auth_repository.dart @@ -10,12 +10,18 @@ abstract class AuthRepository { //login function Future loginWithEmailAndPassword(String username, String password); Future registerWithEmailAndPassword(RegisterInputModel model); + Future requestPasswordReset(String email); + Future updatePassword({ + required String token, + required String password, + required String confirmPassword, + }); } class AuthImpl extends AuthRepository { @override - Future loginWithEmailAndPassword( - String username, String password) async { + Future loginWithEmailAndPassword(String username, + String password) async { Response loginResponse = await http.post( Uri.parse("https://api.airqo.net/api/v2/users/loginUser"), body: jsonEncode({"userName": username, "password": password}), @@ -51,4 +57,60 @@ class AuthImpl extends AuthRepository { throw Exception(data['errors']['message'] ?? data['message']); } } + + @override + Future requestPasswordReset(String email) async { + final response = await http.post( + Uri.parse('https://api.airqo.net/api/v2/users/reset-password-request'), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'email': email, // User's email address + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['success'] == true) { + return data['message'] ?? 'Password reset link sent.'; + } else { + throw Exception(data['message'] ?? 'Failed to send password reset request.'); + } + } else { + final error = jsonDecode(response.body)['message'] ?? 'Something went wrong.'; + throw Exception(error); + } + } + + + @override + Future updatePassword({ + required String token, + required String password, + required String confirmPassword, + }) async { + final response = await http.post( + Uri.parse('https://api.airqo.net/api/v2/users/reset-password/$token'), + + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'password': password, + 'confirmPassword': confirmPassword, // Include confirmPassword + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['message'] ?? 'Password reset successful.'; + } else { + final error = jsonDecode(response.body)['message'] ?? + 'Failed to reset password.'; + throw Exception(error); + } + } } + + diff --git a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart index e22c235614..6d88eaa80b 100644 --- a/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart +++ b/mobile-v3/lib/src/app/dashboard/pages/dashboard_page.dart @@ -192,7 +192,7 @@ class _DashboardPageState extends State { style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor2, + color: Theme.of(context).textTheme.headlineLarge?.color, ), ); } else { @@ -204,7 +204,7 @@ class _DashboardPageState extends State { style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor, + color: Theme.of(context).textTheme.headlineLarge?.color, ), ); } @@ -213,7 +213,7 @@ class _DashboardPageState extends State { style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.boldHeadlineColor, + color: Theme.of(context).textTheme.headlineLarge?.color, ), ); }, @@ -227,7 +227,7 @@ class _DashboardPageState extends State { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: Color(0xff60646C), + color: Theme.of(context).textTheme.headlineMedium?.color, ), ), SizedBox(height: 4) diff --git a/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart b/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart index 31dff6608a..eda3d4745f 100644 --- a/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart +++ b/mobile-v3/lib/src/app/dashboard/widgets/analytics_card.dart @@ -47,7 +47,7 @@ class AnalyticsCard extends StatelessWidget { Text( " PM2.5", style: TextStyle( - color: Color(0xff7A7F87), + color: Theme.of(context).textTheme.headlineSmall?.color, ), ), ], @@ -61,13 +61,17 @@ class AnalyticsCard extends StatelessWidget { style: TextStyle( fontWeight: FontWeight.w700, fontSize: 40, - color: AppColors.boldHeadlineColor2), + color: Theme.of(context).textTheme.headlineLarge?.color + + ), ), Text(" μg/m3", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 20, - color: AppColors.boldHeadlineColor2)) + color: Theme.of(context).textTheme.headlineLarge?.color + ) + ) ]), ]), SizedBox( @@ -99,12 +103,16 @@ class AnalyticsCard extends StatelessWidget { style: TextStyle( fontSize: 28, fontWeight: FontWeight.w700, - color: AppColors.secondaryHeadlineColor3)), + color: Theme.of(context).textTheme.headlineSmall?.color + ) + ), Text(measurement.healthTips![0].description!, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, - color: AppColors.secondaryHeadlineColor2)) + color: Theme.of(context).textTheme.headlineMedium?.color + ) + ) ], ), ), diff --git a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart index 46dfe2fa93..6bd76ba9db 100644 --- a/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart +++ b/mobile-v3/lib/src/app/profile/pages/guest_profile page.dart @@ -74,7 +74,7 @@ class _GuestProfilePageState extends State { Text( "Guest User", style: TextStyle( - color: AppColors.boldHeadlineColor2, + color: Theme.of(context).textTheme.headlineSmall?.color, fontSize: 24, fontWeight: FontWeight.w700, ), @@ -86,7 +86,7 @@ class _GuestProfilePageState extends State { child: Text( 'Settings', style:TextStyle( - color: AppColors.boldHeadlineColor2, + color: Theme.of(context).textTheme.headlineSmall?.color, fontSize: 18, fontWeight: FontWeight.w500, ), diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart index 4911dbec69..70056e173b 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_tile.dart @@ -38,7 +38,7 @@ class SettingsTile extends StatelessWidget { subtitle: description != null ? Text(description!, style: TextStyle( - color: AppColors.secondaryHeadlineColor2, + color: Theme.of(context).textTheme.headlineMedium?.color, fontSize: 14, fontWeight: FontWeight.w400)) : null, @@ -54,11 +54,14 @@ class SettingsTile extends StatelessWidget { ), ], ), - title: Text(title, + title: Text( + title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, - color: AppColors.secondaryHeadlineColor3)), + color: Theme.of(context).textTheme.headlineSmall?.color + ) + ), ), Divider( color:Theme.of(context).highlightColor, diff --git a/mobile-v3/lib/src/meta/utils/colors.dart b/mobile-v3/lib/src/meta/utils/colors.dart index 03cae8558e..6f1d1e6e84 100644 --- a/mobile-v3/lib/src/meta/utils/colors.dart +++ b/mobile-v3/lib/src/meta/utils/colors.dart @@ -8,13 +8,17 @@ class AppColors { // static Color highlightColor = Color(0xff2E2F33); // static Color boldHeadlineColor = Color(0xff9EA3AA); // static Color secondaryHeadlineColor = Color(0xff60646C); - + static Color primaryColor = Color(0xff145FFF); static Color backgroundColor = Color(0xffF9FAFB); static Color highlightColor = Color(0xffF3F6F8); static Color boldHeadlineColor = Color(0xff6F87A1); - static Color secondaryHeadlineColor = Color(0xff6F87A1); static Color boldHeadlineColor2 = Color(0xff9EA3AA); + static Color boldHeadlineColor3 = Color(0xff7A7F87); + static Color boldHeadlineColor4 = Color(0xff2E2F33); + static Color highlightColor2= Color(0xffE2E3E5); + static Color secondaryHeadlineColor = Color(0xff6F87A1); + static Color darkThemeBackground= Color(0xff1C1D20); static Color secondaryHeadlineColor2 = Color(0xff60646C); static Color secondaryHeadlineColor3 = Color(0xff7A7F87); @@ -36,12 +40,18 @@ class AppTheme { highlightColor: const Color(0xffF3F6F8), textTheme: TextTheme( headlineLarge: TextStyle( - color: const Color(0xff6F87A1), + color: const Color(0xff000000), fontWeight: FontWeight.bold, ), headlineMedium: TextStyle( - color: const Color(0xff6F87A1), + color: Colors.black, ), + headlineSmall: TextStyle( + color: const Color(0xff000000), + ), + titleMedium:TextStyle( + color: const Color(0xff000000) + ) , titleLarge: TextStyle( fontSize: 40, fontWeight: FontWeight.w700, color: Colors.black), ), @@ -68,6 +78,12 @@ class AppTheme { headlineMedium: TextStyle( color: const Color(0xff60646C), ), + headlineSmall: TextStyle( + color: const Color(0xff7A7F87), + ), + titleMedium:TextStyle( + color: const Color(0xffE2E3E5) + ) , titleLarge: TextStyle( fontSize: 40, fontWeight: FontWeight.w700, color: Colors.white), ), diff --git a/mobile-v3/pubspec.lock b/mobile-v3/pubspec.lock index 8dd601d822..dd502aaaf3 100644 --- a/mobile-v3/pubspec.lock +++ b/mobile-v3/pubspec.lock @@ -733,6 +733,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/mobile-v3/pubspec.yaml b/mobile-v3/pubspec.yaml index 6fcb1b26a8..f60db567ab 100644 --- a/mobile-v3/pubspec.yaml +++ b/mobile-v3/pubspec.yaml @@ -56,8 +56,10 @@ dependencies: connectivity_plus: ^6.1.0 flutter_loggy: ^2.0.3+1 jwt_decoder: ^2.0.1 + pin_code_fields: ^8.0.1 package_info_plus: ^8.1.3 + dev_dependencies: flutter_test: sdk: flutter