import 'package:flutter/material.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:prosapp_web_app/router/router.dart'; import 'package:prosapp_web_app/services/navigation_service.dart'; import 'package:provider/provider.dart'; class EmailView extends StatefulWidget { const EmailView({super.key}); @override State createState() => _EmailViewState(); } class _EmailViewState extends State { final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); final _confirmCtrl = TextEditingController(); final _otpCtrl = TextEditingController(); bool _step2 = false; // false = ingresar email+pass, true = ingresar OTP bool _loading = false; bool _obscurePass = true; bool _obscureConfirm = true; String? _emailError; String? _passError; String? _confirmError; String? _otpError; final _emailRegex = RegExp(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"); @override void dispose() { _emailCtrl.dispose(); _passCtrl.dispose(); _confirmCtrl.dispose(); _otpCtrl.dispose(); super.dispose(); } bool _validateStep1() { String? eErr, pErr, cErr; if (!_emailRegex.hasMatch(_emailCtrl.text.trim())) { eErr = 'Ingresa un email válido'; } if (_passCtrl.text.length < 6) { pErr = 'Mínimo 6 caracteres'; } else if (!_passCtrl.text.contains(RegExp(r'[0-9]'))) { pErr = 'Debe contener al menos un número'; } if (_confirmCtrl.text != _passCtrl.text) { cErr = 'Las contraseñas no coinciden'; } setState(() { _emailError = eErr; _passError = pErr; _confirmError = cErr; }); return eErr == null && pErr == null && cErr == null; } Future _sendOtp() async { if (!_validateStep1()) return; setState(() => _loading = true); try { final api = ApiService.instance; await api.post('/auth/send-email-otp', {'email': _emailCtrl.text.trim()}); setState(() { _step2 = true; _loading = false; }); NotificationsService.showSnackbar( 'Código enviado a ${_emailCtrl.text.trim()}'); } catch (e) { setState(() => _loading = false); NotificationsService.showSnackbar('Error: ${e.toString()}'); } } Future _verifyOtp() async { final code = _otpCtrl.text.trim(); if (code.length != 6) { setState(() => _otpError = 'Ingresa el código de 6 dígitos'); return; } setState(() { _loading = true; _otpError = null; }); try { await context.read().linkEmailWithOtp( _emailCtrl.text.trim(), _passCtrl.text, code, ); if (mounted) NavigationService.replaceTo(Flurorouter.profileRoute); } catch (e) { setState(() { _loading = false; _otpError = 'Código incorrecto o expirado'; }); } } @override Widget build(BuildContext context) { final isDark = context.watch().isDark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); final textPrimary = isDark ? Colors.white : const Color(0xFF111827); final textSec = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), child: Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(16), border: Border.all(color: border), ), child: _step2 ? _buildStep2(textPrimary, textSec, isDark, border) : _buildStep1(textPrimary, textSec, isDark, border), ), ), ), ); } // ── Paso 1: email + contraseña ────────────────────────────────────────────── Widget _buildStep1(Color textPrimary, Color textSec, bool isDark, Color border) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _Header( icon: Icons.email_outlined, title: 'Vincular correo', subtitle: 'Te enviaremos un código de verificación a tu correo.', ), const SizedBox(height: 24), _Field( label: 'Correo electrónico', controller: _emailCtrl, error: _emailError, isDark: isDark, icon: Icons.email_outlined, keyboardType: TextInputType.emailAddress, onChanged: (_) => setState(() => _emailError = null), ), const SizedBox(height: 14), _Field( label: 'Contraseña', controller: _passCtrl, error: _passError, isDark: isDark, icon: Icons.lock_outline, obscure: _obscurePass, onChanged: (_) => setState(() => _passError = null), suffix: IconButton( icon: Icon(_obscurePass ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: const Color(0xFF42A4EF)), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), const SizedBox(height: 14), _Field( label: 'Confirmar contraseña', controller: _confirmCtrl, error: _confirmError, isDark: isDark, icon: Icons.lock_outline, obscure: _obscureConfirm, onChanged: (_) => setState(() => _confirmError = null), suffix: IconButton( icon: Icon(_obscureConfirm ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: const Color(0xFF42A4EF)), onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), const SizedBox(height: 24), _SubmitBtn( label: 'Enviar código al correo', icon: Icons.send_outlined, loading: _loading, onPressed: _sendOtp, ), ], ); } // ── Paso 2: código OTP ────────────────────────────────────────────────────── Widget _buildStep2(Color textPrimary, Color textSec, bool isDark, Color border) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _Header( icon: Icons.mark_email_read_outlined, title: 'Revisa tu correo', subtitle: 'Ingresa el código de 6 dígitos que te enviamos.', ), const SizedBox(height: 8), Text(_emailCtrl.text.trim(), style: const TextStyle( color: Color(0xFF42A4EF), fontWeight: FontWeight.w600)), const SizedBox(height: 24), _Field( label: 'Código de verificación', controller: _otpCtrl, error: _otpError, isDark: isDark, icon: Icons.pin_outlined, keyboardType: TextInputType.number, onChanged: (_) => setState(() => _otpError = null), ), const SizedBox(height: 20), _SubmitBtn( label: 'Verificar y vincular', icon: Icons.verified_outlined, loading: _loading, onPressed: _verifyOtp, ), const SizedBox(height: 12), Center( child: TextButton.icon( icon: const Icon(Icons.arrow_back, size: 16), label: const Text('Cambiar correo'), onPressed: () => setState(() { _step2 = false; _otpCtrl.clear(); }), ), ), Center( child: TextButton( onPressed: _loading ? null : _sendOtp, child: const Text('Reenviar código'), ), ), ], ); } } // ── Helpers ─────────────────────────────────────────────────────────────────── class _Header extends StatelessWidget { final IconData icon; final String title; final String subtitle; const _Header({required this.icon, required this.title, required this.subtitle}); @override Widget build(BuildContext context) { final isDark = context.watch().isDark; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: const Color(0xFF42A4EF).withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: const Color(0xFF42A4EF), size: 26), ), const SizedBox(height: 16), Text(title, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF111827), )), const SizedBox(height: 4), Text(subtitle, style: TextStyle( fontSize: 13, color: isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280), )), ], ); } } class _Field extends StatelessWidget { final String label; final TextEditingController controller; final String? error; final bool isDark; final IconData icon; final bool obscure; final TextInputType? keyboardType; final Widget? suffix; final ValueChanged? onChanged; const _Field({ required this.label, required this.controller, required this.error, required this.isDark, required this.icon, this.obscure = false, this.keyboardType, this.suffix, this.onChanged, }); @override Widget build(BuildContext context) { final fill = isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB); final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFD1D5DB); final hintColor = isDark ? const Color(0xFF64748B) : const Color(0xFF9CA3AF); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: controller, obscureText: obscure, keyboardType: keyboardType, onChanged: onChanged, style: TextStyle( color: isDark ? Colors.white : const Color(0xFF111827), fontSize: 14), decoration: InputDecoration( labelText: label, labelStyle: TextStyle(color: hintColor, fontSize: 13), prefixIcon: Icon(icon, color: const Color(0xFF42A4EF), size: 20), suffixIcon: suffix, filled: true, fillColor: fill, errorText: null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: borderColor)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( color: error != null ? Colors.redAccent : borderColor)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), ), ), if (error != null) Padding( padding: const EdgeInsets.only(top: 5, left: 4), child: Text(error!, style: const TextStyle( color: Colors.redAccent, fontSize: 11)), ), ], ); } } class _SubmitBtn extends StatelessWidget { final String label; final IconData icon; final bool loading; final VoidCallback onPressed; const _SubmitBtn( {required this.label, required this.icon, required this.loading, required this.onPressed}); @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, height: 48, child: ElevatedButton.icon( icon: loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : Icon(icon, size: 18), label: Text(label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), elevation: 0, ), onPressed: loading ? null : onPressed, ), ); } }