Files
ProxyTrash/lib/screens/auth_screen.dart
2026-05-23 05:33:39 -06:00

461 lines
16 KiB
Dart

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),
),
);
}
}