initial commit 2
This commit is contained in:
461
lib/screens/auth_screen.dart
Normal file
461
lib/screens/auth_screen.dart
Normal file
@@ -0,0 +1,461 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../app_config.dart';
|
||||
import '../models/auth_session.dart';
|
||||
import '../services/address_repository.dart';
|
||||
import '../services/auth_repository.dart';
|
||||
import 'address_screen.dart';
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({
|
||||
super.key,
|
||||
required this.authRepository,
|
||||
required this.addressRepository,
|
||||
this.enableLiveFeatures = true,
|
||||
});
|
||||
|
||||
final AuthRepository authRepository;
|
||||
final AddressRepository addressRepository;
|
||||
final bool enableLiveFeatures;
|
||||
|
||||
@override
|
||||
State<AuthScreen> createState() => _AuthScreenState();
|
||||
}
|
||||
|
||||
class _AuthScreenState extends State<AuthScreen> {
|
||||
final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
|
||||
final GlobalKey<FormState> _registerFormKey = GlobalKey<FormState>();
|
||||
final TextEditingController _loginEmailController = TextEditingController();
|
||||
final TextEditingController _loginPasswordController = TextEditingController();
|
||||
final TextEditingController _registerNameController = TextEditingController();
|
||||
final TextEditingController _registerEmailController = TextEditingController();
|
||||
final TextEditingController _registerPasswordController = TextEditingController();
|
||||
final TextEditingController _registerConfirmPasswordController = TextEditingController();
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_loginEmailController.dispose();
|
||||
_loginPasswordController.dispose();
|
||||
_registerNameController.dispose();
|
||||
_registerEmailController.dispose();
|
||||
_registerPasswordController.dispose();
|
||||
_registerConfirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _signIn() async {
|
||||
if (!(_loginFormKey.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _submit(() {
|
||||
return widget.authRepository.signIn(
|
||||
email: _loginEmailController.text.trim(),
|
||||
password: _loginPasswordController.text,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _signUp() async {
|
||||
if (!(_registerFormKey.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _submit(() {
|
||||
return widget.authRepository.signUp(
|
||||
name: _registerNameController.text.trim(),
|
||||
email: _registerEmailController.text.trim(),
|
||||
password: _registerPasswordController.text,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _submit(Future<AuthSession> Function() action) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final session = await action();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => AddressScreen(
|
||||
authRepository: widget.authRepository,
|
||||
addressRepository: widget.addressRepository,
|
||||
session: session,
|
||||
enableLiveFeatures: widget.enableLiveFeatures,
|
||||
),
|
||||
),
|
||||
);
|
||||
} on AuthException catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = error.message;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_errorMessage = 'No se pudo completar la operación. Verifica el backend y vuelve a intentar.';
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF06141B), Color(0xFF0F766E), Color(0xFFE2E8F0)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 440),
|
||||
child: Card(
|
||||
elevation: 18,
|
||||
color: Colors.white.withValues(alpha: 0.94),
|
||||
shadowColor: Colors.black26,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0F766E).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(Icons.lock_outline, size: 36, color: Color(0xFF0F766E)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Bienvenido',
|
||||
style: TextStyle(fontSize: 30, fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Inicia sesión o crea una cuenta para continuar. Luego irás a la pantalla Dirección.',
|
||||
style: TextStyle(color: Colors.grey.shade700, height: 1.4),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: TabBar(
|
||||
onTap: (_) {
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
},
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
dividerColor: Colors.transparent,
|
||||
indicator: BoxDecoration(
|
||||
color: const Color(0xFF0F766E),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Colors.grey.shade700,
|
||||
tabs: const [
|
||||
Tab(text: 'Entrar'),
|
||||
Tab(text: 'Crear cuenta'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (_errorMessage != null) ...[
|
||||
_AuthStatusBanner(message: _errorMessage!),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: TabBarView(
|
||||
children: [
|
||||
_LoginForm(
|
||||
formKey: _loginFormKey,
|
||||
emailController: _loginEmailController,
|
||||
passwordController: _loginPasswordController,
|
||||
onSubmit: _signIn,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
_RegisterForm(
|
||||
formKey: _registerFormKey,
|
||||
nameController: _registerNameController,
|
||||
emailController: _registerEmailController,
|
||||
passwordController: _registerPasswordController,
|
||||
confirmPasswordController: _registerConfirmPasswordController,
|
||||
onSubmit: _signUp,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Base URL configurada: ${AppConfig.apiBaseUrl}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginForm extends StatelessWidget {
|
||||
const _LoginForm({
|
||||
required this.formKey,
|
||||
required this.emailController,
|
||||
required this.passwordController,
|
||||
required this.onSubmit,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController emailController;
|
||||
final TextEditingController passwordController;
|
||||
final Future<void> Function() onSubmit;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Correo electrónico',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ingresa tu correo';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Ingresa un correo válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Contraseña',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ingresa tu contraseña';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Usa al menos 6 caracteres';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: FilledButton(
|
||||
onPressed: isLoading ? null : onSubmit,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.2, color: Colors.white),
|
||||
)
|
||||
: const Text('Ingresar'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RegisterForm extends StatelessWidget {
|
||||
const _RegisterForm({
|
||||
required this.formKey,
|
||||
required this.nameController,
|
||||
required this.emailController,
|
||||
required this.passwordController,
|
||||
required this.confirmPasswordController,
|
||||
required this.onSubmit,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController emailController;
|
||||
final TextEditingController passwordController;
|
||||
final TextEditingController confirmPasswordController;
|
||||
final Future<void> Function() onSubmit;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nombre',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ingresa tu nombre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Correo electrónico',
|
||||
prefixIcon: Icon(Icons.email_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Ingresa tu correo';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Ingresa un correo válido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Contraseña',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Ingresa una contraseña';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Usa al menos 6 caracteres';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: confirmPasswordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirmar contraseña',
|
||||
prefixIcon: Icon(Icons.lock_reset_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Confirma tu contraseña';
|
||||
}
|
||||
if (value != passwordController.text) {
|
||||
return 'Las contraseñas no coinciden';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 52,
|
||||
child: FilledButton(
|
||||
onPressed: isLoading ? null : onSubmit,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2.2, color: Colors.white),
|
||||
)
|
||||
: const Text('Registrarme'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AuthStatusBanner extends StatelessWidget {
|
||||
const _AuthStatusBanner({required this.message});
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFEE2E2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFFCA5A5)),
|
||||
),
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(color: Color(0xFF991B1B), height: 1.35),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user