From 8bdd12a9b64f099fae3efe2791b92987c3e41634 Mon Sep 17 00:00:00 2001 From: "dinesh.patil" Date: Fri, 27 Mar 2026 19:21:31 +0530 Subject: [PATCH] add login flow with api integration and screen ui updated. --- assets/login/app_icon.png | Bin 10104 -> 14211 bytes lib/all_bloc_poviders/all_bloc_providers.dart | 37 ++ lib/constants/app_assets.dart | 3 + lib/constants/app_colors.dart | 14 + lib/core/app_router.dart | 32 +- lib/custome_widgets/custom_button.dart | 61 +++ lib/custome_widgets/custom_textfield.dart | 118 +++++ lib/local_peference/local_preference.dart | 77 +++ .../forgot_password/forgot_password_bloc.dart | 44 ++ .../forgot_password_event.dart | 25 + .../forgot_password_state.dart | 30 ++ lib/login/blocs/login/login_bloc.dart | 86 ++++ lib/login/blocs/login/login_event.dart | 51 ++ lib/login/blocs/login/login_state.dart | 54 ++ .../reset_password/reset_password_bloc.dart | 93 ++++ .../reset_password/reset_password_event.dart | 55 ++ .../reset_password/reset_password_state.dart | 90 ++++ .../blocs/verify_otp/verify_otp_bloc.dart | 37 ++ .../blocs/verify_otp/verify_otp_event.dart | 21 + .../blocs/verify_otp/verify_otp_state.dart | 26 + lib/login/models/login.dart | 63 +++ .../forgot_password_repository.dart | 17 + lib/login/repositories/login_repository.dart | 28 ++ lib/login/repositories/otp_repository.dart | 23 + .../reset_password_repository.dart | 23 + lib/login/views/forgot_password_page.dart | 359 ++++++------- lib/login/views/login_page.dart | 467 +++++++++-------- lib/login/views/otp_verification_page.dart | 287 +++++------ lib/login/views/reset_password_page.dart | 473 ++++++++++-------- lib/main.dart | 36 +- .../api_service/api_service.dart | 266 ++++++++++ .../api_urls/api_urls.dart | 18 + lib/profile/blocs/profile/profile_bloc.dart | 27 + lib/profile/blocs/profile/profile_event.dart | 10 + lib/profile/blocs/profile/profile_state.dart | 31 ++ lib/profile/blocs/profile_bloc.dart | 8 + lib/profile/models/profile_model.dart | 91 ++++ .../repository/profile_repository.dart | 21 + lib/profile/views/profile_page.dart | 154 +++--- lib/scan/view/qr_scan_screen.dart | 15 +- lib/splash/bloc/splash_bloc.dart | 34 ++ lib/splash/bloc/splash_event.dart | 5 + lib/splash/bloc/splash_state.dart | 13 + lib/splash/splash_view.dart | 72 --- lib/splash/view/splash_view.dart | 87 ++++ pubspec.lock | 116 ++++- pubspec.yaml | 3 + 47 files changed, 2738 insertions(+), 963 deletions(-) create mode 100644 lib/all_bloc_poviders/all_bloc_providers.dart create mode 100644 lib/constants/app_assets.dart create mode 100644 lib/constants/app_colors.dart create mode 100644 lib/custome_widgets/custom_button.dart create mode 100644 lib/custome_widgets/custom_textfield.dart create mode 100644 lib/local_peference/local_preference.dart create mode 100644 lib/login/blocs/forgot_password/forgot_password_bloc.dart create mode 100644 lib/login/blocs/forgot_password/forgot_password_event.dart create mode 100644 lib/login/blocs/forgot_password/forgot_password_state.dart create mode 100644 lib/login/blocs/login/login_bloc.dart create mode 100644 lib/login/blocs/login/login_event.dart create mode 100644 lib/login/blocs/login/login_state.dart create mode 100644 lib/login/blocs/reset_password/reset_password_bloc.dart create mode 100644 lib/login/blocs/reset_password/reset_password_event.dart create mode 100644 lib/login/blocs/reset_password/reset_password_state.dart create mode 100644 lib/login/blocs/verify_otp/verify_otp_bloc.dart create mode 100644 lib/login/blocs/verify_otp/verify_otp_event.dart create mode 100644 lib/login/blocs/verify_otp/verify_otp_state.dart create mode 100644 lib/network_api_service/api_service/api_service.dart create mode 100644 lib/network_api_service/api_urls/api_urls.dart create mode 100644 lib/profile/blocs/profile/profile_bloc.dart create mode 100644 lib/profile/blocs/profile/profile_event.dart create mode 100644 lib/profile/blocs/profile/profile_state.dart create mode 100644 lib/profile/models/profile_model.dart create mode 100644 lib/profile/repository/profile_repository.dart create mode 100644 lib/splash/bloc/splash_bloc.dart create mode 100644 lib/splash/bloc/splash_event.dart create mode 100644 lib/splash/bloc/splash_state.dart delete mode 100644 lib/splash/splash_view.dart create mode 100644 lib/splash/view/splash_view.dart diff --git a/assets/login/app_icon.png b/assets/login/app_icon.png index 702f709084b5b26e3dee88737531f967ba76ead3..569864b9588a9ae014bb6ef2ee5fc20ba5db25b6 100644 GIT binary patch literal 14211 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 cO}O=p!s_#`m@T4P`9Cx;NbyUxq|x{P2W2N providers() { + return [ + // ─── Splash ────────────────────────────────────────────────────────── + BlocProvider( + create: (_) => SplashBloc(), + ), + BlocProvider( + create: (_) => LoginBloc(), + ), + BlocProvider( + create: (_) => ForgotPasswordBloc(), + ), + BlocProvider( + create: (_) => VerifyOtpBloc(), + ), + BlocProvider( + create: (_) => ResetPasswordBloc(), + ), + // ─── Profile ───────────────────────────────────────────────────────── + BlocProvider( + create: (_) => ProfileBloc(profileRepository: ProfileRepository()), + ), + ]; + } +} \ No newline at end of file diff --git a/lib/constants/app_assets.dart b/lib/constants/app_assets.dart new file mode 100644 index 0000000..76f2ad5 --- /dev/null +++ b/lib/constants/app_assets.dart @@ -0,0 +1,3 @@ +class AppAssets { + static const String appIcon = "assets/login/app_icon.png"; +} diff --git a/lib/constants/app_colors.dart b/lib/constants/app_colors.dart new file mode 100644 index 0000000..9c5b780 --- /dev/null +++ b/lib/constants/app_colors.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color primaryRed = Color(0xFFF16F6F); + static const Color backgroundWhite = Colors.white; + static const Color labelGrey = Color(0xFF4B4B4B); + static const Color textGrey = Color(0xFF7C7C7C); + static const Color hintGrey = Color(0xFFBDBDBD); + static const Color borderGrey = Color(0xFFE0E0E0); + static const Color successGreen = Color(0xFF4CAF50); + static const Color errorLight = Color(0xFFFFF1F1); + static const Color bgLightGrey = Color(0xFFF9F9F9); + static const Color black = Colors.black; +} diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 8cfa8c4..c22212f 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -12,7 +12,7 @@ import '../redemption/view/ticket_redemption_screen.dart'; import '../scan/view/qr_scan_screen.dart'; import '../scan_history/views/scan_history_detail_page.dart'; import '../scan_history/views/scan_history_page.dart'; -import '../splash/splash_view.dart'; +import '../splash/view/splash_view.dart'; import '../support/view/help_support_page.dart'; class AppRouter { @@ -45,26 +45,36 @@ class AppRouter { case forgotPassword: return MaterialPageRoute(builder: (_) => const ForgotPasswordPage()); case otpVerification: - return MaterialPageRoute(builder: (_) => const OtpVerificationPage()); + final email = settings.arguments as String? ?? ''; + return MaterialPageRoute( + builder: (_) => OtpVerificationPage(email: email), + ); case resetPassword: - return MaterialPageRoute(builder: (_) => const ResetPasswordPage()); + final email = settings.arguments as String? ?? ''; + return MaterialPageRoute( + builder: (_) => ResetPasswordPage(email: email), + ); case profileScreen: return MaterialPageRoute(builder: (_) => const ProfileScreen()); - case qrScanScreen: + case qrScanScreen: return MaterialPageRoute(builder: (_) => const QrScanScreen()); - case splashScreen: + case splashScreen: return MaterialPageRoute(builder: (_) => const SplashScreen()); - case scanHistoryDetailPage: - return MaterialPageRoute(builder: (_) => const ScanHistoryDetailPage(passId: 'P214125125',)); + case scanHistoryDetailPage: + return MaterialPageRoute( + builder: (_) => const ScanHistoryDetailPage( + passId: 'P214125125', + )); case selectedTimeSlotPage: return MaterialPageRoute(builder: (_) => const SelectedTimeSlotPage()); case bookingPage: return MaterialPageRoute(builder: (_) => const BookingPage()); case helpSupportPage: return MaterialPageRoute(builder: (_) => const HelpSupportPage()); - case ticketRedemptionScreen: - return MaterialPageRoute(builder: (_) => const TicketRedemptionScreen()); - case recurringBlockBasicInfo: + case ticketRedemptionScreen: + return MaterialPageRoute( + builder: (_) => const TicketRedemptionScreen()); + case recurringBlockBasicInfo: return MaterialPageRoute(builder: (_) => const RecurringBlockPage()); default: return MaterialPageRoute( @@ -73,4 +83,4 @@ class AppRouter { ); } } -} +} \ No newline at end of file diff --git a/lib/custome_widgets/custom_button.dart b/lib/custome_widgets/custom_button.dart new file mode 100644 index 0000000..2456170 --- /dev/null +++ b/lib/custome_widgets/custom_button.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../constants/app_colors.dart'; + +class CustomButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + final Color? backgroundColor; + final Color? textColor; + final double height; + final double borderRadius; + + const CustomButton({ + super.key, + required this.text, + this.onPressed, + this.isLoading = false, + this.backgroundColor, + this.textColor, + this.height = 56, + this.borderRadius = 16, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: height.h, + child: ElevatedButton( + onPressed: isLoading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: backgroundColor ?? AppColors.primaryRed, + disabledBackgroundColor: (backgroundColor ?? AppColors.primaryRed).withOpacity(0.4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius.r), + ), + elevation: 0, + ), + child: isLoading + ? SizedBox( + height: 24.h, + width: 24.w, + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + text, + style: GoogleFonts.poppins( + color: textColor ?? Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ); + } +} diff --git a/lib/custome_widgets/custom_textfield.dart b/lib/custome_widgets/custom_textfield.dart new file mode 100644 index 0000000..61bfa8e --- /dev/null +++ b/lib/custome_widgets/custom_textfield.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../constants/app_colors.dart'; + +class CustomTextField extends StatelessWidget { + final String label; + final String hintText; + final TextEditingController controller; + final FocusNode? focusNode; + final IconData? prefixIcon; + final bool hasError; + final String? errorText; + final bool isPassword; + final bool isPasswordVisible; + final VoidCallback? onTogglePasswordVisibility; + final bool readOnly; + final TextInputType keyboardType; + final TextInputAction textInputAction; + final ValueChanged? onChanged; + final ValueChanged? onSubmitted; + final Color? accentColor; + + const CustomTextField({ + super.key, + required this.label, + required this.hintText, + required this.controller, + this.focusNode, + this.prefixIcon, + this.hasError = false, + this.errorText, + this.isPassword = false, + this.isPasswordVisible = false, + this.onTogglePasswordVisibility, + this.readOnly = false, + this.keyboardType = TextInputType.text, + this.textInputAction = TextInputAction.done, + this.onChanged, + this.onSubmitted, + this.accentColor, + }); + + @override + Widget build(BuildContext context) { + final Color primary = accentColor ?? AppColors.primaryRed; + final Color labelColor = AppColors.labelGrey; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: GoogleFonts.poppins( + color: labelColor, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 8.h), + TextField( + controller: controller, + focusNode: focusNode, + readOnly: readOnly, + obscureText: isPassword && !isPasswordVisible, + keyboardType: keyboardType, + textInputAction: textInputAction, + style: GoogleFonts.poppins(fontSize: 14.sp), + onChanged: onChanged, + onSubmitted: onSubmitted, + decoration: InputDecoration( + hintText: hintText, + hintStyle: GoogleFonts.poppins(color: AppColors.hintGrey), + prefixIcon: prefixIcon != null + ? Icon(prefixIcon, color: AppColors.hintGrey, size: 20.sp) + : null, + suffixIcon: isPassword + ? IconButton( + icon: Icon( + isPasswordVisible + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: AppColors.hintGrey, + size: 20.sp, + ), + onPressed: onTogglePasswordVisibility, + ) + : null, + contentPadding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 16.w), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: BorderSide( + color: hasError ? primary : AppColors.borderGrey, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: BorderSide(color: primary), + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + borderSide: BorderSide(color: AppColors.borderGrey.withOpacity(0.5)), + ), + ), + ), + if (hasError && errorText != null) + Padding( + padding: EdgeInsets.only(top: 4.h), + child: Text( + errorText!, + style: GoogleFonts.poppins(color: primary, fontSize: 12.sp), + ), + ), + ], + ); + } +} diff --git a/lib/local_peference/local_preference.dart b/lib/local_peference/local_preference.dart new file mode 100644 index 0000000..8bc55de --- /dev/null +++ b/lib/local_peference/local_preference.dart @@ -0,0 +1,77 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalPreference { + // Keys + static const String _keyLogin = "is_logged_in"; + static const String _keyUserId = "user_id"; + static const String _keyAccessToken = "access_token"; + static const String _keyRefreshToken = "refresh_token"; + static const String _keyOnBoarding = "on_boarding_done"; + + // -------------------- LOGIN -------------------- + + static Future setLogin(bool value) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_keyLogin, value); + } + + static Future getLogin() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_keyLogin) ?? false; + } + + // -------------------- USER ID -------------------- + + static Future setUserId(int id) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_keyUserId, id); + } + + static Future getUserId() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getInt(_keyUserId) ?? 0; + } + + // -------------------- ACCESS TOKEN -------------------- + + static Future setAccessToken(String token) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString(_keyAccessToken, token); + } + + static Future getAccessToken() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(_keyAccessToken) ?? ""; + } + + // -------------------- REFRESH TOKEN -------------------- + + static Future setRefreshToken(String token) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString(_keyRefreshToken, token); + } + + static Future getRefreshToken() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(_keyRefreshToken) ?? ""; + } + + // -------------------- ONBOARDING -------------------- + + static Future setOnBoarding(bool value) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_keyOnBoarding, value); + } + + static Future getOnBoarding() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_keyOnBoarding) ?? true; + } + + // -------------------- CLEAR -------------------- + + static Future clearAll() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.clear(); + } +} \ No newline at end of file diff --git a/lib/login/blocs/forgot_password/forgot_password_bloc.dart b/lib/login/blocs/forgot_password/forgot_password_bloc.dart new file mode 100644 index 0000000..656038c --- /dev/null +++ b/lib/login/blocs/forgot_password/forgot_password_bloc.dart @@ -0,0 +1,44 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repositories/forgot_password_repository.dart'; + +part 'forgot_password_event.dart'; +part 'forgot_password_state.dart'; + +class ForgotPasswordBloc extends Bloc { + final ForgotPasswordRepository _forgotPasswordRepository; + + ForgotPasswordBloc({ForgotPasswordRepository? forgotPasswordRepository}) + : _forgotPasswordRepository = forgotPasswordRepository ?? ForgotPasswordRepository(), + super(const ForgotPasswordState()) { + on(_onForgotPasswordSubmitted); + on(_onEmailErrorToggled); + } + + Future _onForgotPasswordSubmitted( + ForgotPasswordSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith(status: ForgotPasswordStatus.loading, errorMessage: null)); + + try { + await _forgotPasswordRepository.forgotPassword( + emailAddress: event.emailAddress, + ); + + emit(state.copyWith(status: ForgotPasswordStatus.success)); + } catch (e) { + emit(state.copyWith( + status: ForgotPasswordStatus.failure, + errorMessage: e.toString().replaceAll('Exception: ', ''), + )); + } + } + + void _onEmailErrorToggled( + ForgotPasswordEmailErrorToggled event, + Emitter emit, + ) { + emit(state.copyWith(showEmailError: event.show)); + } +} \ No newline at end of file diff --git a/lib/login/blocs/forgot_password/forgot_password_event.dart b/lib/login/blocs/forgot_password/forgot_password_event.dart new file mode 100644 index 0000000..eb166c7 --- /dev/null +++ b/lib/login/blocs/forgot_password/forgot_password_event.dart @@ -0,0 +1,25 @@ +part of 'forgot_password_bloc.dart'; + +abstract class ForgotPasswordEvent extends Equatable { + const ForgotPasswordEvent(); + + @override + List get props => []; +} + +class ForgotPasswordSubmitted extends ForgotPasswordEvent { + final String emailAddress; + + const ForgotPasswordSubmitted({required this.emailAddress}); + + @override + List get props => [emailAddress]; +} + +class ForgotPasswordEmailErrorToggled extends ForgotPasswordEvent { + final bool show; + const ForgotPasswordEmailErrorToggled(this.show); + + @override + List get props => [show]; +} \ No newline at end of file diff --git a/lib/login/blocs/forgot_password/forgot_password_state.dart b/lib/login/blocs/forgot_password/forgot_password_state.dart new file mode 100644 index 0000000..efa2fbe --- /dev/null +++ b/lib/login/blocs/forgot_password/forgot_password_state.dart @@ -0,0 +1,30 @@ +part of 'forgot_password_bloc.dart'; + +enum ForgotPasswordStatus { initial, loading, success, failure } + +class ForgotPasswordState extends Equatable { + final ForgotPasswordStatus status; + final String? errorMessage; + final bool showEmailError; + + const ForgotPasswordState({ + this.status = ForgotPasswordStatus.initial, + this.errorMessage, + this.showEmailError = false, + }); + + ForgotPasswordState copyWith({ + ForgotPasswordStatus? status, + String? errorMessage, + bool? showEmailError, + }) { + return ForgotPasswordState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + showEmailError: showEmailError ?? this.showEmailError, + ); + } + + @override + List get props => [status, errorMessage, showEmailError]; +} \ No newline at end of file diff --git a/lib/login/blocs/login/login_bloc.dart b/lib/login/blocs/login/login_bloc.dart new file mode 100644 index 0000000..91a7724 --- /dev/null +++ b/lib/login/blocs/login/login_bloc.dart @@ -0,0 +1,86 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../local_peference/local_preference.dart'; +import '../../models/login.dart'; +import '../../repositories/login_repository.dart'; +part 'login_event.dart'; +part 'login_state.dart'; + +class LoginBloc extends Bloc { + final LoginRepository _loginRepository; + + LoginBloc({LoginRepository? loginRepository}) + : _loginRepository = loginRepository ?? LoginRepository(), + super(const LoginState()) { + on(_onLoginSubmitted); + on(_onPasswordVisibilityToggled); + on(_onRememberMeToggled); + on(_onEmailErrorToggled); + on(_onPasswordErrorToggled); + } + + // ================= LOGIN SUBMITTED ================= + Future _onLoginSubmitted( + LoginSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith(status: LoginStatus.loading, errorMessage: null)); + + try { + final LoginModel loginData = await _loginRepository.login( + emailAddress: event.emailAddress, + password: event.password, + rememberMe: event.rememberMe, + ); + + // ── Save to local preference ────────────────────────────────────── + await Future.wait([ + LocalPreference.setAccessToken(loginData.accessToken), + LocalPreference.setRefreshToken(loginData.refreshToken), + LocalPreference.setUserId(loginData.partner.id), + LocalPreference.setLogin(true), + ]); + // ───────────────────────────────────────────────────────────────── + + emit(state.copyWith( + status: LoginStatus.success, + loginData: loginData, + )); + } catch (e) { + emit(state.copyWith( + status: LoginStatus.failure, + errorMessage: e.toString().replaceAll('Exception: ', ''), + )); + } + } + + // ================= PASSWORD VISIBILITY ================= + void _onPasswordVisibilityToggled( + LoginPasswordVisibilityToggled event, + Emitter emit, + ) { + emit(state.copyWith(isPasswordVisible: !state.isPasswordVisible)); + } + + // ================= REMEMBER ME ================= + void _onRememberMeToggled( + LoginRememberMeToggled event, + Emitter emit, + ) { + emit(state.copyWith(rememberMe: event.value)); + } + + void _onEmailErrorToggled( + LoginEmailErrorToggled event, + Emitter emit, + ) { + emit(state.copyWith(showEmailError: event.show)); + } + + void _onPasswordErrorToggled( + LoginPasswordErrorToggled event, + Emitter emit, + ) { + emit(state.copyWith(showPasswordError: event.show)); + } +} \ No newline at end of file diff --git a/lib/login/blocs/login/login_event.dart b/lib/login/blocs/login/login_event.dart new file mode 100644 index 0000000..05f5356 --- /dev/null +++ b/lib/login/blocs/login/login_event.dart @@ -0,0 +1,51 @@ +part of 'login_bloc.dart'; + +abstract class LoginEvent extends Equatable { + const LoginEvent(); + + @override + List get props => []; +} + +class LoginSubmitted extends LoginEvent { + final String emailAddress; + final String password; + final bool rememberMe; + + const LoginSubmitted({ + required this.emailAddress, + required this.password, + this.rememberMe = false, + }); + + @override + List get props => [emailAddress, password, rememberMe]; +} + +class LoginPasswordVisibilityToggled extends LoginEvent { + const LoginPasswordVisibilityToggled(); +} + +class LoginRememberMeToggled extends LoginEvent { + final bool value; + const LoginRememberMeToggled(this.value); + + @override + List get props => [value]; +} + +class LoginEmailErrorToggled extends LoginEvent { + final bool show; + const LoginEmailErrorToggled(this.show); + + @override + List get props => [show]; +} + +class LoginPasswordErrorToggled extends LoginEvent { + final bool show; + const LoginPasswordErrorToggled(this.show); + + @override + List get props => [show]; +} \ No newline at end of file diff --git a/lib/login/blocs/login/login_state.dart b/lib/login/blocs/login/login_state.dart new file mode 100644 index 0000000..869b6ad --- /dev/null +++ b/lib/login/blocs/login/login_state.dart @@ -0,0 +1,54 @@ +part of 'login_bloc.dart'; + +enum LoginStatus { initial, loading, success, failure } + +class LoginState extends Equatable { + final LoginStatus status; + final bool isPasswordVisible; + final bool rememberMe; + final String? errorMessage; + final LoginModel? loginData; + final bool showEmailError; + final bool showPasswordError; + + const LoginState({ + this.status = LoginStatus.initial, + this.isPasswordVisible = false, + this.rememberMe = false, + this.errorMessage, + this.loginData, + this.showEmailError = false, + this.showPasswordError = false, + }); + + LoginState copyWith({ + LoginStatus? status, + bool? isPasswordVisible, + bool? rememberMe, + String? errorMessage, + LoginModel? loginData, + bool? showEmailError, + bool? showPasswordError, + }) { + return LoginState( + status: status ?? this.status, + isPasswordVisible: isPasswordVisible ?? this.isPasswordVisible, + rememberMe: rememberMe ?? this.rememberMe, + errorMessage: errorMessage ?? this.errorMessage, + loginData: loginData ?? this.loginData, + showEmailError: showEmailError ?? this.showEmailError, + showPasswordError: showPasswordError ?? this.showPasswordError, + ); + } + + @override + List get props => [ + status, + isPasswordVisible, + rememberMe, + errorMessage, + loginData, + showEmailError, + showPasswordError, + ]; +} \ No newline at end of file diff --git a/lib/login/blocs/reset_password/reset_password_bloc.dart b/lib/login/blocs/reset_password/reset_password_bloc.dart new file mode 100644 index 0000000..f9f20aa --- /dev/null +++ b/lib/login/blocs/reset_password/reset_password_bloc.dart @@ -0,0 +1,93 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repositories/reset_password_repository.dart'; + +part 'reset_password_event.dart'; +part 'reset_password_state.dart'; + +class ResetPasswordBloc extends Bloc { + final ResetPasswordRepository _resetPasswordRepository; + + ResetPasswordBloc({ResetPasswordRepository? resetPasswordRepository}) + : _resetPasswordRepository = + resetPasswordRepository ?? ResetPasswordRepository(), + super(const ResetPasswordState()) { + on(_onResetPasswordSubmitted); + on(_onResetPasswordVisibilityToggled); + on(_onConfirmPasswordVisibilityToggled); + on(_onPasswordChanged); + on(_onPasswordErrorToggled); + on(_onConfirmPasswordErrorToggled); + } + + Future _onResetPasswordSubmitted( + ResetPasswordSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith( + status: ResetPasswordStatus.loading, errorMessage: null)); + + try { + await _resetPasswordRepository.resetPassword( + emailAddress: event.emailAddress, + newPassword: event.newPassword, + ); + + emit(state.copyWith(status: ResetPasswordStatus.success)); + } catch (e) { + emit(state.copyWith( + status: ResetPasswordStatus.failure, + errorMessage: e.toString().replaceAll('Exception: ', ''), + )); + } + } + + void _onResetPasswordVisibilityToggled( + ResetPasswordVisibilityToggled event, + Emitter emit, + ) { + emit(state.copyWith(isPasswordVisible: !state.isPasswordVisible)); + } + + void _onConfirmPasswordVisibilityToggled( + ConfirmPasswordVisibilityToggled event, + Emitter emit, + ) { + emit(state.copyWith( + isConfirmPasswordVisible: !state.isConfirmPasswordVisible)); + } + + void _onPasswordChanged( + PasswordChanged event, + Emitter emit, + ) { + final password = event.password; + emit(state.copyWith( + hasMinLength: password.length >= 8, + hasUppercase: password.contains(RegExp(r'[A-Z]')), + hasLowercase: password.contains(RegExp(r'[a-z]')), + hasNumber: password.contains(RegExp(r'[0-9]')), + hasSpecial: password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')), + )); + } + + void _onPasswordErrorToggled( + ResetPasswordErrorToggled event, + Emitter emit, + ) { + emit(state.copyWith( + showPasswordError: event.show, + passwordErrorText: event.error, + )); + } + + void _onConfirmPasswordErrorToggled( + ResetConfirmPasswordErrorToggled event, + Emitter emit, + ) { + emit(state.copyWith( + showConfirmPasswordError: event.show, + confirmPasswordErrorText: event.error, + )); + } +} diff --git a/lib/login/blocs/reset_password/reset_password_event.dart b/lib/login/blocs/reset_password/reset_password_event.dart new file mode 100644 index 0000000..7b8b9ca --- /dev/null +++ b/lib/login/blocs/reset_password/reset_password_event.dart @@ -0,0 +1,55 @@ +part of 'reset_password_bloc.dart'; + +abstract class ResetPasswordEvent extends Equatable { + const ResetPasswordEvent(); + + @override + List get props => []; +} + +class ResetPasswordSubmitted extends ResetPasswordEvent { + final String emailAddress; + final String newPassword; + + const ResetPasswordSubmitted({ + required this.emailAddress, + required this.newPassword, + }); + + @override + List get props => [emailAddress, newPassword]; +} + +class ResetPasswordVisibilityToggled extends ResetPasswordEvent { + const ResetPasswordVisibilityToggled(); +} + +class ConfirmPasswordVisibilityToggled extends ResetPasswordEvent { + const ConfirmPasswordVisibilityToggled(); +} + +class PasswordChanged extends ResetPasswordEvent { + final String password; + const PasswordChanged(this.password); + + @override + List get props => [password]; +} + +class ResetPasswordErrorToggled extends ResetPasswordEvent { + final bool show; + final String? error; + const ResetPasswordErrorToggled(this.show, {this.error}); + + @override + List get props => [show, error]; +} + +class ResetConfirmPasswordErrorToggled extends ResetPasswordEvent { + final bool show; + final String? error; + const ResetConfirmPasswordErrorToggled(this.show, {this.error}); + + @override + List get props => [show, error]; +} \ No newline at end of file diff --git a/lib/login/blocs/reset_password/reset_password_state.dart b/lib/login/blocs/reset_password/reset_password_state.dart new file mode 100644 index 0000000..c135efa --- /dev/null +++ b/lib/login/blocs/reset_password/reset_password_state.dart @@ -0,0 +1,90 @@ +part of 'reset_password_bloc.dart'; + +enum ResetPasswordStatus { initial, loading, success, failure } + +class ResetPasswordState extends Equatable { + final ResetPasswordStatus status; + final bool isPasswordVisible; + final bool isConfirmPasswordVisible; + final String? errorMessage; + + // Validation flags + final bool hasMinLength; + final bool hasUppercase; + final bool hasLowercase; + final bool hasNumber; + final bool hasSpecial; + + final bool showPasswordError; + final bool showConfirmPasswordError; + final String? passwordErrorText; + final String? confirmPasswordErrorText; + + const ResetPasswordState({ + this.status = ResetPasswordStatus.initial, + this.isPasswordVisible = false, + this.isConfirmPasswordVisible = false, + this.errorMessage, + this.hasMinLength = false, + this.hasUppercase = false, + this.hasLowercase = false, + this.hasNumber = false, + this.hasSpecial = false, + this.showPasswordError = false, + this.showConfirmPasswordError = false, + this.passwordErrorText, + this.confirmPasswordErrorText, + }); + + bool get isPasswordValid => + hasMinLength && hasUppercase && hasLowercase && hasNumber && hasSpecial; + + ResetPasswordState copyWith({ + ResetPasswordStatus? status, + bool? isPasswordVisible, + bool? isConfirmPasswordVisible, + String? errorMessage, + bool? hasMinLength, + bool? hasUppercase, + bool? hasLowercase, + bool? hasNumber, + bool? hasSpecial, + bool? showPasswordError, + bool? showConfirmPasswordError, + String? passwordErrorText, + String? confirmPasswordErrorText, + }) { + return ResetPasswordState( + status: status ?? this.status, + isPasswordVisible: isPasswordVisible ?? this.isPasswordVisible, + isConfirmPasswordVisible: isConfirmPasswordVisible ?? this.isConfirmPasswordVisible, + errorMessage: errorMessage ?? this.errorMessage, + hasMinLength: hasMinLength ?? this.hasMinLength, + hasUppercase: hasUppercase ?? this.hasUppercase, + hasLowercase: hasLowercase ?? this.hasLowercase, + hasNumber: hasNumber ?? this.hasNumber, + hasSpecial: hasSpecial ?? this.hasSpecial, + showPasswordError: showPasswordError ?? this.showPasswordError, + showConfirmPasswordError: showConfirmPasswordError ?? this.showConfirmPasswordError, + passwordErrorText: passwordErrorText ?? this.passwordErrorText, + confirmPasswordErrorText: confirmPasswordErrorText ?? this.confirmPasswordErrorText, + ); + } + + @override + List get props => [ + status, + isPasswordVisible, + isConfirmPasswordVisible, + errorMessage, + hasMinLength, + hasUppercase, + hasLowercase, + hasNumber, + hasSpecial, + showPasswordError, + showConfirmPasswordError, + passwordErrorText, + confirmPasswordErrorText, + ]; +} \ No newline at end of file diff --git a/lib/login/blocs/verify_otp/verify_otp_bloc.dart b/lib/login/blocs/verify_otp/verify_otp_bloc.dart new file mode 100644 index 0000000..242e871 --- /dev/null +++ b/lib/login/blocs/verify_otp/verify_otp_bloc.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../repositories/otp_repository.dart'; + +part 'verify_otp_event.dart'; +part 'verify_otp_state.dart'; + +class VerifyOtpBloc extends Bloc { + final OtpRepository _otpRepository; + + VerifyOtpBloc({OtpRepository? otpRepository}) + : _otpRepository = otpRepository ?? OtpRepository(), + super(const VerifyOtpState()) { + on(_onVerifyOtpSubmitted); + } + + Future _onVerifyOtpSubmitted( + VerifyOtpSubmitted event, + Emitter emit, + ) async { + emit(state.copyWith(status: VerifyOtpStatus.loading, errorMessage: null)); + + try { + await _otpRepository.verifyOtp( + emailAddress: event.emailAddress, + otp: event.otp, + ); + + emit(state.copyWith(status: VerifyOtpStatus.success)); + } catch (e) { + emit(state.copyWith( + status: VerifyOtpStatus.failure, + errorMessage: e.toString().replaceAll('Exception: ', ''), + )); + } + } +} \ No newline at end of file diff --git a/lib/login/blocs/verify_otp/verify_otp_event.dart b/lib/login/blocs/verify_otp/verify_otp_event.dart new file mode 100644 index 0000000..0abe38d --- /dev/null +++ b/lib/login/blocs/verify_otp/verify_otp_event.dart @@ -0,0 +1,21 @@ +part of 'verify_otp_bloc.dart'; + +abstract class VerifyOtpEvent extends Equatable { + const VerifyOtpEvent(); + + @override + List get props => []; +} + +class VerifyOtpSubmitted extends VerifyOtpEvent { + final String emailAddress; + final String otp; + + const VerifyOtpSubmitted({ + required this.emailAddress, + required this.otp, + }); + + @override + List get props => [emailAddress, otp]; +} \ No newline at end of file diff --git a/lib/login/blocs/verify_otp/verify_otp_state.dart b/lib/login/blocs/verify_otp/verify_otp_state.dart new file mode 100644 index 0000000..e8d2522 --- /dev/null +++ b/lib/login/blocs/verify_otp/verify_otp_state.dart @@ -0,0 +1,26 @@ +part of 'verify_otp_bloc.dart'; + +enum VerifyOtpStatus { initial, loading, success, failure } + +class VerifyOtpState extends Equatable { + final VerifyOtpStatus status; + final String? errorMessage; + + const VerifyOtpState({ + this.status = VerifyOtpStatus.initial, + this.errorMessage, + }); + + VerifyOtpState copyWith({ + VerifyOtpStatus? status, + String? errorMessage, + }) { + return VerifyOtpState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, errorMessage]; +} \ No newline at end of file diff --git a/lib/login/models/login.dart b/lib/login/models/login.dart index e69de29..eb7d6b5 100644 --- a/lib/login/models/login.dart +++ b/lib/login/models/login.dart @@ -0,0 +1,63 @@ +class LoginModel { + final String accessToken; + final String refreshToken; + final int refreshMaxAge; + final PartnerModel partner; + + const LoginModel({ + required this.accessToken, + required this.refreshToken, + required this.refreshMaxAge, + required this.partner, + }); + + factory LoginModel.fromJson(Map json) { + return LoginModel( + accessToken: json['accessToken'] ?? '', + refreshToken: json['refreshToken'] ?? '', + refreshMaxAge: json['refreshMaxAge'] ?? 0, + partner: PartnerModel.fromJson(json['partner']), + ); + } + + Map toJson() { + return { + 'accessToken': accessToken, + 'refreshToken': refreshToken, + 'refreshMaxAge': refreshMaxAge, + 'partner': partner.toJson(), + }; + } +} + +class PartnerModel { + final int id; + final String email; + final String name; + final int roleXid; + + const PartnerModel({ + required this.id, + required this.email, + required this.name, + required this.roleXid, + }); + + factory PartnerModel.fromJson(Map json) { + return PartnerModel( + id: json['id'] ?? 0, + email: json['email'] ?? '', + name: json['name'] ?? '', + roleXid: json['roleXid'] ?? 0, + ); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + 'name': name, + 'roleXid': roleXid, + }; + } +} \ No newline at end of file diff --git a/lib/login/repositories/forgot_password_repository.dart b/lib/login/repositories/forgot_password_repository.dart index e69de29..27dbcb6 100644 --- a/lib/login/repositories/forgot_password_repository.dart +++ b/lib/login/repositories/forgot_password_repository.dart @@ -0,0 +1,17 @@ +import '../../network_api_service/api_service/api_service.dart'; +import '../../network_api_service/api_urls/api_urls.dart'; + +class ForgotPasswordRepository { + final ApiService _apiService = ApiService(); + + Future forgotPassword({required String emailAddress}) async { + try { + await _apiService.post( + ApiUrls.forgotPassword, + data: {"emailAddress": emailAddress}, + ); + } catch (e) { + throw Exception('Failed to send forgot password request: $e'); + } + } +} \ No newline at end of file diff --git a/lib/login/repositories/login_repository.dart b/lib/login/repositories/login_repository.dart index e69de29..d8f9e88 100644 --- a/lib/login/repositories/login_repository.dart +++ b/lib/login/repositories/login_repository.dart @@ -0,0 +1,28 @@ +import '../../network_api_service/api_service/api_service.dart'; +import '../../network_api_service/api_urls/api_urls.dart'; +import '../models/login.dart'; + +class LoginRepository { + final ApiService _apiService = ApiService(); + + Future login({ + required String emailAddress, + required String password, + bool rememberMe = false, + }) async { + try { + final response = await _apiService.post( + ApiUrls.login, + data: { + "emailAddress": emailAddress, + "password": password, + "rememberMe": rememberMe, + }, + ); + + return LoginModel.fromJson(response.data as Map); + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/login/repositories/otp_repository.dart b/lib/login/repositories/otp_repository.dart index e69de29..8dd3b4c 100644 --- a/lib/login/repositories/otp_repository.dart +++ b/lib/login/repositories/otp_repository.dart @@ -0,0 +1,23 @@ +import '../../network_api_service/api_service/api_service.dart'; +import '../../network_api_service/api_urls/api_urls.dart'; + +class OtpRepository { + final ApiService _apiService = ApiService(); + + Future verifyOtp({ + required String emailAddress, + required String otp, + }) async { + try { + await _apiService.post( + ApiUrls.verifyOtp, + data: { + "emailAddress": emailAddress, + "otp": otp, + }, + ); + } catch (e) { + throw Exception('Failed to verify OTP: $e'); + } + } +} \ No newline at end of file diff --git a/lib/login/repositories/reset_password_repository.dart b/lib/login/repositories/reset_password_repository.dart index e69de29..c35e3b4 100644 --- a/lib/login/repositories/reset_password_repository.dart +++ b/lib/login/repositories/reset_password_repository.dart @@ -0,0 +1,23 @@ +import '../../network_api_service/api_service/api_service.dart'; +import '../../network_api_service/api_urls/api_urls.dart'; + +class ResetPasswordRepository { + final ApiService _apiService = ApiService(); + + Future resetPassword({ + required String emailAddress, + required String newPassword, + }) async { + try { + await _apiService.post( + ApiUrls.resetPassword, + data: { + "emailAddress": emailAddress, + "newPassword": newPassword, + }, + ); + } catch (e) { + throw Exception('Failed to reset password: $e'); + } + } +} \ No newline at end of file diff --git a/lib/login/views/forgot_password_page.dart b/lib/login/views/forgot_password_page.dart index ba3703c..35c62b7 100644 --- a/lib/login/views/forgot_password_page.dart +++ b/lib/login/views/forgot_password_page.dart @@ -1,241 +1,178 @@ -import 'dart:ui'; +import 'package:citycards_partner_flutter/constants/app_assets.dart'; +import 'package:citycards_partner_flutter/constants/app_colors.dart'; import 'package:citycards_partner_flutter/core/app_router.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../blocs/forgot_password_bloc.dart'; +import '../../custome_widgets/custom_button.dart'; +import '../../custome_widgets/custom_textfield.dart'; +import '../blocs/forgot_password/forgot_password_bloc.dart'; -class ForgotPasswordPage extends StatelessWidget { +class ForgotPasswordPage extends StatefulWidget { const ForgotPasswordPage({super.key}); @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ForgotPasswordBloc(), - child: const _ForgotPasswordView(), - ); - } + State createState() => _ForgotPasswordPageState(); } -class _ForgotPasswordView extends StatelessWidget { - const _ForgotPasswordView(); +class _ForgotPasswordPageState extends State { + final _emailController = TextEditingController(); + final _emailFocusNode = FocusNode(); + + @override + void dispose() { + _emailController.dispose(); + _emailFocusNode.dispose(); + super.dispose(); + } + + bool _isEmailValid(String email) { + return RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(email); + } + + void _onSendLinkPressed(BuildContext context) { + FocusManager.instance.primaryFocus?.unfocus(); + + final email = _emailController.text.trim(); + final isEmailValid = _isEmailValid(email); + + context.read().add(ForgotPasswordEmailErrorToggled(!isEmailValid)); + + if (isEmailValid) { + context.read().add( + ForgotPasswordSubmitted(emailAddress: email), + ); + } + } @override Widget build(BuildContext context) { - final bloc = context.read(); - - return Scaffold( - body: Stack( - children: [ - // Background - Positioned.fill( - child: Image.asset( - 'assets/login/bg.png', - fit: BoxFit.cover, - ), - ), - - // Gradient Overlay - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.6), - Colors.black.withOpacity(1.0), - ], + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + behavior: HitTestBehavior.translucent, + child: Scaffold( + backgroundColor: AppColors.backgroundWhite, + body: BlocConsumer( + listener: (context, state) { + if (state.status == ForgotPasswordStatus.success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("OTP sent successfully!"), + backgroundColor: AppColors.successGreen, ), - ), - ), - ), + ); + Navigator.pushNamed( + context, + AppRouter.otpVerification, + arguments: _emailController.text.trim(), + ); + } else if (state.status == ForgotPasswordStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? "An error occurred"), + backgroundColor: Colors.redAccent, + ), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == ForgotPasswordStatus.loading; - // Foreground content - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: BlocConsumer( - listener: (context, state) { - if (state.isSuccess) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Reset link sent successfully!"), - backgroundColor: Colors.green, - ), - ); - Navigator.pushNamed(context, AppRouter.otpVerification); - } else if (state.message.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: Colors.redAccent, - ), - ); - } - }, - builder: (context, state) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 160), - - // Glass Card - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Align( - alignment: Alignment.center, - child: Image.asset( - "assets/login/app_icon.png", - scale: 4, + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 40.h), + // ===== LOGO SECTION ===== + Center( + child: Column( + children: [ + Image.asset( + AppAssets.appIcon, + height: 60.h, ), - ), - const SizedBox(height: 24), - Text( - "Forgot Password", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - "Forgot your password? Don’t worry — just enter your email and we’ll help you reset it.", - textAlign: TextAlign.start, - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 13, - ), - ), - const SizedBox(height: 24), - - // Email Field - TextField( - style: const TextStyle(color: Colors.white), - onChanged: (value) => - bloc.add(EmailChanged(value)), - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.email_outlined, - color: Colors.white70, - ), - hintText: 'Enter your email address', - hintStyle: const TextStyle( - color: Colors.white54), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: Colors.white54), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: Colors.white), + SizedBox(height: 8.h), + Text( + "Partner’s App", + style: GoogleFonts.poppins( + color: AppColors.primaryRed, + fontSize: 20.sp, + fontWeight: FontWeight.w500, ), ), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ), - - const SizedBox(height: 148), - - // SizedBox( - // width: double.infinity, - // child: ElevatedButton( - // onPressed: state.isValidEmail && !state.isLoading - // ? () => bloc.add(SendResetLink()) - // : null, - // style: ButtonStyle( - // backgroundColor: - // MaterialStateProperty.resolveWith( - // (states) { - // if (states.contains(MaterialState.disabled)) { - // return const Color(0xFF9C3F42); - // } - // return const Color(0xFFFF4C4C); - // }, - // ), - // padding: MaterialStateProperty.all( - // const EdgeInsets.symmetric(vertical: 14), - // ), - // shape: MaterialStateProperty.all( - // RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(12), - // ), - // ), - // ), - // child: Text( - // "Send Reset Link", - // style: GoogleFonts.poppins( - // color: state.isValidEmail?Colors.white:Color(0xff9D9F9F), - // fontSize: 16, - // fontWeight: FontWeight.w600, - // ), - // ), - // ), - // ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => bloc.add(SendResetLink()), - style: ButtonStyle( - - backgroundColor: - MaterialStateProperty.resolveWith( - (states) { - if (states.contains(MaterialState.disabled)) { - return const Color(0xFFFF4C4C); - } - return const Color(0xFFFF4C4C); - }, - ), - - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric(vertical: 14), - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + ], ), ), - child: Text( - "Send Reset Link", + SizedBox(height: 60.h), + + // ===== HEADER TEXT ===== + Text( + "Change Your Password", + textAlign: TextAlign.center, style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 16, + color: AppColors.black, + fontSize: 24.sp, fontWeight: FontWeight.w600, ), ), - ), + SizedBox(height: 12.h), + Text( + "Enter your email to update your password\nand secure your account", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: AppColors.textGrey, + fontSize: 16.sp, + height: 1.4, + ), + ), + SizedBox(height: 48.h), + + // ===== EMAIL FIELD ===== + CustomTextField( + label: 'Email Address', + hintText: 'you@example.com', + controller: _emailController, + focusNode: _emailFocusNode, + prefixIcon: Icons.email_outlined, + hasError: state.showEmailError, + errorText: 'Please enter a valid email address', + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + readOnly: isLoading, + onChanged: (val) { + if (state.showEmailError) { + context.read().add( + ForgotPasswordEmailErrorToggled(!_isEmailValid(val.trim())), + ); + } + }, + onSubmitted: (_) => _onSendLinkPressed(context), + ), + ], ), ), - ], - ); - }, + ), + + // ===== SEND LINK BUTTON ===== + CustomButton( + text: "Send OTP", + isLoading: isLoading, + onPressed: () => _onSendLinkPressed(context), + ), + SizedBox(height: 24.h), + ], + ), ), - ), - ), - ], + ); + }, + ), ), ); } diff --git a/lib/login/views/login_page.dart b/lib/login/views/login_page.dart index bf63857..26d41ab 100644 --- a/lib/login/views/login_page.dart +++ b/lib/login/views/login_page.dart @@ -1,8 +1,13 @@ -import 'dart:ui'; - import 'package:citycards_partner_flutter/core/app_router.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../constants/app_assets.dart'; +import '../../constants/app_colors.dart'; +import '../../custome_widgets/custom_button.dart'; +import '../../custome_widgets/custom_textfield.dart'; +import '../blocs/login/login_bloc.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -12,222 +17,278 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { - bool _isPasswordVisible = false; - bool _rememberMe = false; + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _emailFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + _emailFocusNode.dispose(); + _passwordFocusNode.dispose(); + super.dispose(); + } + + bool _isEmailValid(String email) { + return RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") + .hasMatch(email); + } + + void _onLoginPressed(BuildContext context) { + FocusManager.instance.primaryFocus?.unfocus(); + + final email = _emailController.text.trim(); + final password = _passwordController.text.trim(); + + final isEmailValid = _isEmailValid(email); + final isPasswordValid = password.length >= 8; + + context.read().add(LoginEmailErrorToggled(!isEmailValid)); + context.read().add(LoginPasswordErrorToggled(!isPasswordValid)); + + if (isEmailValid && isPasswordValid) { + context.read().add( + LoginSubmitted( + emailAddress: email, + password: password, + rememberMe: context.read().state.rememberMe, + ), + ); + } + } @override Widget build(BuildContext context) { - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset( - 'assets/login/bg.png', - fit: BoxFit.cover, - ), - ), - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.6), - Colors.black.withOpacity(1.0), - ], - // stops: const [0.5, 1.0], - ), - ), - ), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 80), - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Container( - padding: const EdgeInsets.all(24), + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + behavior: HitTestBehavior.translucent, + child: Scaffold( + backgroundColor: AppColors.backgroundWhite, + resizeToAvoidBottomInset: true, + body: BlocConsumer( + listener: (context, state) { + if (state.status == LoginStatus.success) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRouter.qrScanScreen, + (route) => false, // removes all previous routes + ); + } + }, + builder: (context, state) { + final isLoading = state.status == LoginStatus.loading; + + return SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 40.h), + + // ===== LOGO SECTION ===== + Center( + child: Column( + children: [ + Image.asset( + AppAssets.appIcon, + height: 60.h, + ), + SizedBox(height: 8.h), + Text( + "Partner's App", + style: GoogleFonts.poppins( + color: AppColors.primaryRed, + fontSize: 20.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 60.h), + + // ===== WELCOME TEXT ===== + Center( + child: Column( + children: [ + Text( + "Welcome Back", + style: GoogleFonts.poppins( + color: AppColors.black, + fontSize: 24.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + Text( + "Sign in to your account", + style: GoogleFonts.poppins( + color: AppColors.textGrey, + fontSize: 16.sp, + ), + ), + ], + ), + ), + SizedBox(height: 32.h), + + // ===== ERROR BANNER ===== + if (state.status == LoginStatus.failure) + Container( + margin: EdgeInsets.only(bottom: 24.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(20), + color: AppColors.errorLight, + borderRadius: BorderRadius.circular(12.r), + border: + Border.all(color: AppColors.primaryRed.withOpacity(0.5)), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Align( - alignment: AlignmentGeometry.center, - child: Image.asset("assets/login/app_icon.png",scale: 4,)), - const SizedBox(height: 24), - Text( - "Enter your email", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 8), - TextField( - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: '', - hintStyle: const TextStyle(color: Colors.white54), - prefixIcon: const Icon( - Icons.email_outlined, - color: Colors.white70, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white54), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white), + Icon(Icons.error_outline, + color: AppColors.primaryRed, size: 20.sp), + SizedBox(width: 12.w), + Expanded( + child: Text( + state.errorMessage ?? + "Invalid email or password. Please try again.", + style: GoogleFonts.poppins( + color: AppColors.primaryRed, + fontSize: 13.sp, + fontWeight: FontWeight.w400, ), ), ), - const SizedBox(height: 20), - - Text( - "Enter your password", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 14, - ), - ), - const SizedBox(height: 8), - TextField( - obscureText: !_isPasswordVisible, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - hintText: '', - hintStyle: const TextStyle(color: Colors.white54), - prefixIcon: const Icon( - Icons.lock_outline, - color: Colors.white70, - ), - suffixIcon: IconButton( - icon: Icon( - _isPasswordVisible - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - color: Colors.white70, - ), - onPressed: () { - setState(() { - _isPasswordVisible = !_isPasswordVisible; - }); - }, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white54), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white), - ), - ), - ), - - const SizedBox(height: 12), - - // Remember me + Forgot password - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _rememberMe = !_rememberMe; - }); - }, - child: Container( - height: 18, - width: 18, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(3), - border: Border.all(color: Colors.white), - color: _rememberMe - ? Colors.white - : Colors.transparent, - ), - ), - ), - const SizedBox(width: 8), - Text( - "Remember me", - style: GoogleFonts.poppins( - color: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - ], - ), - GestureDetector( - onTap: () { - Navigator.pushNamed( - context, AppRouter.forgotPassword); - }, - child: Text( - "Forgot Password?", - style: GoogleFonts.poppins( - color: const Color(0xFFFF4C4C), - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), ], ), ), - ), - ), - const SizedBox(height: 68), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pushNamed(context, AppRouter.forgotPassword); + + // ===== EMAIL FIELD ===== + CustomTextField( + label: 'Email Address', + hintText: 'Enter your email', + controller: _emailController, + focusNode: _emailFocusNode, + prefixIcon: Icons.email_outlined, + hasError: state.showEmailError, + errorText: 'Please enter a valid email address', + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + readOnly: isLoading, + onChanged: (val) { + if (state.showEmailError) { + context.read().add( + LoginEmailErrorToggled(!_isEmailValid(val.trim())), + ); + } }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFFF4C4C), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Text( - "Log in", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), ), - ), - ], + SizedBox(height: 20.h), + + // ===== PASSWORD FIELD ===== + CustomTextField( + label: 'Password', + hintText: 'Enter your password', + controller: _passwordController, + focusNode: _passwordFocusNode, + prefixIcon: Icons.lock_outline, + isPassword: true, + isPasswordVisible: state.isPasswordVisible, + onTogglePasswordVisibility: () { + context.read().add( + const LoginPasswordVisibilityToggled(), + ); + }, + hasError: state.showPasswordError, + errorText: 'Password must be at least 8 characters.', + textInputAction: TextInputAction.done, + readOnly: isLoading, + onChanged: (val) { + if (state.showPasswordError) { + context.read().add( + LoginPasswordErrorToggled(val.length < 8), + ); + } + }, + onSubmitted: (_) => _onLoginPressed(context), + ), + SizedBox(height: 16.h), + + // ===== REMEMBER ME + FORGOT PASSWORD ===== + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + SizedBox( + height: 24.h, + width: 24.w, + child: Checkbox( + value: state.rememberMe, + onChanged: isLoading + ? null + : (value) { + context.read().add( + LoginRememberMeToggled( + value ?? false), + ); + }, + activeColor: AppColors.primaryRed, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4.r), + ), + side: BorderSide(color: Colors.grey[300]!), + ), + ), + SizedBox(width: 8.w), + Text( + "Remember me", + style: GoogleFonts.poppins( + color: AppColors.textGrey, + fontSize: 14.sp, + ), + ), + ], + ), + GestureDetector( + onTap: isLoading + ? null + : () { + Navigator.pushNamed( + context, + AppRouter.forgotPassword, + ); + }, + child: Text( + "Forgot Password?", + style: GoogleFonts.poppins( + color: AppColors.primaryRed, + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + SizedBox(height: 80.h), + + // ===== LOGIN BUTTON ===== + CustomButton( + text: "Log in", + isLoading: isLoading, + onPressed: () => _onLoginPressed(context), + ), + SizedBox(height: 24.h), + ], + ), ), - ), - ), - ], + ); + }, + ), ), ); } diff --git a/lib/login/views/otp_verification_page.dart b/lib/login/views/otp_verification_page.dart index 2f52ec5..a09316c 100644 --- a/lib/login/views/otp_verification_page.dart +++ b/lib/login/views/otp_verification_page.dart @@ -1,192 +1,165 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_otp_text_field/flutter_otp_text_field.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../constants/app_assets.dart'; +import '../../constants/app_colors.dart'; import '../../core/app_router.dart'; -import '../blocs/otp_bloc.dart'; +import '../../custome_widgets/custom_button.dart'; +import '../blocs/verify_otp/verify_otp_bloc.dart'; -class OtpVerificationPage extends StatelessWidget { - const OtpVerificationPage({super.key}); +class OtpVerificationPage extends StatefulWidget { + final String email; + + const OtpVerificationPage({super.key, required this.email}); + + @override + State createState() => _OtpVerificationPageState(); +} + +class _OtpVerificationPageState extends State { + String _otp = ""; @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => OtpBloc(), + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + behavior: HitTestBehavior.translucent, child: Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/login/bg.png', fit: BoxFit.cover), - ), - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.6), - Colors.black.withOpacity(1.0), - ], - ), + backgroundColor: AppColors.backgroundWhite, + body: BlocConsumer( + listener: (context, state) { + if (state.status == VerifyOtpStatus.success) { + Navigator.pushReplacementNamed( + context, + AppRouter.resetPassword, + arguments: widget.email, + ); + } else if (state.status == VerifyOtpStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? "Verification failed"), + backgroundColor: Colors.redAccent, ), - ), - ), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == VerifyOtpStatus.loading; - // Foreground content - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 18), + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), child: Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 80), - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.4), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.2), - ), - ), - child: BlocConsumer( - listener: (context, state) { - if (state.isVerified) { - Navigator.pushNamed(context, AppRouter.resetPassword); - } else if (state.message.isNotEmpty && - !state.isLoading) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - backgroundColor: state.isVerified - ? Colors.green - : Colors.redAccent, - ), - ); - } - }, - builder: (context, state) { - final otpBloc = context.read(); - return Column( + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 40.h), + // ===== LOGO SECTION ===== + Center( + child: Column( children: [ - Align( - alignment: Alignment.center, - child: Image.asset( - "assets/login/app_icon.png", - scale: 4, - ), + Image.asset( + AppAssets.appIcon, + height: 60.h, ), - const SizedBox(height: 24), + SizedBox(height: 8.h), Text( - "Verify OTP", + "Partner’s App", style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 24, + color: AppColors.primaryRed, + fontSize: 20.sp, fontWeight: FontWeight.w500, ), ), - const SizedBox(height: 8), - Text( - "We’ve sent an OTP to your registered email. Please enter it below, or use the reset link included in the email.", - textAlign: TextAlign.start, - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 30), - - // OTP input fields - OtpTextField( - borderRadius: BorderRadius.all( - Radius.circular(6), - ), - numberOfFields: 6, - fillColor: Color(0xff242628), - cursorColor: Colors.white, - borderColor: Colors.white, - focusedBorderColor: const Color(0xFFFF4C4C), - showFieldAsBox: true, - fieldWidth: 45, - textStyle: const TextStyle( - color: Colors.white, - ), - onSubmit: (value) { - otpBloc.add(OtpChanged(value)); - otpBloc.add(OtpVerify()); - }, - onCodeChanged: (value) { - otpBloc.add(OtpChanged(value)); - }, - ), - const SizedBox(height: 24), ], - ); - }, - ), - ), - ), - ), - const SizedBox(height: 60), - BlocBuilder( - builder: (context, state) { - final otpBloc = context.read(); - return SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: state.isOtpFilled && !state.isLoading - ? () => otpBloc.add(OtpVerify()) - : null, - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.resolveWith(( - states, - ) { - if (states.contains( - MaterialState.disabled, - )) { - return const Color( - 0xFF9C3F42, - ); // 👈 custom disabled color - } - return const Color( - 0xFFFF4C4C, - ); // 👈 active color - }), - padding: MaterialStateProperty.all( - const EdgeInsets.symmetric(vertical: 14), - ), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), ), ), - child: Text( - "Verify", + SizedBox(height: 60.h), + + // ===== HEADER TEXT ===== + Text( + "Verify OTP", + textAlign: TextAlign.center, style: GoogleFonts.poppins( - color: state.isOtpFilled?Colors.white:Color(0xff9D9F9F), - fontSize: 16, + color: AppColors.black, + fontSize: 24.sp, fontWeight: FontWeight.w600, ), ), - ), - ); - }, + SizedBox(height: 12.h), + Text( + "We’ve sent an OTP to your registered email. Please enter it below.", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: AppColors.textGrey, + fontSize: 16.sp, + height: 1.4, + ), + ), + SizedBox(height: 48.h), + + // ===== OTP INPUT FIELDS ===== + OtpTextField( + numberOfFields: 6, + borderColor: AppColors.borderGrey, + focusedBorderColor: AppColors.primaryRed, + showFieldAsBox: true, + fieldWidth: 45.w, + borderRadius: BorderRadius.circular(12.r), + enabledBorderColor: AppColors.borderGrey, + cursorColor: AppColors.primaryRed, + textStyle: GoogleFonts.poppins( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: AppColors.black, + ), + onCodeChanged: (String code) { + setState(() { + _otp = code; + }); + }, + onSubmit: (String verificationCode) { + setState(() { + _otp = verificationCode; + }); + context.read().add( + VerifyOtpSubmitted( + emailAddress: widget.email, + otp: verificationCode, + ), + ); + }, + ), + ], + ), + ), ), + + // ===== VERIFY BUTTON ===== + CustomButton( + text: "Verify", + isLoading: isLoading, + onPressed: _otp.length == 6 + ? () { + context.read().add( + VerifyOtpSubmitted( + emailAddress: widget.email, + otp: _otp, + ), + ); + } + : null, + ), + SizedBox(height: 24.h), ], ), ), - ), - ], + ); + }, ), ), ); diff --git a/lib/login/views/reset_password_page.dart b/lib/login/views/reset_password_page.dart index 740f250..f4227cc 100644 --- a/lib/login/views/reset_password_page.dart +++ b/lib/login/views/reset_password_page.dart @@ -1,247 +1,284 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../constants/app_assets.dart'; +import '../../constants/app_colors.dart'; import '../../core/app_router.dart'; -import '../blocs/reset_password_bloc.dart'; +import '../../custome_widgets/custom_button.dart'; +import '../../custome_widgets/custom_textfield.dart'; +import '../blocs/reset_password/reset_password_bloc.dart'; -class ResetPasswordPage extends StatelessWidget { - const ResetPasswordPage({super.key}); +class ResetPasswordPage extends StatefulWidget { + final String email; + + const ResetPasswordPage({super.key, required this.email}); @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ResetPasswordBloc(), - child: const _ResetPasswordView(), - ); - } + State createState() => _ResetPasswordPageState(); } -class _ResetPasswordView extends StatelessWidget { - const _ResetPasswordView(); +class _ResetPasswordPageState extends State { + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _passwordFocusNode = FocusNode(); + final _confirmFocusNode = FocusNode(); + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _passwordFocusNode.dispose(); + _confirmFocusNode.dispose(); + super.dispose(); + } + + void _onResetPressed(BuildContext context, ResetPasswordState state) { + FocusManager.instance.primaryFocus?.unfocus(); + + final password = _passwordController.text; + final confirmPassword = _confirmPasswordController.text; + + if (!state.isPasswordValid) { + context.read().add( + const ResetPasswordErrorToggled(true, + error: "Please fulfill all password requirements"), + ); + return; + } + + if (password != confirmPassword) { + context.read().add( + const ResetConfirmPasswordErrorToggled(true, + error: "Passwords do not match"), + ); + return; + } + + context.read().add( + ResetPasswordSubmitted( + emailAddress: widget.email, + newPassword: password, + ), + ); + } @override Widget build(BuildContext context) { - final bloc = context.read(); + return GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + behavior: HitTestBehavior.translucent, + child: Scaffold( + backgroundColor: AppColors.backgroundWhite, + body: BlocConsumer( + listener: (context, state) { + if (state.status == ResetPasswordStatus.success) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Password reset successfully!"), + backgroundColor: AppColors.successGreen, + ), + ); + Navigator.pushNamedAndRemoveUntil( + context, + AppRouter.login, + (route) => false, + ); + } else if (state.status == ResetPasswordStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? "Reset failed"), + backgroundColor: Colors.redAccent, + ), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == ResetPasswordStatus.loading; - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/login/bg.png', fit: BoxFit.cover), - ), - Positioned.fill( - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.6), - Colors.black.withOpacity(1.0), + return SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 40.h), + // ===== LOGO SECTION ===== + Center( + child: Column( + children: [ + Image.asset( + AppAssets.appIcon, + height: 60.h, + ), + SizedBox(height: 8.h), + Text( + "Partner’s App", + style: GoogleFonts.poppins( + color: AppColors.primaryRed, + fontSize: 20.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 60.h), + + // ===== HEADER TEXT ===== + Text( + "Reset your password", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: AppColors.black, + fontSize: 24.sp, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(height: 8.h), + Text( + "Almost there — just set your new password", + textAlign: TextAlign.center, + style: GoogleFonts.poppins( + color: AppColors.textGrey, + fontSize: 16.sp, + ), + ), + SizedBox(height: 48.h), + + // ===== NEW PASSWORD FIELD ===== + CustomTextField( + label: 'New Password', + hintText: 'Enter new password', + controller: _passwordController, + focusNode: _passwordFocusNode, + prefixIcon: Icons.lock_outline, + isPassword: true, + isPasswordVisible: state.isPasswordVisible, + onTogglePasswordVisibility: () { + context.read().add( + const ResetPasswordVisibilityToggled(), + ); + }, + hasError: state.showPasswordError, + errorText: state.passwordErrorText, + textInputAction: TextInputAction.next, + readOnly: isLoading, + onChanged: (val) { + context + .read() + .add(PasswordChanged(val)); + if (state.showPasswordError) { + context.read().add( + const ResetPasswordErrorToggled(false)); + } + }, + ), + SizedBox(height: 20.h), + + // ===== VALIDATION BOX ===== + Container( + padding: EdgeInsets.all(16.r), + decoration: BoxDecoration( + color: AppColors.bgLightGrey, + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Password must contain:", + style: GoogleFonts.poppins( + fontSize: 13.sp, + color: AppColors.textGrey, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 12.h), + _buildValidationRow( + "At least 8 characters", state.hasMinLength), + _buildValidationRow( + "One uppercase letter", state.hasUppercase), + _buildValidationRow( + "One lowercase letter", state.hasLowercase), + _buildValidationRow( + "One number", state.hasNumber), + _buildValidationRow( + "One special character", state.hasSpecial), + ], + ), + ), + SizedBox(height: 20.h), + + // ===== CONFIRM PASSWORD FIELD ===== + CustomTextField( + label: 'Confirm New Password', + hintText: 'Enter your password', + controller: _confirmPasswordController, + focusNode: _confirmFocusNode, + prefixIcon: Icons.lock_outline, + isPassword: true, + isPasswordVisible: state.isConfirmPasswordVisible, + onTogglePasswordVisibility: () { + context.read().add( + const ConfirmPasswordVisibilityToggled(), + ); + }, + hasError: state.showConfirmPasswordError, + errorText: state.confirmPasswordErrorText, + textInputAction: TextInputAction.done, + readOnly: isLoading, + onChanged: (val) { + if (state.showConfirmPasswordError) { + context.read().add( + const ResetConfirmPasswordErrorToggled( + false)); + } + }, + onSubmitted: (_) => _onResetPressed(context, state), + ), + SizedBox(height: 24.h), + ], + ), + ), + ), + + // ===== RESET BUTTON ===== + CustomButton( + text: "Reset Password", + isLoading: isLoading, + onPressed: () => _onResetPressed(context, state), + ), + SizedBox(height: 24.h), ], ), ), - ), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 80), - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.center, - child: Image.asset( - "assets/login/app_icon.png", - scale: 4, - ), - ), - const SizedBox(height: 24), - Align( - alignment: Alignment.center, - child: Text( - "Reset your Password", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(height: 16), - - // ✅ Validation list - _buildValidationRow( - "Minimum of 8 characters", - state.hasMinLength), - _buildValidationRow( - "At least one uppercase letter (A–Z)", - state.hasUppercase), - _buildValidationRow( - "At least one number (0–9)", state.hasNumber), - const SizedBox(height: 20), - - // Password - TextField( - obscureText: true, - cursorColor: Colors.white, - onChanged: (value) => - bloc.add(PasswordChanged(value)), - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.lock_outline, - color: Colors.white70, - ), - hintText: 'Enter your password', - hintStyle: - const TextStyle(color: Colors.white54), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: Colors.white54, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white), - ), - ), - ), - - // ✅ Strength boxes - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate(4, (i) { - final filled = i < state.strengthLevel; - return Expanded( - child: Container( - margin: EdgeInsets.only( - right: i < 3 ? 6 : 0), - height: 5, - decoration: BoxDecoration( - color: filled - ? const Color(0xFFFFA500) // orange - : Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(3), - ), - ), - ); - }), - ), - - const SizedBox(height: 20), - - // Confirm Password - TextField( - obscureText: true, - cursorColor: Colors.white, - onChanged: (value) => - bloc.add(ConfirmPasswordChanged(value)), - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - prefixIcon: const Icon( - Icons.lock_outline, - color: Colors.white70, - ), - hintText: 'Retype your password', - hintStyle: - const TextStyle(color: Colors.white54), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: Colors.white54, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: - const BorderSide(color: Colors.white), - ), - ), - ), - const SizedBox(height: 30), - ], - ), - ), - ), - ), - const SizedBox(height: 60), - SizedBox( - height: 52, - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.pushNamed( - context, AppRouter.qrScanScreen); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFFF4C4C), - padding: - const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - "Verify", - style: GoogleFonts.poppins( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ); - }, - ), - ), - ), - ], + ); + }, + ), ), ); } - // ✅ Validation circle + text Widget _buildValidationRow(String text, bool isValid) { return Padding( - padding: const EdgeInsets.only(bottom: 6), + padding: EdgeInsets.only(bottom: 8.h), child: Row( children: [ + Icon( + isValid ? Icons.check_circle : Icons.circle, + size: 16.sp, + color: isValid ? AppColors.successGreen : const Color(0xFFD1D1D1), + ), + SizedBox(width: 10.w), Text( text, - style: GoogleFonts.poppins(color: Colors.white, fontSize: 13), - ), - const Spacer(), - Container( - height: 12, - width: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white), - color: isValid ? Colors.white : Colors.transparent, + style: GoogleFonts.poppins( + fontSize: 13.sp, + color: isValid ? AppColors.successGreen : AppColors.textGrey, ), ), ], diff --git a/lib/main.dart b/lib/main.dart index e8bbf7b..57c1d3f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'all_bloc_poviders/all_bloc_providers.dart'; import 'core/app_router.dart'; -void main() { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.white, statusBarIconBrightness: Brightness.dark, @@ -18,16 +24,24 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'City Cards Partner', - debugShowCheckedModeBanner: false, - theme: ThemeData( - textTheme: GoogleFonts.poppinsTextTheme( - Theme.of(context).textTheme, - ) + return MultiBlocProvider( + providers: AllBlocProviders.providers(), + child: ScreenUtilInit( // ← Wrap here + designSize: const Size(390, 844), // ← iPhone 14 base size + minTextAdapt: true, + splitScreenMode: true, + builder: (context, child) => MaterialApp( + title: 'City Cards Partner', + debugShowCheckedModeBanner: false, + theme: ThemeData( + textTheme: GoogleFonts.poppinsTextTheme( + Theme.of(context).textTheme, + ), + ), + initialRoute: AppRouter.splashScreen, + onGenerateRoute: AppRouter.generateRoute, + ), ), - initialRoute: AppRouter.splashScreen, - onGenerateRoute: AppRouter.generateRoute, ); } -} +} \ No newline at end of file diff --git a/lib/network_api_service/api_service/api_service.dart b/lib/network_api_service/api_service/api_service.dart new file mode 100644 index 0000000..dc71698 --- /dev/null +++ b/lib/network_api_service/api_service/api_service.dart @@ -0,0 +1,266 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../../local_peference/local_preference.dart'; +import '../api_urls/api_urls.dart'; + +class ApiService { + static const String _baseUrl = 'https://your-api-base-url.com/api'; + + static final ApiService _instance = ApiService._internal(); + late Dio _dio; + + factory ApiService() => _instance; + + ApiService._internal() { + _dio = Dio( + BaseOptions( + baseUrl: _baseUrl, + connectTimeout: const Duration(seconds: 60), + receiveTimeout: const Duration(seconds: 60), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + ), + ); + + // ================= RETRY INTERCEPTOR ================= + _dio.interceptors.add( + InterceptorsWrapper( + onError: (err, handler) async { + final options = err.requestOptions; + const maxRetries = 2; + final currentRetry = options.extra['retry'] as int? ?? 0; + + final shouldRetry = + currentRetry < maxRetries && + (err.type == DioExceptionType.connectionTimeout || + err.type == DioExceptionType.sendTimeout || + err.type == DioExceptionType.receiveTimeout); + + if (shouldRetry) { + if (kDebugMode) { + print( + '🔁 Retrying request (${currentRetry + 1}) => ${options.uri}', + ); + } + options.extra['retry'] = currentRetry + 1; + try { + final response = await _dio.fetch(options); + return handler.resolve(response); + } on DioException catch (e) { + return handler.reject(e); + } + } + + return handler.reject(err); + }, + ), + ); + + // ================= MAIN INTERCEPTOR (Queued for concurrency) ================= + _dio.interceptors.add( + QueuedInterceptorsWrapper( + onRequest: (options, handler) async { + final token = await LocalPreference.getAccessToken(); + if (token != null && token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + handler.next(options); + }, + + onError: (error, handler) async { + if (error.response?.statusCode == 401) { + final requestOptions = error.requestOptions; + + try { + final refreshed = await _refreshToken(); + + if (refreshed) { + final newToken = await LocalPreference.getAccessToken(); + requestOptions.headers['Authorization'] = 'Bearer $newToken'; + final response = await _dio.fetch(requestOptions); + return handler.resolve(response); + } else { + await _forceLogout(); + return handler.reject(error); + } + } catch (_) { + await _forceLogout(); + return handler.reject(error); + } + } + + handler.next(error); + }, + ), + ); + + // ================= LOGGING INTERCEPTOR ================= + if (kDebugMode) { + _dio.interceptors.add( + LogInterceptor( + request: true, + requestHeader: true, + requestBody: true, + responseBody: true, + error: true, + ), + ); + } + } + + // ================= GET ================= + Future get( + String endpoint, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.get( + endpoint, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleError(e); + } + } + + // ================= POST ================= + Future post( + String endpoint, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + }) async { + try { + return await _dio.post( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + ); + } on DioException catch (e) { + throw _handleError(e); + } + } + + // ================= PUT ================= + Future put( + String endpoint, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + }) async { + try { + return await _dio.put( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + ); + } on DioException catch (e) { + throw _handleError(e); + } + } + + // ================= DELETE ================= + Future delete( + String endpoint, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + return await _dio.delete( + endpoint, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + } on DioException catch (e) { + throw _handleError(e); + } + } + + // ================= REFRESH TOKEN ================= + Future _refreshToken() async { + try { + final refreshToken = await LocalPreference.getRefreshToken(); + if (refreshToken == null) return false; + + final response = await _dio.post( + ApiUrls.refreshToken, + data: {"refreshToken": refreshToken}, + options: Options(headers: {'Authorization': null}), + ); + + await LocalPreference.setAccessToken(response.data['accessToken']); + return true; + } catch (_) { + return false; + } + } + + // ================= FORCE LOGOUT ================= + Future _forceLogout() async { + await LocalPreference.clearAll(); + await LocalPreference.setLogin(false); + } + + // ================= ERROR HANDLER ================= + String _handleError(DioException error) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + return "Connection timeout. Please try again."; + case DioExceptionType.sendTimeout: + return "Send timeout. Please try again."; + case DioExceptionType.receiveTimeout: + return "Receive timeout. Please try again."; + case DioExceptionType.badCertificate: + return "Bad certificate."; + case DioExceptionType.badResponse: + try { + final responseData = error.response?.data; + if (responseData is Map) { + return responseData['message'] ?? + responseData['error'] ?? + "Invalid status code: ${error.response?.statusCode}"; + } + if (responseData is String) { + return responseData.isNotEmpty + ? responseData + : "Invalid status code: ${error.response?.statusCode}"; + } + return "Invalid status code: ${error.response?.statusCode}"; + } catch (_) { + return "Invalid status code: ${error.response?.statusCode}"; + } + case DioExceptionType.cancel: + return "Request was cancelled."; + case DioExceptionType.connectionError: + return "No internet connection."; + case DioExceptionType.unknown: + return "Something went wrong. Please try again."; + } + } + + // ================= UPDATE HEADERS ================= + void updateHeaders(Map headers) { + _dio.options.headers.addAll(headers); + } +} \ No newline at end of file diff --git a/lib/network_api_service/api_urls/api_urls.dart b/lib/network_api_service/api_urls/api_urls.dart new file mode 100644 index 0000000..cc42a40 --- /dev/null +++ b/lib/network_api_service/api_urls/api_urls.dart @@ -0,0 +1,18 @@ +class ApiUrls { + + // static const baseUrl = "https://devapi.citycards.betadelivery.com"; // Normal API + static const baseUrl = "https://testingapi.citycards.betadelivery.com"; // Test API + // static const baseUrl = "https://uatapi.citycard.betadelivery.com"; // Production Lvl API + + static const refreshToken = "$baseUrl/auth/refresh"; + + // ================= GET APIs ================= + static const authUserDetails = "$baseUrl/partner/auth"; + + // ================= POST APIs ================= + static const login = "$baseUrl/partner/auth/login"; + static const forgotPassword = "$baseUrl/partner/auth/forgot-password"; + static const verifyOtp = "$baseUrl/partner/auth/verify-otp"; + static const resetPassword = "$baseUrl/partner/auth/set-password"; + +} \ No newline at end of file diff --git a/lib/profile/blocs/profile/profile_bloc.dart b/lib/profile/blocs/profile/profile_bloc.dart new file mode 100644 index 0000000..9a6aef6 --- /dev/null +++ b/lib/profile/blocs/profile/profile_bloc.dart @@ -0,0 +1,27 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../profile/repository/profile_repository.dart'; +import 'profile_event.dart'; +import 'profile_state.dart'; + +class ProfileBloc extends Bloc { + final ProfileRepository _profileRepository; + + ProfileBloc({required ProfileRepository profileRepository}) + : _profileRepository = profileRepository, + super(ProfileInitial()) { + on(_onFetchUserDetails); + } + + Future _onFetchUserDetails( + FetchUserDetailsEvent event, + Emitter emit, + ) async { + emit(ProfileLoading()); + try { + final userDetails = await _profileRepository.fetchUserDetails(); + emit(ProfileLoaded(userDetails: userDetails)); + } catch (e) { + emit(ProfileError(message: e.toString())); + } + } +} diff --git a/lib/profile/blocs/profile/profile_event.dart b/lib/profile/blocs/profile/profile_event.dart new file mode 100644 index 0000000..40d7c38 --- /dev/null +++ b/lib/profile/blocs/profile/profile_event.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +class FetchUserDetailsEvent extends ProfileEvent {} diff --git a/lib/profile/blocs/profile/profile_state.dart b/lib/profile/blocs/profile/profile_state.dart new file mode 100644 index 0000000..4b20d63 --- /dev/null +++ b/lib/profile/blocs/profile/profile_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import '../../../profile/models/profile_model.dart'; + +abstract class ProfileState extends Equatable { + const ProfileState(); + + @override + List get props => []; +} + +class ProfileInitial extends ProfileState {} + +class ProfileLoading extends ProfileState {} + +class ProfileLoaded extends ProfileState { + final UserDetails userDetails; + + const ProfileLoaded({required this.userDetails}); + + @override + List get props => [userDetails]; +} + +class ProfileError extends ProfileState { + final String message; + + const ProfileError({required this.message}); + + @override + List get props => [message]; +} diff --git a/lib/profile/blocs/profile_bloc.dart b/lib/profile/blocs/profile_bloc.dart index bddfc13..361e720 100644 --- a/lib/profile/blocs/profile_bloc.dart +++ b/lib/profile/blocs/profile_bloc.dart @@ -1,4 +1,6 @@ // profile_cubit.dart +import 'package:citycards_partner_flutter/local_peference/local_preference.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../viewmodel/profile_viewmodel.dart'; @@ -28,5 +30,11 @@ class ProfileCubit extends Cubit { void logout() { // Handle logout logic here print("User logged out"); + // LocalPreference.clearAll(); + // Navigator.pushNamedAndRemoveUntil( + // context, + // AppRouter.login, + // (route) => false, + // ); } } diff --git a/lib/profile/models/profile_model.dart b/lib/profile/models/profile_model.dart new file mode 100644 index 0000000..b0c04e3 --- /dev/null +++ b/lib/profile/models/profile_model.dart @@ -0,0 +1,91 @@ +class Role { + final int id; + final String name; + + Role({ + required this.id, + required this.name, + }); + + factory Role.fromJson(Map json) { + return Role( + id: json['id'], + name: json['name'], + ); + } + + Map toJson() { + return { + 'id': id, + 'name': name, + }; + } +} + +class UserDetails { + final int id; + final String fullName; + final String email; + final String phone; + final String round; + final Role role; + final bool isActive; + final bool isDeleted; + final String lastLoginAt; + final String joinDate; + final String createdAtRaw; + final String updatedAt; + final String updatedAtRaw; + + UserDetails({ + required this.id, + required this.fullName, + required this.email, + required this.phone, + required this.round, + required this.role, + required this.isActive, + required this.isDeleted, + required this.lastLoginAt, + required this.joinDate, + required this.createdAtRaw, + required this.updatedAt, + required this.updatedAtRaw, + }); + + factory UserDetails.fromJson(Map json) { + return UserDetails( + id: json['id'], + fullName: json['fullName'] ?? '', + email: json['email'] ?? '', + phone: json['phone'] ?? '', + round: json['round'] ?? '', + role: Role.fromJson(json['role']), + isActive: json['isActive'] ?? false, + isDeleted: json['isDeleted'] ?? false, + lastLoginAt: json['lastLoginAt'] ?? '', + joinDate: json['joinDate'] ?? '', + createdAtRaw: json['createdAtRaw'] ?? '', + updatedAt: json['updatedAt'] ?? '', + updatedAtRaw: json['updatedAtRaw'] ?? '', + ); + } + + Map toJson() { + return { + 'id': id, + 'fullName': fullName, + 'email': email, + 'phone': phone, + 'round': round, + 'role': role.toJson(), + 'isActive': isActive, + 'isDeleted': isDeleted, + 'lastLoginAt': lastLoginAt, + 'joinDate': joinDate, + 'createdAtRaw': createdAtRaw, + 'updatedAt': updatedAt, + 'updatedAtRaw': updatedAtRaw, + }; + } +} \ No newline at end of file diff --git a/lib/profile/repository/profile_repository.dart b/lib/profile/repository/profile_repository.dart new file mode 100644 index 0000000..277c43c --- /dev/null +++ b/lib/profile/repository/profile_repository.dart @@ -0,0 +1,21 @@ + +import '../../local_peference/local_preference.dart'; +import '../../network_api_service/api_service/api_service.dart'; +import '../../network_api_service/api_urls/api_urls.dart'; +import '../models/profile_model.dart'; + +class ProfileRepository { + final ApiService _apiService = ApiService(); + + Future fetchUserDetails() async { + try { + final userId = await LocalPreference.getUserId(); + final response = await _apiService.get( + '${ApiUrls.authUserDetails}/$userId', + ); + return UserDetails.fromJson(response.data); + } catch (e) { + throw Exception('Failed to fetch user details: $e'); + } + } +} diff --git a/lib/profile/views/profile_page.dart b/lib/profile/views/profile_page.dart index 0632dac..2f7ebfe 100644 --- a/lib/profile/views/profile_page.dart +++ b/lib/profile/views/profile_page.dart @@ -1,65 +1,82 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../blocs/profile_bloc.dart'; -import '../viewmodel/profile_viewmodel.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; // Import ScreenUtil +import '../../core/app_router.dart'; +import '../../local_peference/local_preference.dart'; +import '../blocs/profile/profile_bloc.dart'; +import '../blocs/profile/profile_event.dart'; +import '../blocs/profile/profile_state.dart'; +import '../models/profile_model.dart'; // Import UserDetails model class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ProfileCubit(), - child: Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return Center(child: CircularProgressIndicator()); - } + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), // Use w for horizontal padding + child: BlocConsumer( + listener: (context, state) { + if (state is ProfileError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.message)), + ); + } + }, + builder: (context, state) { + if (state is ProfileInitial) { + BlocProvider.of(context).add(FetchUserDetailsEvent()); + return const Center(child: CircularProgressIndicator()); + } else if (state is ProfileLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is ProfileLoaded) { + final userDetails = state.userDetails; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeaderSection( context), - SizedBox(height: 32), - _buildCustomerDetailsSection(state), - SizedBox(height: 24), - _buildAdditionalInfoSection(state, context), - SizedBox(height: 24), - Spacer(), - _buildLastLoginSection(state, context), - const SizedBox(height: 20), + _buildHeaderSection(context), + SizedBox(height: 32.h), // Use h for vertical spacing + _buildCustomerDetailsSection(userDetails), + SizedBox(height: 24.h), + _buildAdditionalInfoSection(userDetails, context), + SizedBox(height: 24.h), + const Spacer(), + _buildLastLoginSection(userDetails, context), + SizedBox(height: 20.h), ], ); - }, - ), + } else if (state is ProfileError) { + return Center(child: Text('Error: ${state.message}')); + } + return const Center(child: Text('Unknown State')); + }, ), ), ), ); } - Widget _buildHeaderSection( BuildContext context) { + Widget _buildHeaderSection(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 14.0), + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 14.h), // Use w and h child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( child: Container( - width: 44, - height: 44, + width: 44.w, // Use w + height: 44.h, // Use h decoration: const BoxDecoration( color: Color(0xFFF95F62), shape: BoxShape.circle, ), - child: const Icon(Icons.arrow_back, color: Colors.white), + child: Icon(Icons.arrow_back, color: Colors.white, size: 24.sp), // Use sp for icon size ), onTap: (){ Navigator.pop(context); @@ -67,22 +84,22 @@ class ProfileScreen extends StatelessWidget { ), Text( 'Profile', - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.w700, - fontSize: 28, + fontSize: 28.sp, // Use sp for font size ), ), SizedBox( - width: 40, + width: 40.w, // Use w ) ], ), ), - SizedBox(height: 8), + SizedBox(height: 8.h), // Use h Text( 'Manage your account, update preferences, and customize app settings for a personalized experience.', style: TextStyle( - fontSize: 14, + fontSize: 14.sp, // Use sp color: Colors.grey[600], height: 1.4, ), @@ -91,30 +108,30 @@ class ProfileScreen extends StatelessWidget { ); } - Widget _buildCustomerDetailsSection(ProfileState state) { + Widget _buildCustomerDetailsSection(UserDetails userDetails) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Customer Details', style: TextStyle( - fontSize: 24, + fontSize: 24.sp, // Use sp fontWeight: FontWeight.w600, color: Colors.black, ), ), - SizedBox(height: 16), + SizedBox(height: 16.h), // Use h Container( decoration: BoxDecoration( border: Border.symmetric(horizontal: BorderSide(color: Colors.grey[300]!)), ), child: Column( children: [ - _buildDetailRow('Name', state.name), + _buildDetailRow('Name', userDetails.fullName), _buildDivider(), - _buildDetailRow('Phone', state.phone), + _buildDetailRow('Phone', userDetails.phone), _buildDivider(), - _buildDetailRow('Role', state.role), + _buildDetailRow('Role', userDetails.role.name), ], ), ), @@ -122,26 +139,26 @@ class ProfileScreen extends StatelessWidget { ); } - Widget _buildAdditionalInfoSection(ProfileState state, BuildContext context) { + Widget _buildAdditionalInfoSection(UserDetails userDetails, BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Additional Info', style: TextStyle( - fontSize: 18, + fontSize: 18.sp, // Use sp fontWeight: FontWeight.w600, color: Colors.black, ), ), - SizedBox(height: 16), + SizedBox(height: 16.h), // Use h Container( decoration: BoxDecoration( border: Border.symmetric(horizontal: BorderSide(color: Colors.grey[300]!)), ), child: Column( children: [ - _buildActionRow('Change Email', state.email), + _buildActionRow('Email', userDetails.email), _buildDivider(), _buildActionRow('Change Password', '● ● ● ● ● ● ●'), ], @@ -151,7 +168,7 @@ class ProfileScreen extends StatelessWidget { ); } - Widget _buildLastLoginSection(ProfileState state, BuildContext context) { + Widget _buildLastLoginSection(UserDetails userDetails, BuildContext context) { return Column( children: [ SizedBox( @@ -160,33 +177,40 @@ class ProfileScreen extends StatelessWidget { children: [ Text( 'Last Login on ', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), + style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), // Use sp ), Text( - state.lastLogin, - style: TextStyle(fontSize: 14, color: Colors.black), + userDetails.lastLoginAt, + style: TextStyle(fontSize: 14.sp, color: Colors.black), // Use sp ), ], ), ), - const SizedBox(height: 14), + SizedBox(height: 14.h), // Use h SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: () => context.read().logout(), + onPressed: () { + LocalPreference.clearAll(); + Navigator.pushNamedAndRemoveUntil( + context, + AppRouter.login, + (route) => false, + ); + }, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), - side: BorderSide(color: Color(0xffDC2626)), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // Use w and h + side: const BorderSide(color: Color(0xffDC2626)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)), // Use r for radius ), child: Row( children: [ - Icon(Icons.logout, color: Color(0xffDC2626)), - SizedBox(width: 10), + Icon(Icons.logout, color: const Color(0xffDC2626), size: 24.sp), // Use sp for icon size + SizedBox(width: 10.w), // Use w Text( 'Log out', - style: TextStyle(fontSize: 16, color: Color(0xffDC2626), fontWeight: FontWeight.w600), + style: TextStyle(fontSize: 16.sp, color: const Color(0xffDC2626), fontWeight: FontWeight.w600), // Use sp ), ], ), @@ -198,12 +222,12 @@ class ProfileScreen extends StatelessWidget { Widget _buildDetailRow(String label, String value) { return Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // Use w and h child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700]))), - Expanded(flex: 3, child: Text(value, style: TextStyle(color: Colors.black))), + Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700], fontSize: 14.sp))), // Use sp + Expanded(flex: 3, child: Text(value, style: TextStyle(color: Colors.black, fontSize: 14.sp))), // Use sp ], ), ); @@ -211,14 +235,14 @@ class ProfileScreen extends StatelessWidget { Widget _buildActionRow(String label, String value) { return Padding( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), // Use w and h child: Row( children: [ - Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700]))), + Expanded(flex: 2, child: Text(label, style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey[700], fontSize: 14.sp))), // Use sp Expanded( flex: 3, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(value, style: TextStyle(color: Colors.grey[600])), + Text(value, style: TextStyle(color: Colors.grey[600], fontSize: 14.sp)), // Use sp ]), ), ], @@ -227,6 +251,6 @@ class ProfileScreen extends StatelessWidget { } Widget _buildDivider() { - return Container(height: 1, color: Colors.grey[300], margin: EdgeInsets.symmetric(horizontal: 2)); + return Container(height: 1.h, color: Colors.grey[300], margin: EdgeInsets.symmetric(horizontal: 2.w)); // Use h and w } -} +} \ No newline at end of file diff --git a/lib/scan/view/qr_scan_screen.dart b/lib/scan/view/qr_scan_screen.dart index 50db712..b13a8f6 100644 --- a/lib/scan/view/qr_scan_screen.dart +++ b/lib/scan/view/qr_scan_screen.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:citycards_partner_flutter/constants/app_assets.dart'; import 'package:citycards_partner_flutter/core/app_router.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -288,7 +289,7 @@ class _QrScanScreenState extends State final icons = [ {'img': 'assets/scan/flash.png', 'route': '/flash'}, {'img': 'assets/scan/menu.png', 'route': '/menu'}, - {'img': 'assets/scan/logo.png', 'route': '/home'}, + {'img': AppAssets.appIcon, 'route': '/home'}, {'img': 'assets/scan/history.png', 'route': AppRouter.scanHistory}, {'img': 'assets/scan/profile.png', 'route': AppRouter.profileScreen}, ]; @@ -304,13 +305,13 @@ class _QrScanScreenState extends State final isFlash = e['route'] == '/flash'; final isMenu = e['route'] == '/menu'; final isHome = e['route'] == '/home'; - final isIdle = status == QrScanStatus.idle; + final statusIdle = status == QrScanStatus.idle; // 🔙 Show Back Button when expanded or after scan - if ((isFlash && isExpanded) || (isFlash && !isIdle)) { + if ((isFlash && isExpanded) || (isFlash && !statusIdle)) { return GestureDetector( onTap: () async { - if (!isIdle) { + if (!statusIdle) { context.read().add(ResetQrScanEvent()); } else if (sheetController.isAttached) { await sheetController.animateTo( @@ -361,7 +362,11 @@ class _QrScanScreenState extends State } } }, - child: Image.asset(e['img']!, scale: 4), + child: Image.asset( + e['img']!, + scale: isHome ? 6 : 4, + color: isHome ? Colors.black : null, + ), ); }).toList(), ); diff --git a/lib/splash/bloc/splash_bloc.dart b/lib/splash/bloc/splash_bloc.dart new file mode 100644 index 0000000..da39db6 --- /dev/null +++ b/lib/splash/bloc/splash_bloc.dart @@ -0,0 +1,34 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../local_peference/local_preference.dart'; // TODO: update path if needed +part 'splash_event.dart'; +part 'splash_state.dart'; + +class SplashBloc extends Bloc { + SplashBloc() : super(SplashInitial()) { + on(_onSplashStarted); + } + + Future _onSplashStarted( + SplashStarted event, + Emitter emit, + ) async { + emit(SplashLoading()); + + await Future.delayed(const Duration(seconds: 3)); + + final bool isLoggedIn = await LocalPreference.getLogin(); // ← CHECK LOGIN FIRST + + if (isLoggedIn) { + emit(SplashNavigateToHome()); // ← already logged in → go to QR + return; + } + + final bool showOnboarding = await LocalPreference.getOnBoarding(); + + if (showOnboarding) { + emit(SplashNavigateToOnboarding()); + } else { + emit(SplashNavigateToLogin()); + } + } +} \ No newline at end of file diff --git a/lib/splash/bloc/splash_event.dart b/lib/splash/bloc/splash_event.dart new file mode 100644 index 0000000..b652931 --- /dev/null +++ b/lib/splash/bloc/splash_event.dart @@ -0,0 +1,5 @@ +part of 'splash_bloc.dart'; + +abstract class SplashEvent {} + +class SplashStarted extends SplashEvent {} \ No newline at end of file diff --git a/lib/splash/bloc/splash_state.dart b/lib/splash/bloc/splash_state.dart new file mode 100644 index 0000000..e1bbad4 --- /dev/null +++ b/lib/splash/bloc/splash_state.dart @@ -0,0 +1,13 @@ +part of 'splash_bloc.dart'; + +abstract class SplashState {} + +class SplashInitial extends SplashState {} + +class SplashLoading extends SplashState {} + +class SplashNavigateToOnboarding extends SplashState {} + +class SplashNavigateToLogin extends SplashState {} + +class SplashNavigateToHome extends SplashState {} // ← ADD THIS \ No newline at end of file diff --git a/lib/splash/splash_view.dart b/lib/splash/splash_view.dart deleted file mode 100644 index 19823d2..0000000 --- a/lib/splash/splash_view.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:citycards_partner_flutter/core/app_router.dart'; -import 'package:flutter/material.dart'; -import 'dart:async'; - -class SplashScreen extends StatefulWidget { - const SplashScreen({super.key}); - - @override - State createState() => _SplashScreenState(); -} - -class _SplashScreenState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 2), - ); - - _fadeAnimation = CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ); - - _controller.forward(); - Timer(const Duration(seconds: 3), () { - Navigator.pushReplacementNamed(context, AppRouter.onboarding); - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Stack( - fit: StackFit.expand, - children: [ - /// 🌄 Background Image - Image.asset( - "assets/splash/bg.png", // your full-screen background - fit: BoxFit.cover, - ), - - /// 🏙️ Logo Fade-in - FadeTransition( - opacity: _fadeAnimation, - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(top: 213), // 👈 adjust height here - child: Image.asset( - "assets/splash/logo.png",scale: 4, - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/splash/view/splash_view.dart b/lib/splash/view/splash_view.dart new file mode 100644 index 0000000..d842674 --- /dev/null +++ b/lib/splash/view/splash_view.dart @@ -0,0 +1,87 @@ +import 'package:citycards_partner_flutter/core/app_router.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../constants/app_assets.dart'; +import '../../local_peference/local_preference.dart'; +import '../bloc/splash_bloc.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + ); + _fadeAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + _controller.forward(); + context.read().add(SplashStarted()); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) async { + if (state is SplashNavigateToHome) { + Navigator.pushReplacementNamed(context, AppRouter.qrScanScreen); + } else if (state is SplashNavigateToOnboarding) { + await LocalPreference.setOnBoarding(false); + if (!context.mounted) return; + Navigator.pushReplacementNamed(context, AppRouter.onboarding); + } else if (state is SplashNavigateToLogin) { + Navigator.pushReplacementNamed(context, AppRouter.login); + } + }, + child: Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + /// 🌄 Background Image + Image.asset( + "assets/splash/bg.png", + fit: BoxFit.cover, + ), + + /// 🏙️ Logo Fade-in + FadeTransition( + opacity: _fadeAnimation, + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: 213.h), // ← ScreenUtil height + child: Image.asset( + AppAssets.appIcon, + color: Colors.white, + width: 0.75.sw, // ← ScreenUtil: 75% of screen width + fit: BoxFit.contain, + ), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 38d94c6..f5df75e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -129,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" equatable: dependency: "direct main" description: @@ -153,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" file_picker: dependency: "direct main" description: @@ -214,6 +238,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.32" + flutter_screenutil: + dependency: "direct main" + description: + name: flutter_screenutil + sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" + url: "https://pub.dev" + source: hosted + version: "5.9.3" flutter_test: dependency: "direct dev" description: flutter @@ -316,26 +348,34 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -448,6 +488,62 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" + url: "https://pub.dev" + source: hosted + version: "2.4.21" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" simple_gesture_detector: dependency: transitive description: @@ -529,10 +625,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1b80589..611ccea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,9 @@ dependencies: mobile_scanner: ^7.1.3 equatable: ^2.0.7 flutter_launcher_icons: ^0.14.4 + shared_preferences: ^2.5.4 + dio: ^5.9.2 + flutter_screenutil: ^5.9.3 dev_dependencies: flutter_test: