feat: improve swap price chart — real data only, 24h stats, min/max

This commit is contained in:
Forte11Cuba
2026-04-11 00:19:43 -06:00
parent e8d8789413
commit 218d798d2a
24 changed files with 260 additions and 46 deletions
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Swap abgeschlossen",
"swapErrorInsufficient": "Unzureichendes Guthaben",
"swapErrorExpired": "Angebot abgelaufen",
"swapErrorGeneric": "Swap-Fehler: {error}"
"swapErrorGeneric": "Swap-Fehler: {error}",
"swapChartUnavailable": "Preis nicht verfügbar · Tippen zum Wiederholen",
"swapChartMin": "Min",
"swapChartMax": "Max"
}
+4 -1
View File
@@ -466,5 +466,8 @@
"swapSuccess": "Swap completed",
"swapErrorInsufficient": "Insufficient balance",
"swapErrorExpired": "Quote has expired",
"swapErrorGeneric": "Swap error: {error}"
"swapErrorGeneric": "Swap error: {error}",
"swapChartUnavailable": "Price unavailable · Tap to retry",
"swapChartMin": "Min",
"swapChartMax": "Max"
}
+4 -1
View File
@@ -596,5 +596,8 @@
"placeholders": {
"error": { "type": "String" }
}
}
},
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
"swapChartMin": "Mín",
"swapChartMax": "Máx"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Swap terminé",
"swapErrorInsufficient": "Solde insuffisant",
"swapErrorExpired": "Le devis a expiré",
"swapErrorGeneric": "Erreur de swap : {error}"
"swapErrorGeneric": "Erreur de swap : {error}",
"swapChartUnavailable": "Prix indisponible · Appuyez pour réessayer",
"swapChartMin": "Min",
"swapChartMax": "Max"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Swap completato",
"swapErrorInsufficient": "Saldo insufficiente",
"swapErrorExpired": "Il preventivo è scaduto",
"swapErrorGeneric": "Errore swap: {error}"
"swapErrorGeneric": "Errore swap: {error}",
"swapChartUnavailable": "Prezzo non disponibile · Tocca per riprovare",
"swapChartMin": "Min",
"swapChartMax": "Max"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "スワップ完了",
"swapErrorInsufficient": "残高不足",
"swapErrorExpired": "見積もりの有効期限切れ",
"swapErrorGeneric": "スワップエラー: {error}"
"swapErrorGeneric": "スワップエラー: {error}",
"swapChartUnavailable": "価格を取得できません・タップで再試行",
"swapChartMin": "安値",
"swapChartMax": "高値"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "교환 완료",
"swapErrorInsufficient": "잔액 부족",
"swapErrorExpired": "견적이 만료되었습니다",
"swapErrorGeneric": "교환 오류: {error}"
"swapErrorGeneric": "교환 오류: {error}",
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
"swapChartMin": "최저",
"swapChartMax": "최고"
}
+18
View File
@@ -2430,6 +2430,24 @@ abstract class L10n {
/// In es, this message translates to:
/// **'Error en el swap: {error}'**
String swapErrorGeneric(String error);
/// No description provided for @swapChartUnavailable.
///
/// In es, this message translates to:
/// **'Precio no disponible · Toca para reintentar'**
String get swapChartUnavailable;
/// No description provided for @swapChartMin.
///
/// In es, this message translates to:
/// **'Mín'**
String get swapChartMin;
/// No description provided for @swapChartMax.
///
/// In es, this message translates to:
/// **'Máx'**
String get swapChartMax;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
+10
View File
@@ -1284,4 +1284,14 @@ class L10nDe extends L10n {
String swapErrorGeneric(String error) {
return 'Swap-Fehler: $error';
}
@override
String get swapChartUnavailable =>
'Preis nicht verfügbar · Tippen zum Wiederholen';
@override
String get swapChartMin => 'Min';
@override
String get swapChartMax => 'Max';
}
+9
View File
@@ -1265,4 +1265,13 @@ class L10nEn extends L10n {
String swapErrorGeneric(String error) {
return 'Swap error: $error';
}
@override
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
@override
String get swapChartMin => 'Min';
@override
String get swapChartMax => 'Max';
}
+10
View File
@@ -1273,4 +1273,14 @@ class L10nEs extends L10n {
String swapErrorGeneric(String error) {
return 'Error en el swap: $error';
}
@override
String get swapChartUnavailable =>
'Precio no disponible · Toca para reintentar';
@override
String get swapChartMin => 'Mín';
@override
String get swapChartMax => 'Máx';
}
+10
View File
@@ -1289,4 +1289,14 @@ class L10nFr extends L10n {
String swapErrorGeneric(String error) {
return 'Erreur de swap : $error';
}
@override
String get swapChartUnavailable =>
'Prix indisponible · Appuyez pour réessayer';
@override
String get swapChartMin => 'Min';
@override
String get swapChartMax => 'Max';
}
+10
View File
@@ -1277,4 +1277,14 @@ class L10nIt extends L10n {
String swapErrorGeneric(String error) {
return 'Errore swap: $error';
}
@override
String get swapChartUnavailable =>
'Prezzo non disponibile · Tocca per riprovare';
@override
String get swapChartMin => 'Min';
@override
String get swapChartMax => 'Max';
}
+9
View File
@@ -1250,4 +1250,13 @@ class L10nJa extends L10n {
String swapErrorGeneric(String error) {
return 'スワップエラー: $error';
}
@override
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
@override
String get swapChartMin => '安値';
@override
String get swapChartMax => '高値';
}
+9
View File
@@ -1252,4 +1252,13 @@ class L10nKo extends L10n {
String swapErrorGeneric(String error) {
return '교환 오류: $error';
}
@override
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
@override
String get swapChartMin => '최저';
@override
String get swapChartMax => '최고';
}
+10
View File
@@ -1277,4 +1277,14 @@ class L10nPt extends L10n {
String swapErrorGeneric(String error) {
return 'Erro no swap: $error';
}
@override
String get swapChartUnavailable =>
'Preço indisponível · Toque para tentar novamente';
@override
String get swapChartMin => 'Mín';
@override
String get swapChartMax => 'Máx';
}
+9
View File
@@ -1273,4 +1273,13 @@ class L10nRu extends L10n {
String swapErrorGeneric(String error) {
return 'Ошибка обмена: $error';
}
@override
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
@override
String get swapChartMin => 'Мин';
@override
String get swapChartMax => 'Макс';
}
+9
View File
@@ -1274,4 +1274,13 @@ class L10nSw extends L10n {
String swapErrorGeneric(String error) {
return 'Kosa la ubadilishaji: $error';
}
@override
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
@override
String get swapChartMin => 'Chini';
@override
String get swapChartMax => 'Juu';
}
+9
View File
@@ -1245,4 +1245,13 @@ class L10nZh extends L10n {
String swapErrorGeneric(String error) {
return '兑换错误: $error';
}
@override
String get swapChartUnavailable => '价格不可用 · 点击重试';
@override
String get swapChartMin => '最低';
@override
String get swapChartMax => '最高';
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Swap concluído",
"swapErrorInsufficient": "Saldo insuficiente",
"swapErrorExpired": "A cotação expirou",
"swapErrorGeneric": "Erro no swap: {error}"
"swapErrorGeneric": "Erro no swap: {error}",
"swapChartUnavailable": "Preço indisponível · Toque para tentar novamente",
"swapChartMin": "Mín",
"swapChartMax": "Máx"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Обмен завершён",
"swapErrorInsufficient": "Недостаточный баланс",
"swapErrorExpired": "Котировка истекла",
"swapErrorGeneric": "Ошибка обмена: {error}"
"swapErrorGeneric": "Ошибка обмена: {error}",
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
"swapChartMin": "Мин",
"swapChartMax": "Макс"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "Ubadilishaji umekamilika",
"swapErrorInsufficient": "Salio haitoshi",
"swapErrorExpired": "Bei imeisha muda",
"swapErrorGeneric": "Kosa la ubadilishaji: {error}"
"swapErrorGeneric": "Kosa la ubadilishaji: {error}",
"swapChartUnavailable": "Bei haipatikani · Gusa kurudia",
"swapChartMin": "Chini",
"swapChartMax": "Juu"
}
+4 -1
View File
@@ -573,5 +573,8 @@
"swapSuccess": "兑换完成",
"swapErrorInsufficient": "余额不足",
"swapErrorExpired": "报价已过期",
"swapErrorGeneric": "兑换错误: {error}"
"swapErrorGeneric": "兑换错误: {error}",
"swapChartUnavailable": "价格不可用 · 点击重试",
"swapChartMin": "最低",
"swapChartMax": "最高"
}
+80 -21
View File
@@ -44,6 +44,7 @@ class _SwapScreenState extends State<SwapScreen>
List<double> _chartData = [];
bool _isLoadingChart = true;
bool _chartError = false;
// --- Swap state ---
bool _isSwapping = false;
@@ -81,7 +82,6 @@ class _SwapScreenState extends State<SwapScreen>
Future<void> _loadBalances() async {
final walletProvider = context.read<WalletProvider>();
final mintUrl = WalletProvider.cubaBitcoinMint;
if (mintUrl == null) return;
try {
final balances = await walletProvider.getBalancesForMint(mintUrl);
@@ -96,6 +96,10 @@ class _SwapScreenState extends State<SwapScreen>
}
Future<void> _loadChartData() async {
setState(() {
_isLoadingChart = true;
_chartError = false;
});
try {
final prices = await PriceService.getHistoricalPrices(range: 'ONE_DAY');
if (mounted && prices.isNotEmpty) {
@@ -107,22 +111,14 @@ class _SwapScreenState extends State<SwapScreen>
} catch (_) {
if (mounted) {
setState(() {
_chartData = _generateMockData();
_chartData = [];
_isLoadingChart = false;
_chartError = true;
});
}
}
}
List<double> _generateMockData() {
final priceProvider = context.read<PriceProvider>();
final basePrice = priceProvider.btcPriceUsd ?? 65000;
final rng = Random();
return List.generate(24, (i) {
return basePrice + (rng.nextDouble() - 0.48) * basePrice * 0.02;
});
}
void _onFromChanged() {
if (_isUpdating) return;
_lastEditedFrom = true;
@@ -263,7 +259,6 @@ class _SwapScreenState extends State<SwapScreen>
Future<void> _startSwap() async {
final walletProvider = context.read<WalletProvider>();
final mintUrl = WalletProvider.cubaBitcoinMint;
if (mintUrl == null) return;
final destAmount = _getDestAmount();
if (destAmount <= BigInt.zero) return;
@@ -708,24 +703,42 @@ class _SwapScreenState extends State<SwapScreen>
// --- Widgets ---
Widget _buildPriceChart(PriceProvider priceProvider) {
final l10n = L10n.of(context)!;
final btcPrice = priceProvider.btcPriceUsd;
final priceStr = btcPrice != null
? '\$${NumberFormat('#,###').format(btcPrice.round())}'
: '...';
final fmt = NumberFormat('#,###');
final priceStr = btcPrice != null ? '\$${fmt.format(btcPrice.round())}' : '...';
String rateStr = '...';
if (btcPrice != null && btcPrice > 0) {
final satsPerDollar = (100000000 / btcPrice).round();
rateStr = '1 USD ≈ ${NumberFormat('#,###').format(satsPerDollar)} sats';
rateStr = '1 USD ≈ ${fmt.format(satsPerDollar)} sats';
}
final isUpTrend =
_chartData.length >= 2 && _chartData.last >= _chartData.first;
final hasData = _chartData.length >= 2;
final isUpTrend = hasData && _chartData.last >= _chartData.first;
final trendColor = isUpTrend ? AppColors.success : AppColors.error;
// % cambio 24h
String? changeStr;
if (hasData) {
final change = ((_chartData.last - _chartData.first) / _chartData.first) * 100;
final sign = change >= 0 ? '+' : '';
changeStr = '$sign${change.toStringAsFixed(1)}% 24h';
}
// Min/Max 24h
String? minMaxStr;
if (hasData) {
final minVal = _chartData.reduce(min);
final maxVal = _chartData.reduce(max);
final minLabel = l10n.swapChartMin;
final maxLabel = l10n.swapChartMax;
minMaxStr = '24h $minLabel: \$${fmt.format(minVal.round())}$maxLabel: \$${fmt.format(maxVal.round())}';
}
return Column(
children: [
// Price
// Precio centrado
Text(
priceStr,
style: const TextStyle(
@@ -736,8 +749,11 @@ class _SwapScreenState extends State<SwapScreen>
),
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'USD',
'BTC/USD',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
@@ -745,9 +761,23 @@ class _SwapScreenState extends State<SwapScreen>
color: AppColors.textSecondary.withValues(alpha: 0.6),
),
),
if (changeStr != null) ...[
const SizedBox(width: 8),
Text(
changeStr,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
fontWeight: FontWeight.w600,
color: trendColor,
),
),
],
],
),
const SizedBox(height: 8),
// Sparkline (sin contenedor, integrado)
// Sparkline o estado de error
SizedBox(
height: 40,
width: double.infinity,
@@ -762,6 +792,20 @@ class _SwapScreenState extends State<SwapScreen>
),
),
)
: _chartError
? GestureDetector(
onTap: _loadChartData,
child: Center(
child: Text(
l10n.swapChartUnavailable,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
)
: CustomPaint(
painter: _SparklinePainter(
data: _chartData,
@@ -772,6 +816,21 @@ class _SwapScreenState extends State<SwapScreen>
),
const SizedBox(height: 8),
// Min/Max 24h
if (minMaxStr != null)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
minMaxStr,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
// Exchange rate
Text(
rateStr,