Compare commits

...
Author SHA1 Message Date
Forte11Cuba 9e3622435b fix(receive): use localized string for invalid token error 2026-02-07 22:54:54 -06:00
Forte11Cuba b6c5b42cf2 fix(receive): add catch block for save-for-later errors 2026-02-07 22:44:46 -06:00
Forte11Cuba 835f5205c7 feat(receive): single button with mint connectivity check 2026-02-07 22:35:13 -06:00
Forte11andGitHub 0629c00c90 Merge pull request #18 from Forte11Cuba/fix/onboarding-ui-polish
fix: improve onboarding UI with language selector and icon consistency
2026-02-06 21:49:40 -06:00
Forte11Cuba f49e6a9c59 fix flags 2026-02-06 21:43:33 -06:00
Forte11Cuba a0821429dd fix: improve onboarding UI with language selector and icon consistency 2026-02-06 21:33:47 -06:00
Forte11andGitHub 2e0f360ff0 Merge pull request #17 from Forte11Cuba/feature/receive-later
feat: add Receive Later functionality for offline token storage
2026-02-06 20:58:34 -06:00
11 changed files with 374 additions and 186 deletions
+5 -1
View File
@@ -186,7 +186,7 @@
"protectWithPin": "Protect the app with PIN",
"recoverTokens": "Recover tokens",
"scanMintsWithSeed": "Scan mints with seed phrase",
"appearanceSection": "APPEARANCE",
"appearanceSection": "LANGUAGE",
"language": "Language",
"informationSection": "INFORMATION",
"version": "Version",
@@ -290,6 +290,10 @@
"receiveNow": "Receive now",
"receiveLater": "Receive later",
"tokenSavedForLater": "Token saved to claim later",
"noConnectionTokenSaved": "No connection. Token saved to claim later.",
"unknownMintOffline": "This token is from an unknown mint. Connect to the internet to add it and claim the token.",
"noConnectionTryLater": "No connection to mint. Try again later.",
"saveTokenError": "Error saving token. Please try again.",
"pendingTokenLimitReached": "Pending tokens limit reached (max 50)",
"filterToReceive": "To receive",
"noPendingTokens": "No pending tokens",
+5 -1
View File
@@ -243,7 +243,7 @@
"protectWithPin": "Proteger la app con PIN",
"recoverTokens": "Recuperar tokens",
"scanMintsWithSeed": "Escanear mints con seed phrase",
"appearanceSection": "APARIENCIA",
"appearanceSection": "IDIOMA",
"language": "Idioma",
"informationSection": "INFORMACIÓN",
"version": "Versión",
@@ -410,6 +410,10 @@
"receiveNow": "Recibir ahora",
"receiveLater": "Recibir después",
"tokenSavedForLater": "Token guardado para reclamar después",
"noConnectionTokenSaved": "Sin conexión. Token guardado para reclamar después.",
"unknownMintOffline": "Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.",
"noConnectionTryLater": "Sin conexión al mint. Intenta más tarde.",
"saveTokenError": "Error al guardar el token. Intenta de nuevo.",
"pendingTokenLimitReached": "Límite de tokens pendientes alcanzado (max 50)",
"filterToReceive": "Para recibir",
"noPendingTokens": "Sin tokens pendientes",
+3 -1
View File
@@ -46,6 +46,8 @@ class ElCajuApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final settingsProvider = context.watch<SettingsProvider>();
return MaterialApp(
title: 'ElCaju',
debugShowCheckedModeBanner: false,
@@ -62,7 +64,7 @@ class ElCajuApp extends StatelessWidget {
Locale('es'), // Español (por defecto)
Locale('en'), // English
],
locale: null, // null = detectar del sistema
locale: Locale(settingsProvider.locale),
home: const SplashScreen(),
);
+27
View File
@@ -165,6 +165,26 @@ class WalletProvider extends ChangeNotifier {
return result;
}
// ============================================================
// CONNECTIVITY
// ============================================================
/// Verifica si podemos alcanzar un mint específico.
/// Hace ping HTTP GET a {mintUrl}/v1/info con timeout de 3 segundos.
/// Retorna true si responde 200, false en cualquier otro caso.
Future<bool> canReachMint(String mintUrl) async {
try {
final uri = Uri.parse('$mintUrl/v1/info');
final response = await http.get(uri).timeout(
const Duration(seconds: 3),
);
return response.statusCode == 200;
} catch (e) {
debugPrint('Ping to mint failed: $e');
return false;
}
}
// ============================================================
// MINT INFO
// ============================================================
@@ -1385,12 +1405,19 @@ class WalletProvider extends ChangeNotifier {
/// Intenta reclamar un token pendiente.
/// Retorna el monto recibido si tiene éxito, o lanza excepción.
/// Si el token está gastado o es inválido, lo elimina automáticamente.
/// Verifica conectividad al mint antes de intentar reclamar.
Future<BigInt> claimPendingToken(String id) async {
final pending = _pendingTokenStorage.get(id);
if (pending == null) {
throw Exception('Token pendiente no encontrado');
}
// Verificar conectividad al mint primero (evita esperas largas sin conexión)
final canReach = await canReachMint(pending.mintUrl);
if (!canReach) {
throw Exception('No connection to mint');
}
try {
// Intentar reclamar usando el método existente
final amount = await receiveToken(pending.encoded);
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -53,7 +54,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
@@ -111,7 +112,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.copy,
LucideIcons.copy,
color: AppColors.textSecondary,
size: 18,
),
@@ -140,7 +141,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
child: Row(
children: [
const Icon(
Icons.warning_amber_rounded,
LucideIcons.alertTriangle,
color: AppColors.warning,
size: 24,
),
@@ -182,7 +183,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
),
),
child: _confirmed
? const Icon(Icons.check, size: 16, color: Colors.white)
? const Icon(LucideIcons.check, size: 16, color: Colors.white)
: null,
),
const SizedBox(width: AppDimensions.paddingSmall),
@@ -205,7 +206,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
if (!_revealed)
PrimaryButton(
text: l10n.revealSeedPhrase,
icon: Icons.visibility,
icon: LucideIcons.eye,
onPressed: () => setState(() => _revealed = true),
)
else
@@ -229,7 +230,7 @@ class _BackupSeedScreenState extends State<BackupSeedScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.visibility_off,
LucideIcons.eyeOff,
size: 48,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -87,7 +88,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
@@ -114,101 +115,111 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
Widget _buildCreateView(L10n l10n) {
return Column(
children: [
const Spacer(),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
_isCreating ? Icons.hourglass_top : Icons.account_balance_wallet,
size: 60,
color: AppColors.secondaryAction,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Text(
_isCreating ? l10n.creatingWallet : l10n.createWalletTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
_isCreating ? l10n.generatingSeed : l10n.createWalletDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
// Mensaje de error si existe
if (_errorMessage != null) ...[
const SizedBox(height: AppDimensions.paddingLarge),
Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
// Contenido principal centrado
Expanded(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
_isCreating ? LucideIcons.hourglass : LucideIcons.wallet,
size: 60,
color: AppColors.secondaryAction,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
Text(
_isCreating ? l10n.creatingWallet : l10n.createWalletTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
_isCreating ? l10n.generatingSeed : l10n.createWalletDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.8),
height: 1.5,
),
textAlign: TextAlign.center,
),
// Mensaje de error si existe
if (_errorMessage != null) ...[
const SizedBox(height: AppDimensions.paddingLarge),
Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
),
],
// Spinner cuando está creando (integrado en el contenido centrado)
if (_isCreating) ...[
const SizedBox(height: AppDimensions.paddingXLarge),
const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.secondaryAction),
),
),
],
],
),
),
],
),
const Spacer(),
if (!_isCreating)
// Botón solo cuando no está creando
if (!_isCreating) ...[
PrimaryButton(
text: l10n.generateWallet,
icon: Icons.auto_awesome,
icon: LucideIcons.sparkles,
onPressed: _createWallet,
),
if (_isCreating)
const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.secondaryAction),
),
),
const SizedBox(height: AppDimensions.paddingXLarge),
const SizedBox(height: AppDimensions.paddingXLarge),
],
],
);
}
@@ -226,7 +237,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_circle,
LucideIcons.checkCircle,
size: 60,
color: AppColors.success,
),
@@ -261,7 +272,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
child: Row(
children: [
const Icon(
Icons.warning_amber_rounded,
LucideIcons.alertTriangle,
color: AppColors.warning,
size: 32,
),
@@ -285,7 +296,7 @@ class _CreateWalletScreenState extends State<CreateWalletScreen> {
PrimaryButton(
text: l10n.backupNow,
icon: Icons.shield,
icon: LucideIcons.shield,
onPressed: _goToBackup,
),
const SizedBox(height: AppDimensions.paddingMedium),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -96,7 +97,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
@@ -174,8 +175,8 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
children: [
Icon(
_isValidWordCount
? Icons.check_circle
: Icons.info_outline,
? LucideIcons.checkCircle
: LucideIcons.info,
size: 18,
color: _isValidWordCount
? AppColors.success
@@ -220,7 +221,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
child: Row(
children: [
const Icon(
Icons.error_outline,
LucideIcons.alertCircle,
color: AppColors.error,
size: 20,
),
@@ -244,7 +245,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
PrimaryButton(
text: l10n.restoreWallet,
icon: Icons.restore,
icon: LucideIcons.rotateCcw,
isLoading: _isRestoring,
onPressed: _isValidWordCount && !_isRestoring
? _restoreWallet
+145 -2
View File
@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../providers/settings_provider.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
@@ -12,10 +15,104 @@ import 'restore_wallet_screen.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
static const _languages = [
{'code': 'es', 'flag': '🇪🇸', 'name': 'Español'},
{'code': 'en', 'flag': '🇬🇧', 'name': 'English'},
];
void _showLanguageSelector(BuildContext context) {
final settingsProvider = context.read<SettingsProvider>();
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Opciones de idioma
..._languages.map((lang) {
final isSelected = settingsProvider.locale == lang['code'];
return GestureDetector(
onTap: () {
settingsProvider.setLocale(lang['code']!);
Navigator.pop(context);
},
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.white.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Text(
lang['flag']!,
style: const TextStyle(fontSize: 24),
),
const SizedBox(width: 12),
Text(
lang['name']!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
color: isSelected ? AppColors.primaryAction : Colors.white,
),
),
const Spacer(),
if (isSelected)
const Icon(
LucideIcons.checkCircle,
color: AppColors.primaryAction,
size: 22,
),
],
),
),
);
}),
const SizedBox(height: 8),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final settingsProvider = context.watch<SettingsProvider>();
final isSpanish = settingsProvider.locale == 'es';
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
@@ -24,6 +121,52 @@ class WelcomeScreen extends StatelessWidget {
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
children: [
// Selector de idioma en esquina superior derecha
Align(
alignment: Alignment.topRight,
child: GestureDetector(
onTap: () => _showLanguageSelector(context),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
isSpanish ? '🇪🇸' : '🇬🇧',
style: const TextStyle(fontSize: 18),
),
const SizedBox(width: 6),
Text(
isSpanish ? 'ES' : 'EN',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(width: 4),
Icon(
LucideIcons.chevronDown,
color: Colors.white.withValues(alpha: 0.7),
size: 18,
),
],
),
),
),
),
const Spacer(flex: 1),
// Logo / Mascota
@@ -66,7 +209,7 @@ class WelcomeScreen extends StatelessWidget {
// Botón crear wallet
PrimaryButton(
text: l10n.createWallet,
icon: Icons.add_circle_outline,
icon: LucideIcons.plusCircle,
onPressed: () {
Navigator.push(
context,
@@ -81,7 +224,7 @@ class WelcomeScreen extends StatelessWidget {
// Botón restaurar wallet
SecondaryButton(
text: l10n.restoreWallet,
icon: Icons.restore,
icon: LucideIcons.rotateCcw,
onPressed: () {
Navigator.push(
context,
+70 -79
View File
@@ -114,20 +114,10 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
),
),
// Botones guardar para después y reclamar (fijo abajo)
// Botón único "Recibir" (fijo abajo)
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Botón principal: Guardar para después
if (_isValidToken && _tokenInfo != null && !_isProcessing) ...[
_buildReceiveLaterButton(),
const SizedBox(height: AppDimensions.paddingSmall),
],
// Botón secundario: Reclamar ahora
_buildClaimButton(),
],
),
child: _buildReceiveButton(),
),
],
);
@@ -404,52 +394,12 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
);
}
Widget _buildClaimButton() {
Widget _buildReceiveButton() {
final l10n = L10n.of(context)!;
// Botón principal (naranja) para reclamar - abajo, cerca de los dedos
// Botón único "Recibir" - auto-detecta conectividad
return PrimaryButton(
text: _isProcessing ? l10n.claiming : l10n.receiveNow,
onPressed: _isValidToken && !_isProcessing ? _claimToken : null,
);
}
Widget _buildReceiveLaterButton() {
final l10n = L10n.of(context)!;
// Botón secundario (outline) para guardar para después
return GestureDetector(
onTap: _saveForLater,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.clock,
color: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 8),
Text(
l10n.receiveLater,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
),
),
text: _isProcessing ? l10n.claiming : l10n.receive,
onPressed: _isValidToken && !_isProcessing ? _receiveToken : null,
);
}
@@ -487,14 +437,63 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
});
}
Future<void> _saveForLater() async {
if (_isProcessing) return; // Guard contra doble-tap
/// Método principal: recibe token con detección automática de conectividad.
/// 1. Si el mint es desconocido y no hay conexión → rechazar
/// 2. Ping al mint (3s) → si OK → claim → si falla → guardar pendiente
Future<void> _receiveToken() async {
if (_isProcessing) return;
setState(() => _isProcessing = true);
setState(() {
_isProcessing = true;
_errorMessage = null;
});
final l10n = L10n.of(context)!;
final walletProvider = context.read<WalletProvider>();
try {
final mintUrl = _tokenInfo?.mintUrl;
if (mintUrl == null) {
throw Exception(l10n.invalidToken);
}
// Verificar si el mint es conocido
final isKnownMint = walletProvider.mintUrls.contains(mintUrl);
// Verificar conectividad al mint
final canReach = await walletProvider.canReachMint(mintUrl);
// Si el mint es desconocido y no hay conexión → rechazar
if (!isKnownMint && !canReach) {
setState(() {
_errorMessage = l10n.unknownMintOffline;
_isProcessing = false;
});
return;
}
// Si no hay conexión → guardar como pendiente
if (!canReach) {
await _saveForLaterOffline();
return;
}
// Hay conexión → intentar reclamar
await _claimToken();
} catch (e) {
if (!mounted) return;
setState(() {
_errorMessage = l10n.claimError(e.toString());
_isProcessing = false;
});
}
}
/// Guarda el token como pendiente cuando no hay conexión.
Future<void> _saveForLaterOffline() async {
final l10n = L10n.of(context)!;
final walletProvider = context.read<WalletProvider>();
try {
final pending = await walletProvider.addPendingToken(
_tokenController.text.trim(),
@@ -502,11 +501,12 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
if (pending != null) {
if (mounted) {
// Mostrar mensaje de sin conexión
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.tokenSavedForLater),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 2),
content: Text(l10n.noConnectionTokenSaved),
backgroundColor: AppColors.warning,
duration: const Duration(seconds: 3),
),
);
Navigator.pop(context);
@@ -527,7 +527,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error: ${e.toString()}'),
content: Text(l10n.saveTokenError),
backgroundColor: AppColors.error,
),
);
@@ -539,18 +539,14 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
}
}
/// Reclama el token directamente (cuando hay conexión).
Future<void> _claimToken() async {
setState(() {
_isProcessing = true;
_errorMessage = null;
});
final walletProvider = context.read<WalletProvider>();
// Guardar unidad detectada del token ANTES de reclamar
final detectedUnit = _tokenInfo?.unit ?? walletProvider.activeUnit;
try {
final walletProvider = context.read<WalletProvider>();
// Guardar unidad detectada del token ANTES de reclamar
final detectedUnit = _tokenInfo?.unit ?? walletProvider.activeUnit;
// Reclamar token (usa unidad detectada internamente)
final amountReceived = await walletProvider.receiveToken(
_tokenController.text.trim(),
@@ -559,7 +555,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
if (mounted) {
setState(() {
_receivedAmount = amountReceived;
_receivedUnit = detectedUnit; // Usar unidad del token
_receivedUnit = detectedUnit;
_showSuccess = true;
_isProcessing = false;
});
@@ -582,13 +578,8 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
}
setState(() {
_errorMessage = errorMessage;
_isProcessing = false;
});
} finally {
if (mounted && !_showSuccess) {
setState(() {
_isProcessing = false;
});
}
}
}
}
+2 -2
View File
@@ -96,7 +96,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: AppDimensions.paddingLarge),
// Sección APARIENCIA
// Sección IDIOMA
_buildSectionHeader(l10n.appearanceSection),
const SizedBox(height: AppDimensions.paddingSmall),
_buildSettingTile(
@@ -513,7 +513,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
settingsProvider,
'en',
l10n.english,
'🇺🇸',
'🇬🇧',
),
const SizedBox(height: AppDimensions.paddingSmall),
],
+5 -1
View File
@@ -238,15 +238,19 @@ class _HistoryScreenState extends State<HistoryScreen> {
if (mounted) {
final errorStr = e.toString().toLowerCase();
String message;
Color bgColor = AppColors.error;
if (errorStr.contains('already spent') || errorStr.contains('token already')) {
message = l10n.tokenAlreadyClaimed;
} else if (errorStr.contains('no connection')) {
message = l10n.noConnectionTryLater;
bgColor = AppColors.warning;
} else {
message = l10n.claimError(e.toString());
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: AppColors.error,
backgroundColor: bgColor,
),
);
}