mobile_adaptive_learning/lib/features/auth/screens/signin/signin_screen.dart

358 lines
15 KiB
Dart
Raw Normal View History

import 'package:english_learning/core/widgets/custom_snackbar.dart';
import 'package:english_learning/features/auth/provider/user_provider.dart';
import 'package:english_learning/features/auth/provider/validator_provider.dart';
import 'package:english_learning/features/auth/screens/forgot_password/forgot_password_screen.dart';
import 'package:english_learning/features/auth/screens/signup/signup_screen.dart';
import 'package:english_learning/features/home/screens/home_screen.dart';
import 'package:english_learning/core/widgets/form_field/custom_field_widget.dart';
import 'package:english_learning/core/widgets/global_button.dart';
import 'package:english_learning/core/utils/styles/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class SigninScreen extends StatefulWidget {
const SigninScreen({super.key});
@override
State<SigninScreen> createState() => _SigninScreenState();
}
class _SigninScreenState extends State<SigninScreen> {
final TextEditingController _loginController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final FocusNode _loginFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
context.read<ValidatorProvider>().setController('login', _loginController);
context
.read<ValidatorProvider>()
.setController('password', _passwordController);
}
@override
void dispose() {
context.read<ValidatorProvider>().removeController('login');
context.read<ValidatorProvider>().removeController('password');
_loginController.dispose();
_passwordController.dispose();
_loginFocus.dispose();
_passwordFocus.dispose();
super.dispose();
}
Future<void> _handleLogin(BuildContext context) async {
if (!_formKey.currentState!.validate()) return;
final userProvider = context.read<UserProvider>();
final validatorProvider = context.read<ValidatorProvider>();
try {
final loginCredential = _loginController.text.trim();
final password = _passwordController.text;
final isSuccess = await userProvider.login(
credential: loginCredential,
password: password,
);
if (!mounted) return;
if (isSuccess) {
// Hide keyboard before navigation
FocusScope.of(context).unfocus();
await Navigator.pushAndRemoveUntil(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const HomeScreen(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 300),
),
(route) => false,
);
// Reset form
validatorProvider.resetFields();
} else {
_handleLoginError(context, userProvider.errorCode);
}
} catch (e) {
if (mounted) {
CustomSnackBar.show(
context,
message: 'An unexpected error occurred. Please try again.',
isError: true,
);
}
}
}
void _handleLoginError(BuildContext context, int? errorCode) {
String message = 'Login failed, please check your credentials';
switch (errorCode) {
case 403:
message = 'Please verify your account first to continue.';
break;
case 401:
message = 'Invalid credentials. Please try again.';
break;
case 429:
message = 'Too many attempts. Please try again later.';
break;
}
CustomSnackBar.show(context, message: message, isError: true);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: AppColors.whiteColor,
body: SafeArea(
child: Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Consumer<ValidatorProvider>(
builder: (context, validatorProvider, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
Hero(
tag: 'welcome_text',
child: Material(
color: Colors.transparent,
child: Text(
'Welcome Back!',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 30,
fontWeight: FontWeight.w900,
),
),
),
),
const SizedBox(height: 4),
Text(
'Login to continue your personalized learning journey.',
style: AppTextStyles.greyTextStyle.copyWith(
fontSize: 14,
),
),
const SizedBox(height: 26),
Center(
child: Hero(
tag: 'login_illustration',
child: SvgPicture.asset(
'lib/features/auth/assets/images/login_illustration.svg',
width: 200,
),
),
),
const SizedBox(height: 30),
CustomFieldWidget(
fieldName: 'login',
controller: _loginController,
focusNode: _loginFocus,
isRequired: true,
textInputAction: TextInputAction.next,
labelText: 'Email or NISN',
hintText: 'Enter Email or NISN Number',
keyboardType: TextInputType.emailAddress,
validator: (value) {
validatorProvider.validateField(
'login', value?.trim(), validator: (val) {
if (val == null || val.isEmpty) {
return 'Login credential cannot be empty';
}
val = val.trim();
if (val.contains('@')) {
return validatorProvider.emailValidator(val);
} else if (RegExp(r'^\d{10}$').hasMatch(val)) {
return null; // Valid NISN
} else {
return 'Please enter a valid email or NISN (10 digits)';
}
});
return validatorProvider.getError('login');
},
onChanged: (value) =>
validatorProvider.validateField(
'login',
value?.trim(),
validator: (val) {
if (val == null || val.isEmpty) {
return 'Login credential cannot be empty';
}
val = val.trim();
if (val.contains('@')) {
return validatorProvider.emailValidator(val);
} else if (RegExp(r'^\d{10}$').hasMatch(val)) {
return null; // Valid NISN
} else {
return 'Please enter a valid email or NISN (10 digits)';
}
},
),
onFieldSubmitted: (_) {
FocusScope.of(context)
.requestFocus(_passwordFocus);
},
errorText: validatorProvider.getError('login'),
),
const SizedBox(height: 14),
CustomFieldWidget(
fieldName: 'password',
controller: _passwordController,
focusNode: _passwordFocus,
isRequired: true,
textInputAction: TextInputAction.done,
labelText: 'Password',
hintText: 'Enter Your Password',
obscureText:
validatorProvider.isObscure('password'),
keyboardType: TextInputType.visiblePassword,
validator: (value) {
validatorProvider.validateField('password', value,
validator:
validatorProvider.passwordValidator);
return validatorProvider.getError('password');
},
onChanged: (value) =>
validatorProvider.validateField(
'password',
value,
validator: validatorProvider.passwordValidator,
),
onFieldSubmitted: (_) => _handleLogin(context),
onSuffixIconTap: () =>
validatorProvider.toggleVisibility('password'),
errorText: validatorProvider.getError('password'),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ForgotPasswordScreen(),
),
);
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: ShaderMask(
shaderCallback: (bounds) =>
AppColors.gradientTheme.createShader(
Rect.fromLTWH(
0, 0, bounds.width, bounds.height),
),
child: const Text(
'Forgot Password?',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
const SizedBox(height: 24),
Consumer<UserProvider>(
builder: (context, userProvider, _) {
return GlobalButton(
text: 'Login',
isLoading: userProvider.isLoading,
onPressed: userProvider.isLoading
? null
: () => _handleLogin(context),
);
},
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Haven\'t joined us yet? ',
style: AppTextStyles.blackTextStyle
.copyWith(fontSize: 14),
),
TextButton(
onPressed: () {
context
.read<ValidatorProvider>()
.resetFields();
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation,
secondaryAnimation) =>
SignupScreen(),
transitionsBuilder: (context, animation,
secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
);
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(0, 0),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'Sign Up Here',
style: AppTextStyles.blueTextStyle.copyWith(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
],
);
},
),
),
),
),
),
),
),
);
}
}