Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a935193c2 | ||
|
|
a0d55df497 | ||
|
|
8ce9ed37bd | ||
|
|
862d9e36ac | ||
|
|
a3f17210bc | ||
|
|
6a1d956bef | ||
|
|
8896bc83f7 | ||
|
|
e607148cca |
@@ -0,0 +1,123 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Utilidades para formatear montos y unidades en El Caju.
|
||||
///
|
||||
/// USD y EUR usan centavos (multiplicador 100).
|
||||
/// SAT y otras unidades usan valores enteros.
|
||||
|
||||
class UnitFormatter {
|
||||
/// Multiplicador para convertir a unidad base.
|
||||
/// USD/EUR almacenan centavos, display en unidades.
|
||||
static int getMultiplier(String unit) {
|
||||
switch (unit.toLowerCase()) {
|
||||
case 'usd':
|
||||
case 'eur':
|
||||
return 100; // centavos
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatea un balance para display.
|
||||
/// Ejemplo: 1000 sat → "1,000", 500 usd → "5.00"
|
||||
static String formatBalance(BigInt amount, String unit) {
|
||||
final multiplier = getMultiplier(unit);
|
||||
|
||||
if (multiplier == 100) {
|
||||
// USD/EUR: mostrar con 2 decimales
|
||||
final value = amount.toDouble() / 100;
|
||||
return value.toStringAsFixed(2);
|
||||
} else {
|
||||
// SAT y otros: entero con separador de miles
|
||||
return NumberFormat('#,###').format(amount.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatea un balance con su unidad.
|
||||
/// Ejemplo: 1000 sat → "1,000 BTC", 500 usd → "5.00 USD"
|
||||
static String formatBalanceWithUnit(BigInt amount, String unit) {
|
||||
final formatted = formatBalance(amount, unit);
|
||||
final label = getUnitLabel(unit);
|
||||
return '$formatted $label';
|
||||
}
|
||||
|
||||
/// Obtiene la etiqueta de display para una unidad.
|
||||
/// sat → BTC, usd → USD, eur → EUR, msat → MSAT
|
||||
static String getUnitLabel(String unit) {
|
||||
switch (unit.toLowerCase()) {
|
||||
case 'sat':
|
||||
return 'BTC';
|
||||
case 'usd':
|
||||
return 'USD';
|
||||
case 'eur':
|
||||
return 'EUR';
|
||||
case 'msat':
|
||||
return 'MSAT';
|
||||
default:
|
||||
return unit.toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte input del usuario a BigInt para la unidad.
|
||||
/// Ejemplo: "5.00" USD → BigInt(500)
|
||||
static BigInt parseUserInput(String input, String unit) {
|
||||
final multiplier = getMultiplier(unit);
|
||||
|
||||
// Limpiar input
|
||||
final cleaned = input.replaceAll(',', '').replaceAll(' ', '');
|
||||
|
||||
if (multiplier == 100) {
|
||||
// USD/EUR: parsear como decimal
|
||||
final value = double.tryParse(cleaned) ?? 0.0;
|
||||
return BigInt.from((value * 100).round());
|
||||
} else {
|
||||
// SAT: parsear como entero
|
||||
return BigInt.from(int.tryParse(cleaned) ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtiene el nombre del host de un mint URL.
|
||||
/// Ejemplo: 'https://mint.cubabitcoin.org' → 'cubabitcoin.org'
|
||||
static String getMintDisplayName(String mintUrl) {
|
||||
try {
|
||||
final uri = Uri.parse(mintUrl);
|
||||
var host = uri.host;
|
||||
|
||||
// Limpiar prefijos comunes
|
||||
host = host.replaceFirst('mint.', '');
|
||||
host = host.replaceFirst('www.', '');
|
||||
|
||||
return host;
|
||||
} catch (e) {
|
||||
return mintUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/// Formatea una fecha relativa.
|
||||
/// Ejemplo: hace 5 min, hace 2 h, ayer, 15 ene
|
||||
static String formatRelativeDate(DateTime date) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(date);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'Ahora';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return 'Hace ${difference.inMinutes} min';
|
||||
} else if (difference.inHours < 24) {
|
||||
return 'Hace ${difference.inHours} h';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Ayer';
|
||||
} else if (difference.inDays < 7) {
|
||||
return 'Hace ${difference.inDays} días';
|
||||
} else {
|
||||
return DateFormat('d MMM').format(date);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convierte timestamp BigInt (unix seconds) a DateTime.
|
||||
static DateTime timestampToDateTime(BigInt timestamp) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(
|
||||
timestamp.toInt() * 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
static const _defaultMintKey = 'default_mint';
|
||||
static const _localeKey = 'locale';
|
||||
static const _pinEnabledKey = 'pin_enabled';
|
||||
static const _activeUnitKey = 'active_unit';
|
||||
static const _activeMintUrlKey = 'active_mint_url';
|
||||
|
||||
// Estado interno
|
||||
bool _isInitialized = false;
|
||||
@@ -24,6 +26,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
String? _pin;
|
||||
String _locale = 'es';
|
||||
String _defaultMint = 'https://mint.cubabitcoin.org';
|
||||
String _activeUnit = 'sat';
|
||||
String? _activeMintUrl;
|
||||
|
||||
// Getters
|
||||
bool get isInitialized => _isInitialized;
|
||||
@@ -32,6 +36,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
bool get hasPin => _pin != null && _pin!.isNotEmpty;
|
||||
String get locale => _locale;
|
||||
String get defaultMint => _defaultMint;
|
||||
String get activeUnit => _activeUnit;
|
||||
String? get activeMintUrl => _activeMintUrl;
|
||||
|
||||
// ============================================================
|
||||
// INICIALIZACION
|
||||
@@ -49,6 +55,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
_locale = _prefs?.getString(_localeKey) ?? 'es';
|
||||
_defaultMint = _prefs?.getString(_defaultMintKey) ?? 'https://mint.cubabitcoin.org';
|
||||
_pinEnabled = _prefs?.getBool(_pinEnabledKey) ?? false;
|
||||
_activeUnit = _prefs?.getString(_activeUnitKey) ?? 'sat';
|
||||
_activeMintUrl = _prefs?.getString(_activeMintUrlKey);
|
||||
|
||||
// Cargar PIN si existe
|
||||
if (_pinEnabled) {
|
||||
@@ -142,6 +150,28 @@ class SettingsProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UNIDAD Y MINT ACTIVO
|
||||
// ============================================================
|
||||
|
||||
/// Guarda la unidad activa (sat, usd, eur, etc.).
|
||||
Future<void> setActiveUnit(String unit) async {
|
||||
await _prefs?.setString(_activeUnitKey, unit);
|
||||
_activeUnit = unit;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Guarda el mint activo.
|
||||
Future<void> setActiveMintUrl(String? mintUrl) async {
|
||||
if (mintUrl != null) {
|
||||
await _prefs?.setString(_activeMintUrlKey, mintUrl);
|
||||
} else {
|
||||
await _prefs?.remove(_activeMintUrlKey);
|
||||
}
|
||||
_activeMintUrl = mintUrl;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BORRAR WALLET
|
||||
// ============================================================
|
||||
@@ -156,12 +186,16 @@ class SettingsProvider extends ChangeNotifier {
|
||||
// Resetear preferencias
|
||||
await _prefs?.setBool(_hasWalletKey, false);
|
||||
await _prefs?.setBool(_pinEnabledKey, false);
|
||||
await _prefs?.remove(_activeUnitKey);
|
||||
await _prefs?.remove(_activeMintUrlKey);
|
||||
// Mantener locale y defaultMint (preferencias de app, no de wallet)
|
||||
|
||||
// Resetear estado
|
||||
_hasWallet = false;
|
||||
_pinEnabled = false;
|
||||
_pin = null;
|
||||
_activeUnit = 'sat';
|
||||
_activeMintUrl = null;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -180,6 +214,8 @@ class SettingsProvider extends ChangeNotifier {
|
||||
_pin = null;
|
||||
_locale = 'es';
|
||||
_defaultMint = 'https://mint.cubabitcoin.org';
|
||||
_activeUnit = 'sat';
|
||||
_activeMintUrl = null;
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
+654
-231
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
@@ -65,6 +66,12 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Restaurar mint y unidad activa desde settings
|
||||
await _restoreActiveState(settingsProvider, walletProvider);
|
||||
|
||||
// Verificar proofs en background (no bloquea navegación)
|
||||
unawaited(_checkPendingTransactions(walletProvider));
|
||||
|
||||
// Ir al Home
|
||||
_navigateTo(const HomeScreen());
|
||||
} else {
|
||||
@@ -91,6 +98,41 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
}
|
||||
}
|
||||
|
||||
/// Restaura el mint y unidad activa desde SettingsProvider.
|
||||
Future<void> _restoreActiveState(
|
||||
SettingsProvider settings,
|
||||
WalletProvider wallet,
|
||||
) async {
|
||||
try {
|
||||
// Restaurar mint activo si está guardado
|
||||
final savedMintUrl = settings.activeMintUrl;
|
||||
if (savedMintUrl != null && wallet.mintUrls.contains(savedMintUrl)) {
|
||||
await wallet.setActiveMint(savedMintUrl);
|
||||
}
|
||||
|
||||
// Restaurar unidad activa si está guardada y es soportada
|
||||
final savedUnit = settings.activeUnit;
|
||||
if (wallet.activeUnits.contains(savedUnit)) {
|
||||
await wallet.setActiveUnit(savedUnit);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error restaurando estado activo: $e');
|
||||
// No bloquear si falla, usar defaults
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica proofs pendientes en background.
|
||||
/// Se ejecuta sin bloquear la navegación.
|
||||
Future<void> _checkPendingTransactions(WalletProvider wallet) async {
|
||||
try {
|
||||
await wallet.checkPendingTransactions();
|
||||
debugPrint('Verificación de proofs completada');
|
||||
} catch (e) {
|
||||
debugPrint('Error verificando proofs: $e');
|
||||
// Silencioso - no mostrar error al usuario
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateTo(Widget screen) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
|
||||
@@ -4,10 +4,12 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/effects/cashu_confetti.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../4_receive/receive_screen.dart';
|
||||
import '../5_send/send_screen.dart';
|
||||
import '../6_mint/mint_screen.dart';
|
||||
@@ -131,6 +133,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
Widget _buildBalanceSection() {
|
||||
final walletProvider = context.watch<WalletProvider>();
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
final activeUnit = walletProvider.activeUnit;
|
||||
final unitLabel = UnitFormatter.getUnitLabel(activeUnit);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -160,15 +165,15 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Balance reactivo con StreamBuilder
|
||||
// Balance reactivo del mint activo
|
||||
StreamBuilder<BigInt>(
|
||||
stream: walletProvider.streamTotalBalance(),
|
||||
stream: walletProvider.streamBalance(),
|
||||
builder: (context, snapshot) {
|
||||
final balance = snapshot.data ?? BigInt.zero;
|
||||
final balanceInt = balance.toInt();
|
||||
final formattedBalance = UnitFormatter.formatBalance(balance, activeUnit);
|
||||
|
||||
return Text(
|
||||
_isBalanceVisible ? _formatBalance(balanceInt) : '••••••',
|
||||
_isBalanceVisible ? formattedBalance : '••••••',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 48,
|
||||
@@ -180,20 +185,42 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Unidad (siempre sats por ahora)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'sats',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryAction,
|
||||
// Unidad - tap para ciclar
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Ciclar unidad
|
||||
await walletProvider.cycleUnit();
|
||||
// Guardar en settings
|
||||
await settingsProvider.setActiveUnit(walletProvider.activeUnit);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
unitLabel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
// Mostrar indicador si hay más de una unidad
|
||||
if (walletProvider.activeUnits.length > 1) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
LucideIcons.refreshCw,
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.7),
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -206,16 +233,10 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final walletProvider = context.watch<WalletProvider>();
|
||||
final activeMintUrl = walletProvider.activeMintUrl;
|
||||
|
||||
// Extraer solo el host del URL para mostrar
|
||||
String displayMint = 'Sin mint';
|
||||
if (activeMintUrl != null) {
|
||||
try {
|
||||
final uri = Uri.parse(activeMintUrl);
|
||||
displayMint = uri.host;
|
||||
} catch (_) {
|
||||
displayMint = activeMintUrl;
|
||||
}
|
||||
}
|
||||
// Extraer nombre del mint para mostrar
|
||||
final displayMint = activeMintUrl != null
|
||||
? UnitFormatter.getMintDisplayName(activeMintUrl)
|
||||
: 'Sin mint';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppDimensions.paddingSmall),
|
||||
@@ -406,17 +427,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
builder: (context) => const _HistoryModal(),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatBalance(int amount) {
|
||||
if (amount >= 1000) {
|
||||
final formatted = amount.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]},',
|
||||
);
|
||||
return formatted;
|
||||
}
|
||||
return amount.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Botón de acción para el home
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -26,7 +27,8 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
bool _isValidToken = false;
|
||||
bool _isProcessing = false;
|
||||
bool _showSuccess = false;
|
||||
int _receivedAmount = 0;
|
||||
BigInt _receivedAmount = BigInt.zero;
|
||||
String? _receivedUnit; // Se asigna al reclamar desde walletProvider.activeUnit
|
||||
TokenInfo? _tokenInfo;
|
||||
String? _errorMessage;
|
||||
|
||||
@@ -132,14 +134,19 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
const SizedBox(height: AppDimensions.paddingLarge),
|
||||
|
||||
// Monto recibido
|
||||
Text(
|
||||
'+$_receivedAmount sats',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final unit = _receivedUnit ?? context.read<WalletProvider>().activeUnit;
|
||||
return Text(
|
||||
'+${UnitFormatter.formatBalance(_receivedAmount, unit)} ${UnitFormatter.getUnitLabel(unit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
@@ -229,14 +236,12 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
}
|
||||
|
||||
Widget _buildTokenPreview() {
|
||||
final amount = _tokenInfo!.amount.toInt();
|
||||
|
||||
// Extraer solo el host del mint URL
|
||||
String mintDisplay = _tokenInfo!.mintUrl;
|
||||
try {
|
||||
final uri = Uri.parse(_tokenInfo!.mintUrl);
|
||||
mintDisplay = uri.host;
|
||||
} catch (_) {}
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final amount = _tokenInfo!.amount;
|
||||
// Usar unidad detectada del token, o fallback a unidad activa
|
||||
final tokenUnit = _tokenInfo!.unit ?? walletProvider.activeUnit;
|
||||
final mintDisplay = UnitFormatter.getMintDisplayName(_tokenInfo!.mintUrl);
|
||||
final unitDetected = _tokenInfo!.unit != null;
|
||||
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
@@ -271,7 +276,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Monto
|
||||
// Monto con unidad detectada
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -283,14 +288,27 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$amount sats',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${UnitFormatter.formatBalance(amount, tokenUnit)} ${UnitFormatter.getUnitLabel(tokenUnit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
// Indicador si la unidad fue detectada automáticamente
|
||||
if (!unitDetected) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
LucideIcons.helpCircle,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
size: 14,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -410,14 +428,18 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
try {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
|
||||
// Reclamar token real
|
||||
// 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(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_receivedAmount = amountReceived.toInt();
|
||||
_receivedAmount = amountReceived;
|
||||
_receivedUnit = detectedUnit; // Usar unidad del token
|
||||
_showSuccess = true;
|
||||
_isProcessing = false;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -21,24 +22,24 @@ class _SendScreenState extends State<SendScreen> {
|
||||
final TextEditingController _amountController = TextEditingController();
|
||||
final TextEditingController _memoController = TextEditingController();
|
||||
|
||||
final String _unit = 'sats';
|
||||
|
||||
bool _isProcessing = false;
|
||||
String? _errorMessage;
|
||||
int _availableBalance = 0;
|
||||
BigInt _availableBalance = BigInt.zero;
|
||||
late String _activeUnit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeUnit = context.read<WalletProvider>().activeUnit;
|
||||
_loadBalance();
|
||||
}
|
||||
|
||||
Future<void> _loadBalance() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final balance = await walletProvider.getTotalBalance();
|
||||
final balance = await walletProvider.getBalance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_availableBalance = balance.toInt();
|
||||
_availableBalance = balance;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -50,8 +51,13 @@ class _SendScreenState extends State<SendScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get _amount => int.tryParse(_amountController.text) ?? 0;
|
||||
bool get _isValidAmount => _amount > 0 && _amount <= _availableBalance;
|
||||
/// Obtiene la etiqueta de la unidad para display
|
||||
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
|
||||
|
||||
/// Parsea el input del usuario a BigInt según la unidad
|
||||
BigInt get _amount => UnitFormatter.parseUserInput(_amountController.text, _activeUnit);
|
||||
|
||||
bool get _isValidAmount => _amount > BigInt.zero && _amount <= _availableBalance;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -154,7 +160,7 @@ class _SendScreenState extends State<SendScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_unit,
|
||||
_unitLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
@@ -185,7 +191,7 @@ class _SendScreenState extends State<SendScreen> {
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'$_availableBalance $_unit',
|
||||
'${UnitFormatter.formatBalance(_availableBalance, _activeUnit)} $_unitLabel',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
@@ -300,7 +306,8 @@ class _SendScreenState extends State<SendScreen> {
|
||||
}
|
||||
|
||||
void _setMaxAmount() {
|
||||
_amountController.text = _availableBalance.toString();
|
||||
// Formatear el balance para el input (sin separadores de miles)
|
||||
_amountController.text = UnitFormatter.formatBalance(_availableBalance, _activeUnit).replaceAll(',', '');
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
});
|
||||
@@ -312,7 +319,7 @@ class _SendScreenState extends State<SendScreen> {
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _ConfirmationModal(
|
||||
amount: _amount,
|
||||
unit: _unit,
|
||||
unit: _activeUnit,
|
||||
memo: _memoController.text.isNotEmpty ? _memoController.text : null,
|
||||
onConfirm: () {
|
||||
Navigator.pop(context);
|
||||
@@ -334,10 +341,8 @@ class _SendScreenState extends State<SendScreen> {
|
||||
|
||||
// Crear token real con cdk-flutter
|
||||
final memo = _memoController.text.isNotEmpty ? _memoController.text : null;
|
||||
final token = await walletProvider.sendTokens(
|
||||
BigInt.from(_amount),
|
||||
memo,
|
||||
);
|
||||
final amount = _amount;
|
||||
final token = await walletProvider.sendTokens(amount, memo);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pushReplacement(
|
||||
@@ -345,8 +350,8 @@ class _SendScreenState extends State<SendScreen> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareTokenScreen(
|
||||
token: token,
|
||||
amount: _amount,
|
||||
unit: _unit,
|
||||
amount: amount,
|
||||
unit: _activeUnit,
|
||||
memo: memo,
|
||||
),
|
||||
),
|
||||
@@ -373,7 +378,7 @@ class _SendScreenState extends State<SendScreen> {
|
||||
|
||||
/// Modal de confirmacion
|
||||
class _ConfirmationModal extends StatelessWidget {
|
||||
final int amount;
|
||||
final BigInt amount;
|
||||
final String unit;
|
||||
final String? memo;
|
||||
final VoidCallback onConfirm;
|
||||
@@ -443,7 +448,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
// Monto
|
||||
Text(
|
||||
'$amount $unit',
|
||||
'${UnitFormatter.formatBalance(amount, unit)} ${UnitFormatter.getUnitLabel(unit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 32,
|
||||
|
||||
@@ -4,8 +4,10 @@ import 'package:flutter/services.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -13,7 +15,7 @@ import '../../widgets/common/primary_button.dart';
|
||||
/// Pantalla para compartir el token Cashu generado
|
||||
class ShareTokenScreen extends StatefulWidget {
|
||||
final String token;
|
||||
final int amount;
|
||||
final BigInt amount;
|
||||
final String unit;
|
||||
final String? memo;
|
||||
|
||||
@@ -30,61 +32,74 @@ class ShareTokenScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
late PageController _pageController;
|
||||
late List<String> _qrParts;
|
||||
int _currentPage = 0;
|
||||
Timer? _timer;
|
||||
List<String> _urFragments = [];
|
||||
int _currentFragment = 0;
|
||||
Timer? _animationTimer;
|
||||
|
||||
// Configuración de velocidad (como cashu.me)
|
||||
static const int _intervalFast = 100; // ms
|
||||
static const int _intervalMedium = 200; // ms
|
||||
static const int _intervalSlow = 400; // ms
|
||||
int _currentInterval = _intervalMedium;
|
||||
String _speedLabel = 'M';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController();
|
||||
_qrParts = _encodeTokenToQR();
|
||||
_startQRAnimation();
|
||||
_encodeTokenToUR();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_pageController.dispose();
|
||||
_animationTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<String> _encodeTokenToQR() {
|
||||
// TODO: Implementar con cdk-flutter cuando esté disponible
|
||||
// final qrParts = encodeQrToken(token: widget.token);
|
||||
void _encodeTokenToUR() {
|
||||
// Parsear el token string a objeto Token de cdk-flutter
|
||||
final token = cdk.Token.parse(encoded: widget.token);
|
||||
|
||||
// Por ahora, simulamos fragmentos UR para el token
|
||||
if (widget.token.length <= 200) {
|
||||
return [widget.token];
|
||||
}
|
||||
// Codificar a fragmentos UR usando cdk-flutter
|
||||
// maxFragmentLength por defecto es 150 bytes
|
||||
_urFragments = cdk.encodeQrToken(token: token);
|
||||
|
||||
// Simular fragmentación para tokens largos
|
||||
final parts = <String>[];
|
||||
const chunkSize = 200;
|
||||
for (int i = 0; i < widget.token.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < widget.token.length)
|
||||
? i + chunkSize
|
||||
: widget.token.length;
|
||||
parts.add(widget.token.substring(i, end));
|
||||
// Si hay múltiples fragmentos, iniciar animación
|
||||
if (_urFragments.length > 1) {
|
||||
_startAnimation();
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
void _startQRAnimation() {
|
||||
// Solo animar si hay múltiples fragmentos
|
||||
if (_qrParts.length <= 1) return;
|
||||
void _startAnimation() {
|
||||
_animationTimer?.cancel();
|
||||
_animationTimer = Timer.periodic(
|
||||
Duration(milliseconds: _currentInterval),
|
||||
(timer) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFragment = (_currentFragment + 1) % _urFragments.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_timer = Timer.periodic(const Duration(seconds: 2), (timer) {
|
||||
if (mounted) {
|
||||
final nextPage = (_currentPage + 1) % _qrParts.length;
|
||||
_pageController.animateToPage(
|
||||
nextPage,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
void _cycleSpeed() {
|
||||
setState(() {
|
||||
if (_currentInterval == _intervalMedium) {
|
||||
_currentInterval = _intervalSlow;
|
||||
_speedLabel = 'S';
|
||||
} else if (_currentInterval == _intervalSlow) {
|
||||
_currentInterval = _intervalFast;
|
||||
_speedLabel = 'F';
|
||||
} else {
|
||||
_currentInterval = _intervalMedium;
|
||||
_speedLabel = 'M';
|
||||
}
|
||||
});
|
||||
// Reiniciar timer con nueva velocidad
|
||||
if (_urFragments.length > 1) {
|
||||
_startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -133,11 +148,6 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
// Token truncado
|
||||
_buildTokenTextDisplay(),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Indicador de página (si hay múltiples fragmentos)
|
||||
if (_qrParts.length > 1) _buildPageIndicator(),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Botones copiar y compartir
|
||||
@@ -176,10 +186,13 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
}
|
||||
|
||||
Widget _buildAmountDisplay() {
|
||||
final formattedAmount = UnitFormatter.formatBalance(widget.amount, widget.unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(widget.unit);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${widget.amount} ${widget.unit}',
|
||||
'$formattedAmount $unitLabel',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 36,
|
||||
@@ -204,58 +217,111 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
}
|
||||
|
||||
Widget _buildQRDisplay() {
|
||||
if (_urFragments.isEmpty) {
|
||||
return const SizedBox(
|
||||
width: 220,
|
||||
height: 220,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final size = 220.0;
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
final isAnimated = _urFragments.length > 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// QR Container
|
||||
Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: _qrParts.length > 1
|
||||
? PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
itemCount: _qrParts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: QrImageView(
|
||||
data: _qrParts[index],
|
||||
version: QrVersions.auto,
|
||||
size: size - 40,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: QrImageView(
|
||||
data: _qrParts[0],
|
||||
version: QrVersions.auto,
|
||||
size: size - 40,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: QrImageView(
|
||||
data: _urFragments[_currentFragment],
|
||||
version: QrVersions.auto,
|
||||
size: size - 32,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Controles de animación (solo si hay múltiples fragmentos)
|
||||
if (isAnimated) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicador de fragmento actual
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${_currentFragment + 1} / ${_urFragments.length}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Botón de velocidad
|
||||
GestureDetector(
|
||||
onTap: _cycleSpeed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.gauge,
|
||||
color: AppColors.primaryAction,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
_speedLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,6 +330,8 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
? '${widget.token.substring(0, 25)}...${widget.token.substring(widget.token.length - 20)}'
|
||||
: widget.token;
|
||||
|
||||
final isAnimated = _urFragments.length > 1;
|
||||
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
|
||||
child: Column(
|
||||
@@ -274,7 +342,9 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Token Cashu (${_qrParts.length} ${_qrParts.length == 1 ? "parte" : "partes"})',
|
||||
isAnimated
|
||||
? 'Token Cashu (QR animado - ${_urFragments.length} fragmentos UR)'
|
||||
: 'Token Cashu',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
@@ -298,27 +368,6 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageIndicator() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
_qrParts.length,
|
||||
(index) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: index == _currentPage ? 24 : 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: index == _currentPage
|
||||
? AppColors.primaryAction
|
||||
: AppColors.textSecondary.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
@@ -438,10 +487,13 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
? '\n"${widget.memo}"'
|
||||
: '';
|
||||
|
||||
final formattedAmount = UnitFormatter.formatBalance(widget.amount, widget.unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(widget.unit);
|
||||
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: '${widget.amount} ${widget.unit}$memo\n\n${widget.token}',
|
||||
subject: 'Token Cashu - ${widget.amount} ${widget.unit}',
|
||||
text: '$formattedAmount $unitLabel$memo\n\n${widget.token}',
|
||||
subject: 'Token Cashu - $formattedAmount $unitLabel',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/effects/cashu_confetti.dart';
|
||||
@@ -17,7 +18,7 @@ enum MintStatus { loading, unpaid, paid, issued, error }
|
||||
|
||||
/// Pantalla para mostrar el invoice generado y esperar el pago
|
||||
class InvoiceScreen extends StatefulWidget {
|
||||
final int amount;
|
||||
final BigInt amount;
|
||||
final String unit;
|
||||
final String? description;
|
||||
|
||||
@@ -57,7 +58,7 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
|
||||
try {
|
||||
final mintStream = walletProvider.mintTokens(
|
||||
BigInt.from(widget.amount),
|
||||
widget.amount,
|
||||
widget.description,
|
||||
);
|
||||
|
||||
@@ -116,7 +117,7 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'+${widget.amount} ${widget.unit} depositados',
|
||||
'+${UnitFormatter.formatBalance(widget.amount, widget.unit)} ${UnitFormatter.getUnitLabel(widget.unit)} depositados',
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 3),
|
||||
@@ -320,10 +321,13 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final formattedAmount = UnitFormatter.formatBalance(widget.amount, widget.unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(widget.unit);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Depositar ${widget.amount} ${widget.unit}',
|
||||
'Depositar $formattedAmount $unitLabel',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -21,11 +22,17 @@ class _MintScreenState extends State<MintScreen> {
|
||||
final TextEditingController _amountController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
final String _unit = 'sats';
|
||||
late String _activeUnit;
|
||||
|
||||
bool _isProcessing = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeUnit = context.read<WalletProvider>().activeUnit;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
@@ -33,8 +40,13 @@ class _MintScreenState extends State<MintScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get _amount => int.tryParse(_amountController.text) ?? 0;
|
||||
bool get _isValidAmount => _amount > 0;
|
||||
/// Obtiene la etiqueta de la unidad para display
|
||||
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
|
||||
|
||||
/// Parsea el input del usuario a BigInt según la unidad
|
||||
BigInt get _amount => UnitFormatter.parseUserInput(_amountController.text, _activeUnit);
|
||||
|
||||
bool get _isValidAmount => _amount > BigInt.zero;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -137,7 +149,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_unit,
|
||||
_unitLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
@@ -242,7 +254,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _ConfirmationModal(
|
||||
amount: _amount,
|
||||
unit: _unit,
|
||||
unit: _activeUnit,
|
||||
description: _descriptionController.text.isNotEmpty
|
||||
? _descriptionController.text
|
||||
: null,
|
||||
@@ -262,7 +274,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => InvoiceScreen(
|
||||
amount: _amount,
|
||||
unit: _unit,
|
||||
unit: _activeUnit,
|
||||
description: _descriptionController.text.isNotEmpty
|
||||
? _descriptionController.text
|
||||
: null,
|
||||
@@ -274,7 +286,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
|
||||
/// Modal de confirmación
|
||||
class _ConfirmationModal extends StatelessWidget {
|
||||
final int amount;
|
||||
final BigInt amount;
|
||||
final String unit;
|
||||
final String? description;
|
||||
final VoidCallback onConfirm;
|
||||
@@ -344,7 +356,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
// Monto
|
||||
Text(
|
||||
'$amount $unit',
|
||||
'${UnitFormatter.formatBalance(amount, unit)} ${UnitFormatter.getUnitLabel(unit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 32,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
@@ -21,30 +22,34 @@ class MeltScreen extends StatefulWidget {
|
||||
class _MeltScreenState extends State<MeltScreen> {
|
||||
final TextEditingController _invoiceController = TextEditingController();
|
||||
|
||||
final String _unit = 'sats';
|
||||
late String _activeUnit;
|
||||
|
||||
bool _isValidInvoice = false;
|
||||
bool _isLoadingQuote = false;
|
||||
bool _isProcessing = false;
|
||||
MeltQuote? _quote;
|
||||
int _invoiceAmount = 0;
|
||||
int _feeReserve = 0;
|
||||
int _total = 0;
|
||||
int _availableBalance = 0;
|
||||
BigInt _invoiceAmount = BigInt.zero;
|
||||
BigInt _feeReserve = BigInt.zero;
|
||||
BigInt _total = BigInt.zero;
|
||||
BigInt _availableBalance = BigInt.zero;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeUnit = context.read<WalletProvider>().activeUnit;
|
||||
_loadBalance();
|
||||
}
|
||||
|
||||
/// Obtiene la etiqueta de la unidad para display
|
||||
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
|
||||
|
||||
Future<void> _loadBalance() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final balance = await walletProvider.getTotalBalance();
|
||||
final balance = await walletProvider.getBalance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_availableBalance = balance.toInt();
|
||||
_availableBalance = balance;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -274,7 +279,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_invoiceAmount $_unit',
|
||||
'${UnitFormatter.formatBalance(_invoiceAmount, _activeUnit)} $_unitLabel',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
@@ -299,7 +304,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'~$_feeReserve $_unit',
|
||||
'~${UnitFormatter.formatBalance(_feeReserve, _activeUnit)} $_unitLabel',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
@@ -328,7 +333,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_total $_unit',
|
||||
'${UnitFormatter.formatBalance(_total, _activeUnit)} $_unitLabel',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
@@ -400,7 +405,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$_availableBalance $_unit',
|
||||
'${UnitFormatter.formatBalance(_availableBalance, _activeUnit)} $_unitLabel',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
@@ -440,9 +445,9 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
_isValidInvoice = false;
|
||||
|
||||
if (value.isEmpty) {
|
||||
_invoiceAmount = 0;
|
||||
_feeReserve = 0;
|
||||
_total = 0;
|
||||
_invoiceAmount = BigInt.zero;
|
||||
_feeReserve = BigInt.zero;
|
||||
_total = BigInt.zero;
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -475,8 +480,8 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_quote = quote;
|
||||
_invoiceAmount = quote.amount.toInt();
|
||||
_feeReserve = quote.feeReserve.toInt();
|
||||
_invoiceAmount = quote.amount;
|
||||
_feeReserve = quote.feeReserve;
|
||||
_total = _invoiceAmount + _feeReserve;
|
||||
_isValidInvoice = true;
|
||||
_isLoadingQuote = false;
|
||||
@@ -507,7 +512,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
amount: _invoiceAmount,
|
||||
fee: _feeReserve,
|
||||
total: _total,
|
||||
unit: _unit,
|
||||
unit: _activeUnit,
|
||||
onConfirm: () {
|
||||
Navigator.pop(context);
|
||||
_payInvoice();
|
||||
@@ -535,7 +540,7 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
// Mostrar éxito y volver
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('-${totalPaid.toInt()} $_unit enviados'),
|
||||
content: Text('-${UnitFormatter.formatBalance(totalPaid, _activeUnit)} $_unitLabel enviados'),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
@@ -567,9 +572,9 @@ class _MeltScreenState extends State<MeltScreen> {
|
||||
|
||||
/// Modal de confirmación
|
||||
class _ConfirmationModal extends StatelessWidget {
|
||||
final int amount;
|
||||
final int fee;
|
||||
final int total;
|
||||
final BigInt amount;
|
||||
final BigInt fee;
|
||||
final BigInt total;
|
||||
final String unit;
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
@@ -639,7 +644,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
// Detalles
|
||||
Text(
|
||||
'$amount $unit',
|
||||
'${UnitFormatter.formatBalance(amount, unit)} ${UnitFormatter.getUnitLabel(unit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 28,
|
||||
@@ -648,7 +653,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'+ ~$fee $unit fee',
|
||||
'+ ~${UnitFormatter.formatBalance(fee, unit)} ${UnitFormatter.getUnitLabel(unit)} fee',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
@@ -657,7 +662,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
Text(
|
||||
'Total: $total $unit',
|
||||
'Total: ${UnitFormatter.formatBalance(total, unit)} ${UnitFormatter.getUnitLabel(unit)}',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
@@ -42,51 +42,38 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
body: SafeArea(
|
||||
child: Consumer<WalletProvider>(
|
||||
builder: (context, walletProvider, child) {
|
||||
return FutureBuilder<List<Mint>>(
|
||||
future: walletProvider.listMints(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
);
|
||||
}
|
||||
final mintUrls = walletProvider.mintUrls;
|
||||
|
||||
final mints = snapshot.data ?? [];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Lista de mints
|
||||
Expanded(
|
||||
child: mints.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
itemCount: mints.length,
|
||||
itemBuilder: (context, index) {
|
||||
final mint = mints[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: AppDimensions.paddingMedium,
|
||||
),
|
||||
child: _buildMintCard(
|
||||
mint,
|
||||
walletProvider,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Botón agregar mint
|
||||
_buildAddMintButton(walletProvider),
|
||||
],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Lista de mints
|
||||
Expanded(
|
||||
child: mintUrls.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
itemCount: mintUrls.length,
|
||||
itemBuilder: (context, index) {
|
||||
final mintUrl = mintUrls[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: AppDimensions.paddingMedium,
|
||||
),
|
||||
child: _buildMintCard(
|
||||
mintUrl,
|
||||
walletProvider,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
// Botón agregar mint
|
||||
_buildAddMintButton(walletProvider),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -129,20 +116,21 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMintCard(Mint mint, WalletProvider walletProvider) {
|
||||
final isActive = walletProvider.activeMintUrl == mint.url;
|
||||
Widget _buildMintCard(String mintUrl, WalletProvider walletProvider) {
|
||||
final isActive = walletProvider.activeMintUrl == mintUrl;
|
||||
final units = walletProvider.getUnitsForMint(mintUrl);
|
||||
|
||||
return FutureBuilder<BigInt>(
|
||||
future: walletProvider.getBalanceForMint(mint.url),
|
||||
return FutureBuilder<Map<String, BigInt>>(
|
||||
future: walletProvider.getBalancesForMint(mintUrl),
|
||||
builder: (context, balanceSnapshot) {
|
||||
final balance = balanceSnapshot.data ?? BigInt.zero;
|
||||
final balances = balanceSnapshot.data ?? {};
|
||||
|
||||
return GlassCard(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: URL + estado
|
||||
// Header: URL + estado + refresh
|
||||
Row(
|
||||
children: [
|
||||
// Icono estado conexión (verde si es el activo)
|
||||
@@ -156,10 +144,10 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// URL del mint (solo el host)
|
||||
// URL del mint (display name)
|
||||
Expanded(
|
||||
child: Text(
|
||||
_extractHost(mint.url),
|
||||
UnitFormatter.getMintDisplayName(mintUrl),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
@@ -170,6 +158,21 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Botón refresh
|
||||
GestureDetector(
|
||||
onTap: () => _refreshMint(mintUrl, walletProvider),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
LucideIcons.refreshCw,
|
||||
color: AppColors.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Badge activo
|
||||
if (isActive)
|
||||
Container(
|
||||
@@ -198,7 +201,7 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
|
||||
// URL completa (más pequeña)
|
||||
Text(
|
||||
mint.url,
|
||||
mintUrl,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
@@ -209,19 +212,19 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Balance
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Balance:',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
// Balances por unidad
|
||||
if (balanceSnapshot.connectionState == ConnectionState.waiting)
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Balance:',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (balanceSnapshot.connectionState == ConnectionState.waiting)
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
@@ -229,19 +232,11 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
strokeWidth: 2,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'${_formatBalance(balance)} sats',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
_buildBalancesList(units, balances),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
@@ -252,7 +247,7 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
if (!isActive)
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _setActiveMint(mint.url, walletProvider),
|
||||
onTap: () => _setActiveMint(mintUrl, walletProvider),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -288,7 +283,7 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
// Eliminar
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDeleteDialog(mint, balance, walletProvider),
|
||||
onTap: () => _showDeleteDialog(mintUrl, balances, walletProvider),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -327,6 +322,87 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Construye la lista de balances por unidad.
|
||||
Widget _buildBalancesList(List<String> units, Map<String, BigInt> balances) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Balances:',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...units.map((unit) {
|
||||
final balance = balances[unit] ?? BigInt.zero;
|
||||
final formattedBalance = UnitFormatter.formatBalance(balance, unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(unit);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, top: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: balance > BigInt.zero
|
||||
? AppColors.success
|
||||
: AppColors.textSecondary.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$formattedBalance $unitLabel',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: balance > BigInt.zero ? FontWeight.w600 : FontWeight.normal,
|
||||
color: balance > BigInt.zero
|
||||
? Colors.white
|
||||
: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresca la información del mint (detecta nuevas unidades).
|
||||
Future<void> _refreshMint(String mintUrl, WalletProvider walletProvider) async {
|
||||
try {
|
||||
final units = await walletProvider.refreshMint(mintUrl);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {}); // Rebuild para actualizar UI
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Unidades detectadas: ${units.join(", ")}'),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAddMintButton(WalletProvider walletProvider) {
|
||||
return PrimaryButton(
|
||||
text: 'Agregar mint',
|
||||
@@ -335,23 +411,6 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String _extractHost(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
return uri.host;
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatBalance(BigInt balance) {
|
||||
final value = balance.toInt();
|
||||
if (value >= 1000) {
|
||||
return '${(value / 1000).toStringAsFixed(value % 1000 == 0 ? 0 : 1)}k';
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
Future<void> _setActiveMint(String mintUrl, WalletProvider walletProvider) async {
|
||||
try {
|
||||
await walletProvider.setActiveMint(mintUrl);
|
||||
@@ -379,19 +438,26 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteDialog(Mint mint, BigInt balance, WalletProvider walletProvider) {
|
||||
final hasBalance = balance > BigInt.zero;
|
||||
void _showDeleteDialog(String mintUrl, Map<String, BigInt> balances, WalletProvider walletProvider) {
|
||||
// Verificar si hay balance en alguna unidad
|
||||
final hasBalance = balances.values.any((b) => b > BigInt.zero);
|
||||
|
||||
// Calcular balance total para mostrar (simplificado)
|
||||
final balanceStrings = balances.entries
|
||||
.where((e) => e.value > BigInt.zero)
|
||||
.map((e) => '${UnitFormatter.formatBalance(e.value, e.key)} ${UnitFormatter.getUnitLabel(e.key)}')
|
||||
.toList();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _DeleteMintModal(
|
||||
mintUrl: mint.url,
|
||||
mintUrl: mintUrl,
|
||||
hasBalance: hasBalance,
|
||||
balance: balance.toInt(),
|
||||
balanceDescription: balanceStrings.isEmpty ? '' : balanceStrings.join(', '),
|
||||
onConfirm: () async {
|
||||
Navigator.pop(context);
|
||||
await _deleteMint(mint.url, walletProvider);
|
||||
await _deleteMint(mintUrl, walletProvider);
|
||||
},
|
||||
onCancel: () => Navigator.pop(context),
|
||||
),
|
||||
@@ -443,14 +509,14 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
class _DeleteMintModal extends StatelessWidget {
|
||||
final String mintUrl;
|
||||
final bool hasBalance;
|
||||
final int balance;
|
||||
final String balanceDescription;
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const _DeleteMintModal({
|
||||
required this.mintUrl,
|
||||
required this.hasBalance,
|
||||
required this.balance,
|
||||
required this.balanceDescription,
|
||||
required this.onConfirm,
|
||||
required this.onCancel,
|
||||
});
|
||||
@@ -547,7 +613,7 @@ class _DeleteMintModal extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasBalance
|
||||
? 'Este mint tiene $balance sats. Puedes recuperarlos después agregando el mint de nuevo.'
|
||||
? 'Este mint tiene $balanceDescription. Puedes recuperarlos después agregando el mint de nuevo.'
|
||||
: 'Perderás acceso a este mint',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
@@ -1227,10 +1228,9 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
|
||||
Future<void> _loadMints() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final mints = await walletProvider.listMints();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_availableMints = mints.map((m) => m.url).toList();
|
||||
_availableMints = walletProvider.mintUrls;
|
||||
_isLoadingMints = false;
|
||||
if (_availableMints.isNotEmpty) {
|
||||
_selectedMintUrl = _availableMints.first;
|
||||
@@ -1749,27 +1749,46 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
if (_useCurrentMnemonic) {
|
||||
// Usar mnemonic actual
|
||||
if (_scanAllMints) {
|
||||
// Escanear todos los mints
|
||||
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
|
||||
final results = await walletProvider.restoreAllMints();
|
||||
|
||||
BigInt totalRecovered = BigInt.zero;
|
||||
int mintsScanned = 0;
|
||||
int mintsWithError = 0;
|
||||
final recoveredDetails = <String>[];
|
||||
|
||||
for (final entry in results.entries) {
|
||||
if (entry.value >= BigInt.zero) {
|
||||
totalRecovered += entry.value;
|
||||
mintsScanned++;
|
||||
} else {
|
||||
for (final mintEntry in results.entries) {
|
||||
final mintUrl = mintEntry.key;
|
||||
final unitBalances = mintEntry.value;
|
||||
bool hasError = false;
|
||||
BigInt mintTotal = BigInt.zero;
|
||||
|
||||
for (final unitEntry in unitBalances.entries) {
|
||||
final unit = unitEntry.key;
|
||||
final balance = unitEntry.value;
|
||||
if (balance < BigInt.zero) {
|
||||
hasError = true;
|
||||
} else if (balance > BigInt.zero) {
|
||||
mintTotal += balance;
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
mintsWithError++;
|
||||
} else {
|
||||
mintsScanned++;
|
||||
totalRecovered += mintTotal;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (totalRecovered > BigInt.zero) {
|
||||
_result = '¡Recuperados ${totalRecovered.toInt()} sats de $mintsScanned mint(s)!';
|
||||
if (recoveredDetails.isNotEmpty) {
|
||||
_result = '¡Recuperados ${recoveredDetails.join(", ")} de $mintsScanned mint(s)!';
|
||||
} else {
|
||||
_result = 'Escaneo completado. No se encontraron tokens nuevos.';
|
||||
}
|
||||
@@ -1778,7 +1797,7 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Escanear mint específico
|
||||
// Escanear mint específico (retorna Map<String, BigInt>)
|
||||
if (_selectedMintUrl == null) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -1788,14 +1807,25 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
return;
|
||||
}
|
||||
|
||||
final recovered = await walletProvider.restoreFromMint(_selectedMintUrl!);
|
||||
final mintHost = Uri.parse(_selectedMintUrl!).host;
|
||||
final unitBalances = await walletProvider.restoreFromMint(_selectedMintUrl!);
|
||||
final mintHost = UnitFormatter.getMintDisplayName(_selectedMintUrl!);
|
||||
|
||||
final recoveredDetails = <String>[];
|
||||
for (final entry in unitBalances.entries) {
|
||||
final unit = entry.key;
|
||||
final balance = entry.value;
|
||||
if (balance > BigInt.zero) {
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label');
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (recovered > BigInt.zero) {
|
||||
_result = '¡Recuperados ${recovered.toInt()} sats de $mintHost!';
|
||||
if (recoveredDetails.isNotEmpty) {
|
||||
_result = '¡Recuperados ${recoveredDetails.join(", ")} de $mintHost!';
|
||||
} else {
|
||||
_result = 'No se encontraron tokens en $mintHost.';
|
||||
}
|
||||
@@ -1816,8 +1846,7 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
}
|
||||
|
||||
// Obtener lista de mints actuales para escanear
|
||||
final mints = await walletProvider.listMints();
|
||||
final mintUrls = mints.map((m) => m.url).toList();
|
||||
final mintUrls = walletProvider.mintUrls;
|
||||
|
||||
if (mintUrls.isEmpty) {
|
||||
if (!mounted) return;
|
||||
@@ -1837,7 +1866,11 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (recovered > BigInt.zero) {
|
||||
_result = '¡Recuperados y transferidos ${recovered.toInt()} sats a tu wallet!';
|
||||
// Usamos la unidad activa como aproximación para el formato
|
||||
final activeUnit = walletProvider.activeUnit;
|
||||
final formatted = UnitFormatter.formatBalance(recovered, activeUnit);
|
||||
final label = UnitFormatter.getUnitLabel(activeUnit);
|
||||
_result = '¡Recuperados y transferidos $formatted $label a tu wallet!';
|
||||
} else {
|
||||
_result = 'No se encontraron tokens asociados a ese mnemonic.';
|
||||
}
|
||||
|
||||
+33
-1
@@ -33,6 +33,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
cbor:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cbor
|
||||
sha256: e60380c7329da6b415841be93884b8d4380cbd86cd4cecb2067baa221b8d88b5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.5"
|
||||
cdk_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -66,6 +74,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -222,8 +238,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
http:
|
||||
hex:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hex
|
||||
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
@@ -238,6 +262,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
ieee754:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ieee754
|
||||
sha256: "7d87451c164a56c156180d34a4e93779372edd191d2c219206100b976203128c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -41,6 +41,12 @@ dependencies:
|
||||
# URL Launcher
|
||||
url_launcher: ^6.2.0
|
||||
|
||||
# HTTP client for mint API calls
|
||||
http: ^1.2.0
|
||||
|
||||
# CBOR decoder for V4 tokens
|
||||
cbor: ^6.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user