Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e3622435b | ||
|
|
b6c5b42cf2 | ||
|
|
835f5205c7 | ||
|
|
0629c00c90 | ||
|
|
f49e6a9c59 | ||
|
|
a0821429dd | ||
|
|
2e0f360ff0 |
+5
-1
@@ -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
@@ -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
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user