302 lines
12 KiB
Dart
302 lines
12 KiB
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 '../../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});
|
|
|
|
@override
|
|
State<LoginPage> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends State<LoginPage> {
|
|
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'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$')
|
|
.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 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) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text("Login Successful."),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
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: AppColors.errorLight,
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
border:
|
|
Border.all(color: AppColors.primaryRed.withOpacity(0.5)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ===== 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())),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |