add login flow with api integration and screen ui updated.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 14 KiB |
37
lib/all_bloc_poviders/all_bloc_providers.dart
Normal file
37
lib/all_bloc_poviders/all_bloc_providers.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../login/blocs/login/login_bloc.dart';
|
||||
import '../login/blocs/forgot_password/forgot_password_bloc.dart';
|
||||
import '../login/blocs/verify_otp/verify_otp_bloc.dart';
|
||||
import '../login/blocs/reset_password/reset_password_bloc.dart';
|
||||
import '../splash/bloc/splash_bloc.dart';
|
||||
import '../profile/blocs/profile/profile_bloc.dart';
|
||||
import '../profile/repository/profile_repository.dart';
|
||||
|
||||
class AllBlocProviders {
|
||||
AllBlocProviders._(); // Private constructor — not meant to be instantiated
|
||||
|
||||
static List<BlocProvider> providers() {
|
||||
return [
|
||||
// ─── Splash ──────────────────────────────────────────────────────────
|
||||
BlocProvider<SplashBloc>(
|
||||
create: (_) => SplashBloc(),
|
||||
),
|
||||
BlocProvider<LoginBloc>(
|
||||
create: (_) => LoginBloc(),
|
||||
),
|
||||
BlocProvider<ForgotPasswordBloc>(
|
||||
create: (_) => ForgotPasswordBloc(),
|
||||
),
|
||||
BlocProvider<VerifyOtpBloc>(
|
||||
create: (_) => VerifyOtpBloc(),
|
||||
),
|
||||
BlocProvider<ResetPasswordBloc>(
|
||||
create: (_) => ResetPasswordBloc(),
|
||||
),
|
||||
// ─── Profile ─────────────────────────────────────────────────────────
|
||||
BlocProvider<ProfileBloc>(
|
||||
create: (_) => ProfileBloc(profileRepository: ProfileRepository()),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
3
lib/constants/app_assets.dart
Normal file
3
lib/constants/app_assets.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class AppAssets {
|
||||
static const String appIcon = "assets/login/app_icon.png";
|
||||
}
|
||||
14
lib/constants/app_colors.dart
Normal file
14
lib/constants/app_colors.dart
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
lib/custome_widgets/custom_button.dart
Normal file
61
lib/custome_widgets/custom_button.dart
Normal file
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/custome_widgets/custom_textfield.dart
Normal file
118
lib/custome_widgets/custom_textfield.dart
Normal file
@@ -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<String>? onChanged;
|
||||
final ValueChanged<String>? 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
77
lib/local_peference/local_preference.dart
Normal file
77
lib/local_peference/local_preference.dart
Normal file
@@ -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<void> setLogin(bool value) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_keyLogin, value);
|
||||
}
|
||||
|
||||
static Future<bool> getLogin() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_keyLogin) ?? false;
|
||||
}
|
||||
|
||||
// -------------------- USER ID --------------------
|
||||
|
||||
static Future<void> setUserId(int id) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyUserId, id);
|
||||
}
|
||||
|
||||
static Future<int> getUserId() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_keyUserId) ?? 0;
|
||||
}
|
||||
|
||||
// -------------------- ACCESS TOKEN --------------------
|
||||
|
||||
static Future<void> setAccessToken(String token) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keyAccessToken, token);
|
||||
}
|
||||
|
||||
static Future<String> getAccessToken() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_keyAccessToken) ?? "";
|
||||
}
|
||||
|
||||
// -------------------- REFRESH TOKEN --------------------
|
||||
|
||||
static Future<void> setRefreshToken(String token) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keyRefreshToken, token);
|
||||
}
|
||||
|
||||
static Future<String> getRefreshToken() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_keyRefreshToken) ?? "";
|
||||
}
|
||||
|
||||
// -------------------- ONBOARDING --------------------
|
||||
|
||||
static Future<void> setOnBoarding(bool value) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_keyOnBoarding, value);
|
||||
}
|
||||
|
||||
static Future<bool> getOnBoarding() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_keyOnBoarding) ?? true;
|
||||
}
|
||||
|
||||
// -------------------- CLEAR --------------------
|
||||
|
||||
static Future<void> clearAll() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.clear();
|
||||
}
|
||||
}
|
||||
44
lib/login/blocs/forgot_password/forgot_password_bloc.dart
Normal file
44
lib/login/blocs/forgot_password/forgot_password_bloc.dart
Normal file
@@ -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<ForgotPasswordEvent, ForgotPasswordState> {
|
||||
final ForgotPasswordRepository _forgotPasswordRepository;
|
||||
|
||||
ForgotPasswordBloc({ForgotPasswordRepository? forgotPasswordRepository})
|
||||
: _forgotPasswordRepository = forgotPasswordRepository ?? ForgotPasswordRepository(),
|
||||
super(const ForgotPasswordState()) {
|
||||
on<ForgotPasswordSubmitted>(_onForgotPasswordSubmitted);
|
||||
on<ForgotPasswordEmailErrorToggled>(_onEmailErrorToggled);
|
||||
}
|
||||
|
||||
Future<void> _onForgotPasswordSubmitted(
|
||||
ForgotPasswordSubmitted event,
|
||||
Emitter<ForgotPasswordState> 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<ForgotPasswordState> emit,
|
||||
) {
|
||||
emit(state.copyWith(showEmailError: event.show));
|
||||
}
|
||||
}
|
||||
25
lib/login/blocs/forgot_password/forgot_password_event.dart
Normal file
25
lib/login/blocs/forgot_password/forgot_password_event.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
part of 'forgot_password_bloc.dart';
|
||||
|
||||
abstract class ForgotPasswordEvent extends Equatable {
|
||||
const ForgotPasswordEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ForgotPasswordSubmitted extends ForgotPasswordEvent {
|
||||
final String emailAddress;
|
||||
|
||||
const ForgotPasswordSubmitted({required this.emailAddress});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [emailAddress];
|
||||
}
|
||||
|
||||
class ForgotPasswordEmailErrorToggled extends ForgotPasswordEvent {
|
||||
final bool show;
|
||||
const ForgotPasswordEmailErrorToggled(this.show);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [show];
|
||||
}
|
||||
30
lib/login/blocs/forgot_password/forgot_password_state.dart
Normal file
30
lib/login/blocs/forgot_password/forgot_password_state.dart
Normal file
@@ -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<Object?> get props => [status, errorMessage, showEmailError];
|
||||
}
|
||||
86
lib/login/blocs/login/login_bloc.dart
Normal file
86
lib/login/blocs/login/login_bloc.dart
Normal file
@@ -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<LoginEvent, LoginState> {
|
||||
final LoginRepository _loginRepository;
|
||||
|
||||
LoginBloc({LoginRepository? loginRepository})
|
||||
: _loginRepository = loginRepository ?? LoginRepository(),
|
||||
super(const LoginState()) {
|
||||
on<LoginSubmitted>(_onLoginSubmitted);
|
||||
on<LoginPasswordVisibilityToggled>(_onPasswordVisibilityToggled);
|
||||
on<LoginRememberMeToggled>(_onRememberMeToggled);
|
||||
on<LoginEmailErrorToggled>(_onEmailErrorToggled);
|
||||
on<LoginPasswordErrorToggled>(_onPasswordErrorToggled);
|
||||
}
|
||||
|
||||
// ================= LOGIN SUBMITTED =================
|
||||
Future<void> _onLoginSubmitted(
|
||||
LoginSubmitted event,
|
||||
Emitter<LoginState> 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<LoginState> emit,
|
||||
) {
|
||||
emit(state.copyWith(isPasswordVisible: !state.isPasswordVisible));
|
||||
}
|
||||
|
||||
// ================= REMEMBER ME =================
|
||||
void _onRememberMeToggled(
|
||||
LoginRememberMeToggled event,
|
||||
Emitter<LoginState> emit,
|
||||
) {
|
||||
emit(state.copyWith(rememberMe: event.value));
|
||||
}
|
||||
|
||||
void _onEmailErrorToggled(
|
||||
LoginEmailErrorToggled event,
|
||||
Emitter<LoginState> emit,
|
||||
) {
|
||||
emit(state.copyWith(showEmailError: event.show));
|
||||
}
|
||||
|
||||
void _onPasswordErrorToggled(
|
||||
LoginPasswordErrorToggled event,
|
||||
Emitter<LoginState> emit,
|
||||
) {
|
||||
emit(state.copyWith(showPasswordError: event.show));
|
||||
}
|
||||
}
|
||||
51
lib/login/blocs/login/login_event.dart
Normal file
51
lib/login/blocs/login/login_event.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
part of 'login_bloc.dart';
|
||||
|
||||
abstract class LoginEvent extends Equatable {
|
||||
const LoginEvent();
|
||||
|
||||
@override
|
||||
List<Object?> 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<Object?> get props => [emailAddress, password, rememberMe];
|
||||
}
|
||||
|
||||
class LoginPasswordVisibilityToggled extends LoginEvent {
|
||||
const LoginPasswordVisibilityToggled();
|
||||
}
|
||||
|
||||
class LoginRememberMeToggled extends LoginEvent {
|
||||
final bool value;
|
||||
const LoginRememberMeToggled(this.value);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [value];
|
||||
}
|
||||
|
||||
class LoginEmailErrorToggled extends LoginEvent {
|
||||
final bool show;
|
||||
const LoginEmailErrorToggled(this.show);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [show];
|
||||
}
|
||||
|
||||
class LoginPasswordErrorToggled extends LoginEvent {
|
||||
final bool show;
|
||||
const LoginPasswordErrorToggled(this.show);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [show];
|
||||
}
|
||||
54
lib/login/blocs/login/login_state.dart
Normal file
54
lib/login/blocs/login/login_state.dart
Normal file
@@ -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<Object?> get props => [
|
||||
status,
|
||||
isPasswordVisible,
|
||||
rememberMe,
|
||||
errorMessage,
|
||||
loginData,
|
||||
showEmailError,
|
||||
showPasswordError,
|
||||
];
|
||||
}
|
||||
93
lib/login/blocs/reset_password/reset_password_bloc.dart
Normal file
93
lib/login/blocs/reset_password/reset_password_bloc.dart
Normal file
@@ -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<ResetPasswordEvent, ResetPasswordState> {
|
||||
final ResetPasswordRepository _resetPasswordRepository;
|
||||
|
||||
ResetPasswordBloc({ResetPasswordRepository? resetPasswordRepository})
|
||||
: _resetPasswordRepository =
|
||||
resetPasswordRepository ?? ResetPasswordRepository(),
|
||||
super(const ResetPasswordState()) {
|
||||
on<ResetPasswordSubmitted>(_onResetPasswordSubmitted);
|
||||
on<ResetPasswordVisibilityToggled>(_onResetPasswordVisibilityToggled);
|
||||
on<ConfirmPasswordVisibilityToggled>(_onConfirmPasswordVisibilityToggled);
|
||||
on<PasswordChanged>(_onPasswordChanged);
|
||||
on<ResetPasswordErrorToggled>(_onPasswordErrorToggled);
|
||||
on<ResetConfirmPasswordErrorToggled>(_onConfirmPasswordErrorToggled);
|
||||
}
|
||||
|
||||
Future<void> _onResetPasswordSubmitted(
|
||||
ResetPasswordSubmitted event,
|
||||
Emitter<ResetPasswordState> 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<ResetPasswordState> emit,
|
||||
) {
|
||||
emit(state.copyWith(isPasswordVisible: !state.isPasswordVisible));
|
||||
}
|
||||
|
||||
void _onConfirmPasswordVisibilityToggled(
|
||||
ConfirmPasswordVisibilityToggled event,
|
||||
Emitter<ResetPasswordState> emit,
|
||||
) {
|
||||
emit(state.copyWith(
|
||||
isConfirmPasswordVisible: !state.isConfirmPasswordVisible));
|
||||
}
|
||||
|
||||
void _onPasswordChanged(
|
||||
PasswordChanged event,
|
||||
Emitter<ResetPasswordState> 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<ResetPasswordState> emit,
|
||||
) {
|
||||
emit(state.copyWith(
|
||||
showPasswordError: event.show,
|
||||
passwordErrorText: event.error,
|
||||
));
|
||||
}
|
||||
|
||||
void _onConfirmPasswordErrorToggled(
|
||||
ResetConfirmPasswordErrorToggled event,
|
||||
Emitter<ResetPasswordState> emit,
|
||||
) {
|
||||
emit(state.copyWith(
|
||||
showConfirmPasswordError: event.show,
|
||||
confirmPasswordErrorText: event.error,
|
||||
));
|
||||
}
|
||||
}
|
||||
55
lib/login/blocs/reset_password/reset_password_event.dart
Normal file
55
lib/login/blocs/reset_password/reset_password_event.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
part of 'reset_password_bloc.dart';
|
||||
|
||||
abstract class ResetPasswordEvent extends Equatable {
|
||||
const ResetPasswordEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ResetPasswordSubmitted extends ResetPasswordEvent {
|
||||
final String emailAddress;
|
||||
final String newPassword;
|
||||
|
||||
const ResetPasswordSubmitted({
|
||||
required this.emailAddress,
|
||||
required this.newPassword,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> 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<Object?> get props => [password];
|
||||
}
|
||||
|
||||
class ResetPasswordErrorToggled extends ResetPasswordEvent {
|
||||
final bool show;
|
||||
final String? error;
|
||||
const ResetPasswordErrorToggled(this.show, {this.error});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [show, error];
|
||||
}
|
||||
|
||||
class ResetConfirmPasswordErrorToggled extends ResetPasswordEvent {
|
||||
final bool show;
|
||||
final String? error;
|
||||
const ResetConfirmPasswordErrorToggled(this.show, {this.error});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [show, error];
|
||||
}
|
||||
90
lib/login/blocs/reset_password/reset_password_state.dart
Normal file
90
lib/login/blocs/reset_password/reset_password_state.dart
Normal file
@@ -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<Object?> get props => [
|
||||
status,
|
||||
isPasswordVisible,
|
||||
isConfirmPasswordVisible,
|
||||
errorMessage,
|
||||
hasMinLength,
|
||||
hasUppercase,
|
||||
hasLowercase,
|
||||
hasNumber,
|
||||
hasSpecial,
|
||||
showPasswordError,
|
||||
showConfirmPasswordError,
|
||||
passwordErrorText,
|
||||
confirmPasswordErrorText,
|
||||
];
|
||||
}
|
||||
37
lib/login/blocs/verify_otp/verify_otp_bloc.dart
Normal file
37
lib/login/blocs/verify_otp/verify_otp_bloc.dart
Normal file
@@ -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<VerifyOtpEvent, VerifyOtpState> {
|
||||
final OtpRepository _otpRepository;
|
||||
|
||||
VerifyOtpBloc({OtpRepository? otpRepository})
|
||||
: _otpRepository = otpRepository ?? OtpRepository(),
|
||||
super(const VerifyOtpState()) {
|
||||
on<VerifyOtpSubmitted>(_onVerifyOtpSubmitted);
|
||||
}
|
||||
|
||||
Future<void> _onVerifyOtpSubmitted(
|
||||
VerifyOtpSubmitted event,
|
||||
Emitter<VerifyOtpState> 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: ', ''),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
21
lib/login/blocs/verify_otp/verify_otp_event.dart
Normal file
21
lib/login/blocs/verify_otp/verify_otp_event.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
part of 'verify_otp_bloc.dart';
|
||||
|
||||
abstract class VerifyOtpEvent extends Equatable {
|
||||
const VerifyOtpEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class VerifyOtpSubmitted extends VerifyOtpEvent {
|
||||
final String emailAddress;
|
||||
final String otp;
|
||||
|
||||
const VerifyOtpSubmitted({
|
||||
required this.emailAddress,
|
||||
required this.otp,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [emailAddress, otp];
|
||||
}
|
||||
26
lib/login/blocs/verify_otp/verify_otp_state.dart
Normal file
26
lib/login/blocs/verify_otp/verify_otp_state.dart
Normal file
@@ -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<Object?> get props => [status, errorMessage];
|
||||
}
|
||||
@@ -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<String, dynamic> json) {
|
||||
return LoginModel(
|
||||
accessToken: json['accessToken'] ?? '',
|
||||
refreshToken: json['refreshToken'] ?? '',
|
||||
refreshMaxAge: json['refreshMaxAge'] ?? 0,
|
||||
partner: PartnerModel.fromJson(json['partner']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> json) {
|
||||
return PartnerModel(
|
||||
id: json['id'] ?? 0,
|
||||
email: json['email'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
roleXid: json['roleXid'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'email': email,
|
||||
'name': name,
|
||||
'roleXid': roleXid,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LoginModel> 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<String, dynamic>);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordView extends StatelessWidget {
|
||||
const _ForgotPasswordView();
|
||||
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||
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<ForgotPasswordBloc>().add(ForgotPasswordEmailErrorToggled(!isEmailValid));
|
||||
|
||||
if (isEmailValid) {
|
||||
context.read<ForgotPasswordBloc>().add(
|
||||
ForgotPasswordSubmitted(emailAddress: email),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<ForgotPasswordBloc>();
|
||||
|
||||
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<ForgotPasswordBloc, ForgotPasswordState>(
|
||||
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<ForgotPasswordBloc, ForgotPasswordState>(
|
||||
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<Color>(
|
||||
// (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<Color>(
|
||||
(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<ForgotPasswordBloc>().add(
|
||||
ForgotPasswordEmailErrorToggled(!_isEmailValid(val.trim())),
|
||||
);
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) => _onSendLinkPressed(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// ===== SEND LINK BUTTON =====
|
||||
CustomButton(
|
||||
text: "Send OTP",
|
||||
isLoading: isLoading,
|
||||
onPressed: () => _onSendLinkPressed(context),
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<LoginPage> {
|
||||
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<LoginBloc>().add(LoginEmailErrorToggled(!isEmailValid));
|
||||
context.read<LoginBloc>().add(LoginPasswordErrorToggled(!isPasswordValid));
|
||||
|
||||
if (isEmailValid && isPasswordValid) {
|
||||
context.read<LoginBloc>().add(
|
||||
LoginSubmitted(
|
||||
emailAddress: email,
|
||||
password: password,
|
||||
rememberMe: context.read<LoginBloc>().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<LoginBloc, LoginState>(
|
||||
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<LoginBloc>().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<LoginBloc>().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<LoginBloc>().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<LoginBloc>().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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<OtpVerificationPage> createState() => _OtpVerificationPageState();
|
||||
}
|
||||
|
||||
class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||
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<VerifyOtpBloc, VerifyOtpState>(
|
||||
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<OtpBloc, OtpState>(
|
||||
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<OtpBloc>();
|
||||
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<OtpBloc, OtpState>(
|
||||
builder: (context, state) {
|
||||
final otpBloc = context.read<OtpBloc>();
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: state.isOtpFilled && !state.isLoading
|
||||
? () => otpBloc.add(OtpVerify())
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
MaterialStateProperty.resolveWith<Color>((
|
||||
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<VerifyOtpBloc>().add(
|
||||
VerifyOtpSubmitted(
|
||||
emailAddress: widget.email,
|
||||
otp: verificationCode,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ===== VERIFY BUTTON =====
|
||||
CustomButton(
|
||||
text: "Verify",
|
||||
isLoading: isLoading,
|
||||
onPressed: _otp.length == 6
|
||||
? () {
|
||||
context.read<VerifyOtpBloc>().add(
|
||||
VerifyOtpSubmitted(
|
||||
emailAddress: widget.email,
|
||||
otp: _otp,
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
SizedBox(height: 24.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<ResetPasswordPage> createState() => _ResetPasswordPageState();
|
||||
}
|
||||
|
||||
class _ResetPasswordView extends StatelessWidget {
|
||||
const _ResetPasswordView();
|
||||
class _ResetPasswordPageState extends State<ResetPasswordPage> {
|
||||
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<ResetPasswordBloc>().add(
|
||||
const ResetPasswordErrorToggled(true,
|
||||
error: "Please fulfill all password requirements"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (password != confirmPassword) {
|
||||
context.read<ResetPasswordBloc>().add(
|
||||
const ResetConfirmPasswordErrorToggled(true,
|
||||
error: "Passwords do not match"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<ResetPasswordBloc>().add(
|
||||
ResetPasswordSubmitted(
|
||||
emailAddress: widget.email,
|
||||
newPassword: password,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<ResetPasswordBloc>();
|
||||
return GestureDetector(
|
||||
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.backgroundWhite,
|
||||
body: BlocConsumer<ResetPasswordBloc, ResetPasswordState>(
|
||||
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<ResetPasswordBloc>().add(
|
||||
const ResetPasswordVisibilityToggled(),
|
||||
);
|
||||
},
|
||||
hasError: state.showPasswordError,
|
||||
errorText: state.passwordErrorText,
|
||||
textInputAction: TextInputAction.next,
|
||||
readOnly: isLoading,
|
||||
onChanged: (val) {
|
||||
context
|
||||
.read<ResetPasswordBloc>()
|
||||
.add(PasswordChanged(val));
|
||||
if (state.showPasswordError) {
|
||||
context.read<ResetPasswordBloc>().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<ResetPasswordBloc>().add(
|
||||
const ConfirmPasswordVisibilityToggled(),
|
||||
);
|
||||
},
|
||||
hasError: state.showConfirmPasswordError,
|
||||
errorText: state.confirmPasswordErrorText,
|
||||
textInputAction: TextInputAction.done,
|
||||
readOnly: isLoading,
|
||||
onChanged: (val) {
|
||||
if (state.showConfirmPasswordError) {
|
||||
context.read<ResetPasswordBloc>().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<ResetPasswordBloc, ResetPasswordState>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<void> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
266
lib/network_api_service/api_service/api_service.dart
Normal file
266
lib/network_api_service/api_service/api_service.dart
Normal file
@@ -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<Response> get(
|
||||
String endpoint, {
|
||||
Map<String, dynamic>? 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<Response> post(
|
||||
String endpoint, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? 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<Response> put(
|
||||
String endpoint, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? 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<Response> delete(
|
||||
String endpoint, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? 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<bool> _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<void> _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<String, dynamic>) {
|
||||
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<String, dynamic> headers) {
|
||||
_dio.options.headers.addAll(headers);
|
||||
}
|
||||
}
|
||||
18
lib/network_api_service/api_urls/api_urls.dart
Normal file
18
lib/network_api_service/api_urls/api_urls.dart
Normal file
@@ -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";
|
||||
|
||||
}
|
||||
27
lib/profile/blocs/profile/profile_bloc.dart
Normal file
27
lib/profile/blocs/profile/profile_bloc.dart
Normal file
@@ -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<ProfileEvent, ProfileState> {
|
||||
final ProfileRepository _profileRepository;
|
||||
|
||||
ProfileBloc({required ProfileRepository profileRepository})
|
||||
: _profileRepository = profileRepository,
|
||||
super(ProfileInitial()) {
|
||||
on<FetchUserDetailsEvent>(_onFetchUserDetails);
|
||||
}
|
||||
|
||||
Future<void> _onFetchUserDetails(
|
||||
FetchUserDetailsEvent event,
|
||||
Emitter<ProfileState> emit,
|
||||
) async {
|
||||
emit(ProfileLoading());
|
||||
try {
|
||||
final userDetails = await _profileRepository.fetchUserDetails();
|
||||
emit(ProfileLoaded(userDetails: userDetails));
|
||||
} catch (e) {
|
||||
emit(ProfileError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
lib/profile/blocs/profile/profile_event.dart
Normal file
10
lib/profile/blocs/profile/profile_event.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ProfileEvent extends Equatable {
|
||||
const ProfileEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class FetchUserDetailsEvent extends ProfileEvent {}
|
||||
31
lib/profile/blocs/profile/profile_state.dart
Normal file
31
lib/profile/blocs/profile/profile_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../profile/models/profile_model.dart';
|
||||
|
||||
abstract class ProfileState extends Equatable {
|
||||
const ProfileState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class ProfileInitial extends ProfileState {}
|
||||
|
||||
class ProfileLoading extends ProfileState {}
|
||||
|
||||
class ProfileLoaded extends ProfileState {
|
||||
final UserDetails userDetails;
|
||||
|
||||
const ProfileLoaded({required this.userDetails});
|
||||
|
||||
@override
|
||||
List<Object> get props => [userDetails];
|
||||
}
|
||||
|
||||
class ProfileError extends ProfileState {
|
||||
final String message;
|
||||
|
||||
const ProfileError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -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<ProfileState> {
|
||||
void logout() {
|
||||
// Handle logout logic here
|
||||
print("User logged out");
|
||||
// LocalPreference.clearAll();
|
||||
// Navigator.pushNamedAndRemoveUntil(
|
||||
// context,
|
||||
// AppRouter.login,
|
||||
// (route) => false,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
91
lib/profile/models/profile_model.dart
Normal file
91
lib/profile/models/profile_model.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
class Role {
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
Role({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory Role.fromJson(Map<String, dynamic> json) {
|
||||
return Role(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
lib/profile/repository/profile_repository.dart
Normal file
21
lib/profile/repository/profile_repository.dart
Normal file
@@ -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<UserDetails> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ProfileCubit, ProfileState>(
|
||||
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<ProfileBloc, ProfileState>(
|
||||
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<ProfileBloc>(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<ProfileCubit>().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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<QrScanScreen>
|
||||
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<QrScanScreen>
|
||||
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<QrScanBloc>().add(ResetQrScanEvent());
|
||||
} else if (sheetController.isAttached) {
|
||||
await sheetController.animateTo(
|
||||
@@ -361,7 +362,11 @@ class _QrScanScreenState extends State<QrScanScreen>
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Image.asset(e['img']!, scale: 4),
|
||||
child: Image.asset(
|
||||
e['img']!,
|
||||
scale: isHome ? 6 : 4,
|
||||
color: isHome ? Colors.black : null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
34
lib/splash/bloc/splash_bloc.dart
Normal file
34
lib/splash/bloc/splash_bloc.dart
Normal file
@@ -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<SplashEvent, SplashState> {
|
||||
SplashBloc() : super(SplashInitial()) {
|
||||
on<SplashStarted>(_onSplashStarted);
|
||||
}
|
||||
|
||||
Future<void> _onSplashStarted(
|
||||
SplashStarted event,
|
||||
Emitter<SplashState> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
5
lib/splash/bloc/splash_event.dart
Normal file
5
lib/splash/bloc/splash_event.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
part of 'splash_bloc.dart';
|
||||
|
||||
abstract class SplashEvent {}
|
||||
|
||||
class SplashStarted extends SplashEvent {}
|
||||
13
lib/splash/bloc/splash_state.dart
Normal file
13
lib/splash/bloc/splash_state.dart
Normal file
@@ -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
|
||||
@@ -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<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/splash/view/splash_view.dart
Normal file
87
lib/splash/view/splash_view.dart
Normal file
@@ -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<SplashScreen> createState() => _SplashScreenState();
|
||||
}
|
||||
|
||||
class _SplashScreenState extends State<SplashScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _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<SplashBloc>().add(SplashStarted());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<SplashBloc, SplashState>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
116
pubspec.lock
116
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user