461 lines
16 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
} |