2024-11-11 08:44:31 +00:00
|
|
|
import 'package:english_learning/core/widgets/custom_snackbar.dart';
|
2024-10-10 05:49:33 +00:00
|
|
|
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';
|
|
|
|
|
|
2024-11-25 23:19:28 +00:00
|
|
|
class SigninScreen extends StatefulWidget {
|
|
|
|
|
const SigninScreen({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<SigninScreen> createState() => _SigninScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _SigninScreenState extends State<SigninScreen> {
|
2024-12-15 13:50:15 +00:00
|
|
|
late TextEditingController _loginController = TextEditingController();
|
|
|
|
|
late TextEditingController _passwordController = TextEditingController();
|
|
|
|
|
late ValidatorProvider _validatorProvider;
|
2024-12-02 03:18:16 +00:00
|
|
|
final FocusNode _loginFocus = FocusNode();
|
2024-11-25 23:19:28 +00:00
|
|
|
final FocusNode _passwordFocus = FocusNode();
|
|
|
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2024-12-15 13:50:15 +00:00
|
|
|
_loginController = TextEditingController();
|
|
|
|
|
_passwordController = TextEditingController();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didChangeDependencies() {
|
|
|
|
|
super.didChangeDependencies();
|
|
|
|
|
_validatorProvider = context.read<ValidatorProvider>();
|
|
|
|
|
_validatorProvider.setController('login', _loginController);
|
|
|
|
|
_validatorProvider.setController('password', _passwordController);
|
2024-11-25 23:19:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2024-12-15 13:50:15 +00:00
|
|
|
_validatorProvider.removeController('login');
|
|
|
|
|
_validatorProvider.removeController('password');
|
2024-12-02 03:18:16 +00:00
|
|
|
|
|
|
|
|
_loginController.dispose();
|
2024-11-25 23:19:28 +00:00
|
|
|
_passwordController.dispose();
|
2024-12-02 03:18:16 +00:00
|
|
|
_loginFocus.dispose();
|
2024-11-25 23:19:28 +00:00
|
|
|
_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>();
|
2024-10-10 05:49:33 +00:00
|
|
|
|
2024-11-25 23:19:28 +00:00
|
|
|
try {
|
2024-12-02 03:18:16 +00:00
|
|
|
final loginCredential = _loginController.text.trim();
|
|
|
|
|
final password = _passwordController.text;
|
|
|
|
|
|
2024-11-25 23:19:28 +00:00
|
|
|
final isSuccess = await userProvider.login(
|
2024-12-02 03:18:16 +00:00
|
|
|
credential: loginCredential,
|
|
|
|
|
password: password,
|
2024-11-25 23:19:28 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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:
|
2024-12-02 03:18:16 +00:00
|
|
|
message = 'Please verify your account first to continue.';
|
2024-11-25 23:19:28 +00:00
|
|
|
break;
|
|
|
|
|
case 401:
|
2024-12-02 03:18:16 +00:00
|
|
|
message = 'Invalid credentials. Please try again.';
|
2024-11-25 23:19:28 +00:00
|
|
|
break;
|
|
|
|
|
case 429:
|
|
|
|
|
message = 'Too many attempts. Please try again later.';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CustomSnackBar.show(context, message: message, isError: true);
|
|
|
|
|
}
|
2024-10-10 05:49:33 +00:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2024-12-05 09:54:20 +00:00
|
|
|
final mediaQuery = MediaQuery.of(context);
|
|
|
|
|
final screenHeight = mediaQuery.size.height;
|
|
|
|
|
|
2024-11-25 23:19:28 +00:00
|
|
|
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: [
|
2024-12-05 09:54:20 +00:00
|
|
|
SizedBox(height: screenHeight * 0.02),
|
2024-11-25 23:19:28 +00:00
|
|
|
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,
|
2024-10-10 05:49:33 +00:00
|
|
|
),
|
2024-11-25 23:19:28 +00:00
|
|
|
),
|
|
|
|
|
const SizedBox(height: 26),
|
2024-12-05 09:54:20 +00:00
|
|
|
// SizedBox(height: screenHeight * 0.05),
|
2024-11-25 23:19:28 +00:00
|
|
|
Center(
|
|
|
|
|
child: Hero(
|
|
|
|
|
tag: 'login_illustration',
|
|
|
|
|
child: SvgPicture.asset(
|
|
|
|
|
'lib/features/auth/assets/images/login_illustration.svg',
|
|
|
|
|
width: 200,
|
2024-10-10 05:49:33 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2024-11-25 23:19:28 +00:00
|
|
|
const SizedBox(height: 30),
|
2024-12-05 09:54:20 +00:00
|
|
|
// SizedBox(height: screenHeight * 0.05),
|
2024-11-25 23:19:28 +00:00
|
|
|
CustomFieldWidget(
|
2024-12-02 03:18:16 +00:00
|
|
|
fieldName: 'login',
|
|
|
|
|
controller: _loginController,
|
|
|
|
|
focusNode: _loginFocus,
|
2024-11-25 23:19:28 +00:00
|
|
|
isRequired: true,
|
|
|
|
|
textInputAction: TextInputAction.next,
|
2024-12-02 03:18:16 +00:00
|
|
|
labelText: 'Email or NISN',
|
|
|
|
|
hintText: 'Enter Email or NISN Number',
|
2024-11-25 23:19:28 +00:00
|
|
|
keyboardType: TextInputType.emailAddress,
|
2024-12-02 07:12:42 +00:00
|
|
|
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');
|
|
|
|
|
},
|
2024-11-25 23:19:28 +00:00
|
|
|
onChanged: (value) =>
|
2024-12-02 07:12:42 +00:00
|
|
|
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)';
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
),
|
2024-11-25 23:19:28 +00:00
|
|
|
onFieldSubmitted: (_) {
|
|
|
|
|
FocusScope.of(context)
|
|
|
|
|
.requestFocus(_passwordFocus);
|
2024-10-31 09:03:22 +00:00
|
|
|
},
|
2024-12-02 07:12:42 +00:00
|
|
|
errorText: validatorProvider.getError('login'),
|
2024-11-25 23:19:28 +00:00
|
|
|
),
|
|
|
|
|
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,
|
2024-12-02 07:12:42 +00:00
|
|
|
validator: (value) {
|
|
|
|
|
validatorProvider.validateField('password', value,
|
|
|
|
|
validator:
|
|
|
|
|
validatorProvider.passwordValidator);
|
|
|
|
|
return validatorProvider.getError('password');
|
|
|
|
|
},
|
2024-11-25 23:19:28 +00:00
|
|
|
onChanged: (value) =>
|
|
|
|
|
validatorProvider.validateField(
|
|
|
|
|
'password',
|
|
|
|
|
value,
|
|
|
|
|
validator: validatorProvider.passwordValidator,
|
2024-10-10 05:49:33 +00:00
|
|
|
),
|
2024-11-25 23:19:28 +00:00
|
|
|
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) =>
|
2024-12-05 09:54:20 +00:00
|
|
|
const SignupScreen(),
|
2024-11-25 23:19:28 +00:00
|
|
|
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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
2024-10-10 05:49:33 +00:00
|
|
|
),
|
2024-12-15 13:50:15 +00:00
|
|
|
SizedBox(height: screenHeight * 0.1),
|
2024-11-25 23:19:28 +00:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2024-10-10 05:49:33 +00:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|