add auth, format
This commit is contained in:
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
492
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,492 @@
|
||||
/// Login Page
|
||||
///
|
||||
/// Main authentication page for the Worker app.
|
||||
/// Allows users to login with phone number and password.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/password_visibility_provider.dart';
|
||||
import 'package:worker/features/auth/presentation/widgets/phone_input_field.dart';
|
||||
|
||||
/// Login Page
|
||||
///
|
||||
/// Provides phone and password authentication.
|
||||
/// On successful login, navigates to home page.
|
||||
/// Links to registration page for new users.
|
||||
///
|
||||
/// Features:
|
||||
/// - Phone number input with Vietnamese format validation
|
||||
/// - Password input with visibility toggle
|
||||
/// - Form validation
|
||||
/// - Loading states
|
||||
/// - Error handling with snackbar
|
||||
/// - Link to registration
|
||||
/// - Customer support link
|
||||
class LoginPage extends ConsumerStatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Form key for validation
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers
|
||||
final _phoneController = TextEditingController(text: "0988111111");
|
||||
final _passwordController = TextEditingController(text: "123456");
|
||||
|
||||
// Focus nodes
|
||||
final _phoneFocusNode = FocusNode();
|
||||
final _passwordFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
_passwordController.dispose();
|
||||
_phoneFocusNode.dispose();
|
||||
_passwordFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Handle login button press
|
||||
Future<void> _handleLogin() async {
|
||||
// Validate form
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unfocus keyboard
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
// Call login method
|
||||
await ref
|
||||
.read(authProvider.notifier)
|
||||
.login(
|
||||
phoneNumber: _phoneController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
);
|
||||
|
||||
// Check if login was successful
|
||||
final authState = ref.read(authProvider);
|
||||
authState.when(
|
||||
data: (user) {
|
||||
if (user != null && mounted) {
|
||||
// Navigate to home on success
|
||||
context.goHome();
|
||||
}
|
||||
},
|
||||
loading: () {},
|
||||
error: (error, stack) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(error.toString()),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Đăng nhập thất bại: ${e.toString()}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to register page
|
||||
void _navigateToRegister() {
|
||||
// TODO: Navigate to register page when route is set up
|
||||
// context.go('/register');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng đăng ký đang được phát triển'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show support dialog
|
||||
void _showSupport() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Hỗ trợ khách hàng'),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Hotline: 1900 xxxx'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Email: support@eurotile.vn'),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text('Giờ làm việc: 8:00 - 17:00 (T2-T6)'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch auth state for loading indicator
|
||||
final authState = ref.watch(authProvider);
|
||||
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Logo Section
|
||||
_buildLogo(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
_buildWelcomeMessage(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Login Form Card
|
||||
_buildLoginForm(authState, isPasswordVisible),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Register Link
|
||||
_buildRegisterLink(),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Support Link
|
||||
_buildSupportLink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build logo section
|
||||
Widget _buildLogo() {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0, vertical: 20.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
Text(
|
||||
'EUROTILE',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.0),
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 12.0,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build welcome message
|
||||
Widget _buildWelcomeMessage() {
|
||||
return const Column(
|
||||
children: [
|
||||
Text(
|
||||
'Xin chào!',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Đăng nhập để tiếp tục',
|
||||
style: TextStyle(fontSize: 16.0, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Build login form card
|
||||
Widget _buildLoginForm(
|
||||
AsyncValue<dynamic> authState,
|
||||
bool isPasswordVisible,
|
||||
) {
|
||||
final isLoading = authState.isLoading;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Phone Input
|
||||
PhoneInputField(
|
||||
controller: _phoneController,
|
||||
focusNode: _phoneFocusNode,
|
||||
validator: Validators.phone,
|
||||
enabled: !isLoading,
|
||||
onFieldSubmitted: (_) {
|
||||
// Move focus to password field
|
||||
FocusScope.of(context).requestFocus(_passwordFocusNode);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Password Input
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _passwordFocusNode,
|
||||
enabled: !isLoading,
|
||||
obscureText: !isPasswordVisible,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mật khẩu',
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.labelFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
hintText: 'Nhập mật khẩu',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock,
|
||||
color: AppColors.primaryBlue,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(passwordVisibilityProvider.notifier).toggle();
|
||||
},
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.danger,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.danger,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
errorStyle: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
),
|
||||
validator: (value) =>
|
||||
Validators.passwordSimple(value, minLength: 6),
|
||||
onFieldSubmitted: (_) {
|
||||
if (!isLoading) {
|
||||
_handleLogin();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Login Button
|
||||
SizedBox(
|
||||
height: ButtonSpecs.height,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
elevation: ButtonSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: ButtonSpecs.fontSize,
|
||||
fontWeight: ButtonSpecs.fontWeight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build register link
|
||||
Widget _buildRegisterLink() {
|
||||
return Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Chưa có tài khoản? ',
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: _navigateToRegister,
|
||||
child: const Text(
|
||||
'Đăng ký ngay',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build support link
|
||||
Widget _buildSupportLink() {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: _showSupport,
|
||||
icon: const Icon(
|
||||
Icons.headset_mic,
|
||||
size: AppIconSize.sm,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
label: const Text(
|
||||
'Hỗ trợ khách hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user