- ProfileView vuelve a StatefulWidget para inicializar ProfileFormProvider - Quitar import custom_inputs.dart sin usar - DropdownButtonFormField con ValueKey para forzar rebuild cuando cargan ciudades - Normalizacion de acentos en ciudad (cucuta == cucuta con/sin tilde) - _EmailCard siempre muestra el formulario (no requiere expandir) - Eliminado campo email read-only del formulario principal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
706 lines
26 KiB
Dart
706 lines
26 KiB
Dart
import 'dart:typed_data';
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:prosapp_web_app/models/city.dart';
|
||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||
import 'package:prosapp_web_app/providers/cities_provider.dart';
|
||
import 'package:prosapp_web_app/providers/profile_form_provider.dart';
|
||
import 'package:prosapp_web_app/router/router.dart';
|
||
import 'package:prosapp_web_app/services/api_service.dart';
|
||
import 'package:prosapp_web_app/services/navigation_service.dart';
|
||
import 'package:prosapp_web_app/services/notifications_service.dart';
|
||
import 'package:prosapp_web_app/ui/labels/custom_labels.dart';
|
||
import 'package:provider/provider.dart';
|
||
|
||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||
|
||
String _avatarUrl(String name) {
|
||
final encoded = Uri.encodeComponent(name.isEmpty ? 'U' : name);
|
||
return 'https://ui-avatars.com/api/?name=$encoded&background=42A4EF&color=fff&size=200&bold=true&rounded=true';
|
||
}
|
||
|
||
InputDecoration _field(String label, String hint, IconData icon,
|
||
{Color? fill, Color? border}) =>
|
||
InputDecoration(
|
||
labelText: label,
|
||
hintText: hint,
|
||
prefixIcon: Icon(icon, size: 20),
|
||
filled: true,
|
||
fillColor: fill,
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide: BorderSide.none),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide: BorderSide(color: border ?? Colors.grey.shade300)),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide:
|
||
const BorderSide(color: Color(0xFF42A4EF), width: 2)),
|
||
);
|
||
|
||
// ─── ProfileView ─────────────────────────────────────────────────────────────
|
||
|
||
class ProfileView extends StatefulWidget {
|
||
const ProfileView({super.key});
|
||
|
||
@override
|
||
State<ProfileView> createState() => _ProfileViewState();
|
||
}
|
||
|
||
class _ProfileViewState extends State<ProfileView> {
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// Inicializa el form provider con el usuario actual
|
||
final auth = context.read<AuthProvider>();
|
||
final pfp = context.read<ProfileFormProvider>();
|
||
pfp.user = auth.user;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(builder: (context, c) {
|
||
final narrow = c.maxWidth < 700;
|
||
const body = _ProfileBody();
|
||
return narrow
|
||
? ListView(
|
||
physics: const ClampingScrollPhysics(),
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
children: [body],
|
||
)
|
||
: ListView(
|
||
physics: const ClampingScrollPhysics(),
|
||
padding:
|
||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||
children: [
|
||
Center(
|
||
child: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: 900),
|
||
child: body),
|
||
)
|
||
],
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── _ProfileBody ─────────────────────────────────────────────────────────────
|
||
|
||
class _ProfileBody extends StatelessWidget {
|
||
const _ProfileBody();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(builder: (context, c) {
|
||
if (c.maxWidth < 700) {
|
||
return const Column(children: [
|
||
_AvatarCard(),
|
||
SizedBox(height: 12),
|
||
_InfoCard(),
|
||
SizedBox(height: 12),
|
||
_EmailCard(),
|
||
SizedBox(height: 24),
|
||
]);
|
||
}
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: const [
|
||
SizedBox(width: 260, child: _AvatarCard()),
|
||
SizedBox(width: 16),
|
||
Expanded(
|
||
child: Column(children: [
|
||
_InfoCard(),
|
||
SizedBox(height: 12),
|
||
_EmailCard(),
|
||
]),
|
||
),
|
||
],
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── _AvatarCard ─────────────────────────────────────────────────────────────
|
||
|
||
class _AvatarCard extends StatelessWidget {
|
||
const _AvatarCard();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||
final user = context.watch<AuthProvider>().user!;
|
||
final pfp =
|
||
context.watch<ProfileFormProvider>().user?.picture;
|
||
final hasPic = pfp != null && pfp.isNotEmpty;
|
||
final imgUrl = hasPic ? pfp : _avatarUrl(user.name);
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.all(4),
|
||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
|
||
decoration: BoxDecoration(
|
||
color: cardBg,
|
||
borderRadius: BorderRadius.circular(12),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.06), blurRadius: 6)
|
||
],
|
||
),
|
||
child: Column(children: [
|
||
Text(user.name,
|
||
style: CustomLabels.h2, textAlign: TextAlign.center),
|
||
const SizedBox(height: 16),
|
||
Stack(children: [
|
||
ClipOval(
|
||
child: SizedBox(
|
||
width: 130,
|
||
height: 130,
|
||
child: Image.network(imgUrl, fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) => Image.network(
|
||
_avatarUrl(user.name),
|
||
fit: BoxFit.cover)),
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: 4,
|
||
right: 4,
|
||
child: _CameraButton(),
|
||
),
|
||
]),
|
||
const SizedBox(height: 12),
|
||
if (user.phone != null && user.phone!.isNotEmpty)
|
||
Text(user.phone!,
|
||
style: TextStyle(
|
||
color: isDark ? Colors.white54 : Colors.black45,
|
||
fontSize: 13)),
|
||
]),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CameraButton extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final pfp = context.watch<ProfileFormProvider>();
|
||
|
||
return GestureDetector(
|
||
onTap: () async {
|
||
final result = await FilePicker.platform.pickFiles(withData: true);
|
||
if (result == null) return;
|
||
final bytes = result.files.first.bytes;
|
||
if (bytes == null) return;
|
||
NotificationsService.showBusyIndicator(context);
|
||
await pfp.uploadPicture(bytes);
|
||
if (context.mounted) {
|
||
Navigator.pop(context);
|
||
context.read<AuthProvider>().refreshUser();
|
||
}
|
||
},
|
||
child: Container(
|
||
width: 34,
|
||
height: 34,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF42A4EF),
|
||
shape: BoxShape.circle,
|
||
border: Border.all(color: Colors.white, width: 2),
|
||
),
|
||
child: const Icon(Icons.camera_alt_outlined,
|
||
size: 16, color: Colors.white),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ─── _InfoCard ────────────────────────────────────────────────────────────────
|
||
|
||
class _InfoCard extends StatelessWidget {
|
||
const _InfoCard();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||
final fillColor =
|
||
isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFF);
|
||
final borderColor =
|
||
isDark ? const Color(0xFF334155) : const Color(0xFFE0E7FF);
|
||
|
||
final pfp = context.watch<ProfileFormProvider>();
|
||
final cities = context.watch<CitiesProvider>();
|
||
final user = pfp.user!;
|
||
|
||
// Ciudad que coincide con la guardada
|
||
String? currentCity;
|
||
if (user.city != null && user.city!.isNotEmpty && cities.cities.isNotEmpty) {
|
||
final saved = _normalize(user.city!);
|
||
currentCity = cities.cities
|
||
.map((c) => c.cityName)
|
||
.cast<String?>()
|
||
.firstWhere(
|
||
(n) => _normalize(n!) == saved,
|
||
orElse: () => null);
|
||
}
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.all(4),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: cardBg,
|
||
borderRadius: BorderRadius.circular(12),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.06), blurRadius: 6)
|
||
],
|
||
),
|
||
child: Form(
|
||
key: pfp.formKey,
|
||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Información personal',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 15,
|
||
color: isDark ? Colors.white : Colors.black87)),
|
||
const Divider(height: 20),
|
||
|
||
// Nombre
|
||
TextFormField(
|
||
initialValue: user.name,
|
||
validator: (v) => (v == null || v.trim().length < 3)
|
||
? 'Mínimo 3 caracteres'
|
||
: null,
|
||
onChanged: (v) => pfp.copyUserWith(name: v),
|
||
decoration: _field('Nombre', 'Tu nombre completo',
|
||
Icons.person_outline,
|
||
fill: fillColor, border: borderColor),
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// Teléfono (read-only)
|
||
TextFormField(
|
||
readOnly: true,
|
||
initialValue: user.phone ?? '',
|
||
onTap: (user.phone == null || user.phone!.isEmpty)
|
||
? () => NavigationService.navigateTo(Flurorouter.phoneRoute)
|
||
: null,
|
||
decoration: _field(
|
||
'Teléfono', 'Número de teléfono', Icons.phone,
|
||
fill: fillColor, border: borderColor),
|
||
),
|
||
const SizedBox(height: 12),
|
||
|
||
// Ciudad
|
||
cities.isLoading
|
||
? const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.symmetric(vertical: 16),
|
||
child: CircularProgressIndicator(),
|
||
),
|
||
)
|
||
: DropdownButtonFormField<String>(
|
||
key: ValueKey(cities.cities.length),
|
||
value: currentCity,
|
||
isExpanded: true,
|
||
validator: (v) =>
|
||
v == null ? 'Selecciona tu ciudad' : null,
|
||
decoration: _field(
|
||
'Ciudad', 'Selecciona tu ciudad',
|
||
Icons.location_city_outlined,
|
||
fill: fillColor, border: borderColor),
|
||
items: cities.cities.map((City c) {
|
||
return DropdownMenuItem<String>(
|
||
value: c.cityName,
|
||
child: Text(
|
||
'${c.cityName} – ${c.stateOfCity}',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: isDark
|
||
? Colors.white
|
||
: Colors.black87),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
}).toList(),
|
||
onChanged: (v) {
|
||
if (v != null) pfp.copyUserWith(city: v);
|
||
},
|
||
),
|
||
const SizedBox(height: 20),
|
||
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton(
|
||
onPressed: () async {
|
||
await pfp.updateUserInfo();
|
||
if (context.mounted) {
|
||
context.read<AuthProvider>().refreshUser();
|
||
NotificationsService.showSnackbar('Perfil actualizado');
|
||
}
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF42A4EF),
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10)),
|
||
elevation: 0,
|
||
),
|
||
child: const Text('Guardar cambios',
|
||
style: TextStyle(fontWeight: FontWeight.w600)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
String _normalize(String s) => s
|
||
.toLowerCase()
|
||
.trim()
|
||
.replaceAll('á', 'a')
|
||
.replaceAll('é', 'e')
|
||
.replaceAll('í', 'i')
|
||
.replaceAll('ó', 'o')
|
||
.replaceAll('ú', 'u')
|
||
.replaceAll('ü', 'u')
|
||
.replaceAll('ñ', 'n');
|
||
}
|
||
|
||
// ─── _EmailCard ───────────────────────────────────────────────────────────────
|
||
|
||
class _EmailCard extends StatefulWidget {
|
||
const _EmailCard();
|
||
@override
|
||
State<_EmailCard> createState() => _EmailCardState();
|
||
}
|
||
|
||
class _EmailCardState extends State<_EmailCard> {
|
||
final _emailCtrl = TextEditingController();
|
||
final _passCtrl = TextEditingController();
|
||
final _confirmCtrl = TextEditingController();
|
||
final _codeCtrl = TextEditingController();
|
||
|
||
bool _codeSent = false;
|
||
bool _loading = false;
|
||
bool _obscurePass = true;
|
||
bool _obscureConfirm = true;
|
||
|
||
@override
|
||
void dispose() {
|
||
_emailCtrl.dispose();
|
||
_passCtrl.dispose();
|
||
_confirmCtrl.dispose();
|
||
_codeCtrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _sendCode() async {
|
||
final email = _emailCtrl.text.trim();
|
||
final pass = _passCtrl.text;
|
||
if (!RegExp(r'^[\w.+-]+@[\w-]+\.\w+$').hasMatch(email)) {
|
||
_err('Ingresa un correo válido'); return;
|
||
}
|
||
if (pass.length < 6) {
|
||
_err('La contraseña debe tener mínimo 6 caracteres'); return;
|
||
}
|
||
if (pass != _confirmCtrl.text) {
|
||
_err('Las contraseñas no coinciden'); return;
|
||
}
|
||
setState(() => _loading = true);
|
||
try {
|
||
await ApiService.instance
|
||
.post('/auth/send-email-otp', {'email': email});
|
||
setState(() => _codeSent = true);
|
||
NotificationsService.showSnackbar('Código enviado a $email');
|
||
} catch (_) {
|
||
_err('No se pudo enviar el código. Verifica el correo.');
|
||
} finally {
|
||
setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
Future<void> _verify() async {
|
||
final code = _codeCtrl.text.trim();
|
||
if (code.length != 6) { _err('Ingresa los 6 dígitos'); return; }
|
||
setState(() => _loading = true);
|
||
try {
|
||
await ApiService.instance.post('/auth/link-email-otp', {
|
||
'email': _emailCtrl.text.trim(),
|
||
'password': _passCtrl.text,
|
||
'code': code,
|
||
});
|
||
await context.read<AuthProvider>().refreshUser();
|
||
NotificationsService.showSnackbar('Correo vinculado correctamente');
|
||
} catch (_) {
|
||
_err('Código incorrecto o expirado');
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
void _err(String m) => NotificationsService.showSnackBarError(m);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||
final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||
final fillColor =
|
||
isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFF);
|
||
final borderColor =
|
||
isDark ? const Color(0xFF334155) : const Color(0xFFE0E7FF);
|
||
final user = context.watch<AuthProvider>().user!;
|
||
final hasEmail = user.email != null && user.email!.isNotEmpty;
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.all(4),
|
||
padding: const EdgeInsets.all(20),
|
||
decoration: BoxDecoration(
|
||
color: cardBg,
|
||
borderRadius: BorderRadius.circular(12),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.06), blurRadius: 6)
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// ─ Cabecera ─
|
||
Row(children: [
|
||
Container(
|
||
width: 38, height: 38,
|
||
decoration: BoxDecoration(
|
||
color: hasEmail
|
||
? const Color(0xFF10B981).withOpacity(0.12)
|
||
: const Color(0xFF42A4EF).withOpacity(0.12),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
hasEmail
|
||
? Icons.mark_email_read_outlined
|
||
: Icons.email_outlined,
|
||
color: hasEmail
|
||
? const Color(0xFF10B981)
|
||
: const Color(0xFF42A4EF),
|
||
size: 18,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
hasEmail
|
||
? 'Correo vinculado'
|
||
: 'Vincular correo electrónico',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
color: isDark ? Colors.white : Colors.black87),
|
||
),
|
||
Text(
|
||
hasEmail
|
||
? user.email!
|
||
: 'Inicia sesión también con email y contraseña',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: isDark ? Colors.white38 : Colors.black45),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (hasEmail)
|
||
const Icon(Icons.verified,
|
||
color: Color(0xFF10B981), size: 22),
|
||
]),
|
||
|
||
// ─ Formulario (solo si no tiene email) ─
|
||
if (!hasEmail) ...[
|
||
const SizedBox(height: 16),
|
||
const Divider(height: 1),
|
||
const SizedBox(height: 16),
|
||
|
||
if (!_codeSent) ...[
|
||
// Paso 1
|
||
TextField(
|
||
controller: _emailCtrl,
|
||
keyboardType: TextInputType.emailAddress,
|
||
decoration: _field('Correo electrónico',
|
||
'tu@correo.com', Icons.email_outlined,
|
||
fill: fillColor, border: borderColor),
|
||
),
|
||
const SizedBox(height: 10),
|
||
TextField(
|
||
controller: _passCtrl,
|
||
obscureText: _obscurePass,
|
||
decoration: _field('Contraseña',
|
||
'Mínimo 6 caracteres', Icons.lock_outline,
|
||
fill: fillColor, border: borderColor)
|
||
.copyWith(
|
||
suffixIcon: IconButton(
|
||
icon: Icon(
|
||
_obscurePass
|
||
? Icons.visibility_off_outlined
|
||
: Icons.visibility_outlined,
|
||
size: 18,
|
||
color: Colors.grey),
|
||
onPressed: () =>
|
||
setState(() => _obscurePass = !_obscurePass),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
TextField(
|
||
controller: _confirmCtrl,
|
||
obscureText: _obscureConfirm,
|
||
decoration: _field('Confirmar contraseña',
|
||
'Repite la contraseña', Icons.lock_outline,
|
||
fill: fillColor, border: borderColor)
|
||
.copyWith(
|
||
suffixIcon: IconButton(
|
||
icon: Icon(
|
||
_obscureConfirm
|
||
? Icons.visibility_off_outlined
|
||
: Icons.visibility_outlined,
|
||
size: 18,
|
||
color: Colors.grey),
|
||
onPressed: () =>
|
||
setState(() => _obscureConfirm = !_obscureConfirm),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton.icon(
|
||
onPressed: _loading ? null : _sendCode,
|
||
icon: _loading
|
||
? const SizedBox(
|
||
width: 16, height: 16,
|
||
child: CircularProgressIndicator(
|
||
color: Colors.white, strokeWidth: 2))
|
||
: const Icon(Icons.send_outlined, size: 18),
|
||
label: Text(_loading
|
||
? 'Enviando...'
|
||
: 'Enviar código de verificación'),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF42A4EF),
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10)),
|
||
elevation: 0,
|
||
),
|
||
),
|
||
),
|
||
] else ...[
|
||
// Paso 2 – ingresar código
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF42A4EF).withOpacity(0.08),
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(
|
||
color: const Color(0xFF42A4EF).withOpacity(0.3)),
|
||
),
|
||
child: Row(children: [
|
||
const Icon(Icons.mark_email_read_outlined,
|
||
color: Color(0xFF42A4EF), size: 18),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
'Código enviado a ${_emailCtrl.text.trim()}',
|
||
style: const TextStyle(
|
||
fontSize: 13, color: Color(0xFF42A4EF)),
|
||
),
|
||
),
|
||
]),
|
||
),
|
||
const SizedBox(height: 14),
|
||
TextField(
|
||
controller: _codeCtrl,
|
||
keyboardType: TextInputType.number,
|
||
textAlign: TextAlign.center,
|
||
maxLength: 6,
|
||
autofocus: true,
|
||
style: const TextStyle(
|
||
fontSize: 30,
|
||
fontWeight: FontWeight.bold,
|
||
letterSpacing: 14),
|
||
decoration: InputDecoration(
|
||
counterText: '',
|
||
hintText: '· · · · · ·',
|
||
hintStyle: TextStyle(
|
||
letterSpacing: 8, color: Colors.grey.shade400),
|
||
filled: true,
|
||
fillColor: fillColor,
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide: BorderSide.none),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide: BorderSide(color: borderColor)),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(10),
|
||
borderSide: const BorderSide(
|
||
color: Color(0xFF42A4EF), width: 2)),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
Row(children: [
|
||
Expanded(
|
||
child: OutlinedButton(
|
||
onPressed: _loading
|
||
? null
|
||
: () => setState(
|
||
() { _codeSent = false; _codeCtrl.clear(); }),
|
||
style: OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10)),
|
||
side: BorderSide(color: borderColor),
|
||
),
|
||
child: const Text('← Volver'),
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: ElevatedButton(
|
||
onPressed: _loading ? null : _verify,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF10B981),
|
||
foregroundColor: Colors.white,
|
||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(10)),
|
||
elevation: 0,
|
||
),
|
||
child: _loading
|
||
? const SizedBox(
|
||
width: 16, height: 16,
|
||
child: CircularProgressIndicator(
|
||
color: Colors.white, strokeWidth: 2))
|
||
: const Text('Verificar',
|
||
style: TextStyle(fontWeight: FontWeight.w600)),
|
||
),
|
||
),
|
||
]),
|
||
],
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|