diff --git a/app/lib/helpers/transaction_helpers.dart b/app/lib/helpers/transaction_helpers.dart index d4bb3f17b..3d0352773 100644 --- a/app/lib/helpers/transaction_helpers.dart +++ b/app/lib/helpers/transaction_helpers.dart @@ -12,3 +12,47 @@ Decimal roundAmount(String amount) { Decimal parsedAmount = Decimal.parse(amount).shift(2).floor().shift(-2); return parsedAmount; } + +String formatAmountDisplay(String amount) { + try { + final Decimal decimalAmount = Decimal.parse(amount); + if (decimalAmount == Decimal.zero) return '0'; + + final Decimal absAmount = decimalAmount.abs(); + final double doubleAmount = absAmount.toDouble(); + String formatted; + + if (absAmount % Decimal.one == Decimal.zero) { + formatted = NumberFormat('#,##0').format(doubleAmount); + } else if (doubleAmount >= 1000) { + formatted = NumberFormat('#,##0.00').format(doubleAmount); + } else if (doubleAmount >= 1) { + formatted = NumberFormat('0.###').format(doubleAmount); + } else if (doubleAmount >= 0.01) { + formatted = NumberFormat('0.####').format(doubleAmount); + } else { + // Very small amounts: Show with approximation for clarity + String decimals = absAmount.toString().split('.').length > 1 + ? absAmount.toString().split('.')[1] + : ''; + int firstNonZero = decimals.indexOf(RegExp(r'[1-9]')); + if (firstNonZero != -1) { + int precision = (firstNonZero + 2).clamp(0, 8); + double approxValue = double.parse(absAmount.toStringAsFixed(precision)); + String approximated = NumberFormat('0.${'0' * (precision - 1)}#').format(approxValue); + formatted = decimals.length > precision ? '~$approximated' : approximated; + } else { + formatted = NumberFormat('0.########').format(doubleAmount); + } + } + + // Remove trailing zeros after decimal point only (if any), but keep approximation symbol if present + if (formatted.contains('.')) { + formatted = formatted.replaceFirst(RegExp(r'(\.\d*?[1-9])0+\u001b'), r'$1\u001b'); + formatted = formatted.replaceFirst(RegExp(r'\.$'), ''); + } + return formatted; + } catch (e) { + return amount; + } +} \ No newline at end of file diff --git a/app/lib/screens/market/buy_tft.dart b/app/lib/screens/market/buy_tft.dart index 5e2a555b1..32b25f942 100644 --- a/app/lib/screens/market/buy_tft.dart +++ b/app/lib/screens/market/buy_tft.dart @@ -39,9 +39,13 @@ class _BuyTFTWidgetState extends State { void initState() { super.initState(); amountController = TextEditingController( - text: widget.edit ? widget.offer?.amount.toString() ?? '' : ''); + text: widget.edit + ? formatAmountDisplay(widget.offer?.amount.toString() ?? '') + : ''); priceController = TextEditingController( - text: widget.edit ? widget.offer?.price.toString() ?? '' : ''); + text: widget.edit + ? formatAmountDisplay(widget.offer?.price.toString() ?? '') + : ''); amountController.addListener(_calculateTotal); priceController.addListener(_calculateTotal); if (widget.edit) _calculateTotal(); @@ -96,7 +100,7 @@ class _BuyTFTWidgetState extends State { currentMarketPrice = 1 / marketData.lastUsdPrice; if (!widget.edit && priceController.text.isEmpty) { - priceController.text = currentMarketPrice!.toStringAsFixed(7); + priceController.text = formatAmountDisplay(currentMarketPrice!.toString()); _calculateTotal(); } }); @@ -171,7 +175,7 @@ class _BuyTFTWidgetState extends State { calculateAmount(int percentage) { final amount = Decimal.parse(availableUSDC ?? '0') * (Decimal.fromInt(percentage).shift(-2)); - amountController.text = roundAmount(amount.toString()).toString(); + amountController.text = formatAmountDisplay(roundAmount(amount.toString()).toString()); _calculateTotal(); } @@ -184,7 +188,7 @@ class _BuyTFTWidgetState extends State { final amount = Decimal.parse(amountText); final price = Decimal.parse(priceText); final total = amount * price; - totalAmountController.text = roundAmount(total.toString()).toString(); + totalAmountController.text = formatAmountDisplay(total.toString()); } catch (e) { totalAmountController.text = ''; } @@ -504,7 +508,7 @@ class _BuyTFTWidgetState extends State { ], ) : Text( - 'Available: $availableUSDC USDC', + 'Available: ${formatAmountDisplay(availableUSDC ?? '0')} USDC', style: Theme.of(context) .textTheme .bodySmall! diff --git a/app/lib/screens/market/order_book.dart b/app/lib/screens/market/order_book.dart index c805d32b2..0e10a3265 100644 --- a/app/lib/screens/market/order_book.dart +++ b/app/lib/screens/market/order_book.dart @@ -4,6 +4,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; import 'package:threebotlogin/models/order_book.dart'; import 'package:threebotlogin/services/stellar_service.dart'; @@ -256,9 +257,9 @@ class _OrderbookWidgetState extends State { bid != null ? (double.tryParse(bid.price) != null && double.parse(bid.price) > 0) - ? (double.parse(bid.amount) / + ? formatAmountDisplay((double.parse(bid.amount) / double.parse(bid.price)) - .toStringAsFixed(7) + .toString()) : '' : '', style: Theme.of(context) @@ -272,7 +273,7 @@ class _OrderbookWidgetState extends State { bid != null ? (double.tryParse(bid.price) != null && double.parse(bid.price) > 0) - ? bid.price.toString() + ? formatAmountDisplay(bid.price.toString()) : '' : '', style: Theme.of(context) @@ -296,14 +297,14 @@ class _OrderbookWidgetState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text(ask != null ? ask.amount.toString() : '', + Text(ask != null ? formatAmountDisplay(ask.amount.toString()) : '', style: Theme.of(context) .textTheme .bodySmall! .copyWith( color: Theme.of(context).colorScheme.error)), - Text(ask != null ? ask.price.toString() : '', + Text(ask != null ? formatAmountDisplay(ask.price.toString()) : '', style: Theme.of(context) .textTheme .bodySmall! diff --git a/app/lib/screens/market/order_details.dart b/app/lib/screens/market/order_details.dart index a56e425a0..e1163f2f4 100644 --- a/app/lib/screens/market/order_details.dart +++ b/app/lib/screens/market/order_details.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stellar_client/models/exceptions.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; import 'package:threebotlogin/models/offer.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/services/stellar_service.dart' as Stellar; @@ -248,7 +249,7 @@ class _OrderDetailsWidgetState extends State { ), ), Text( - '${totalCost.toStringAsFixed(2)} TFT', + '${formatAmountDisplay(totalCost.toString())} TFT', style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.primary, ), @@ -267,7 +268,7 @@ class _OrderDetailsWidgetState extends State { ), ), Text( - '${pricePerUSDC.toString()} USDC', + '${formatAmountDisplay(pricePerUSDC.toString())} USDC', style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.onSurface, ), @@ -283,7 +284,7 @@ class _OrderDetailsWidgetState extends State { ), ), Text( - '${pricePerTFT.toStringAsFixed(2)} TFT', + '${formatAmountDisplay(pricePerTFT.toString())} TFT', style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.onSurface, ), @@ -299,7 +300,7 @@ class _OrderDetailsWidgetState extends State { ), ), Text( - '${amount.toStringAsFixed(2)} USDC', + '${formatAmountDisplay(amount.toString())} USDC', style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.error, ), diff --git a/app/lib/screens/market/overview.dart b/app/lib/screens/market/overview.dart index 16bacb44f..a3b658797 100644 --- a/app/lib/screens/market/overview.dart +++ b/app/lib/screens/market/overview.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; import 'package:threebotlogin/models/market_data.dart'; import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; @@ -294,7 +295,7 @@ class _OverviewWidgetState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - marketData!.lastPrice.toStringAsFixed(7), + formatAmountDisplay(marketData!.lastPrice.toString()), style: Theme.of(context) .textTheme .headlineLarge! @@ -437,9 +438,9 @@ class _OverviewWidgetState extends ConsumerState { CrossAxisAlignment.start, children: [ _buildMarketColumn('Last Price', - '${marketData!.lastPrice.toStringAsFixed(7)} USDC'), + '${formatAmountDisplay(marketData!.lastPrice.toString())} USDC'), _buildMarketColumn('Last USD Price', - '\$${marketData!.lastUsdPrice.toStringAsFixed(7)}'), + '\$${formatAmountDisplay(marketData!.lastUsdPrice.toString())}'), ], ), ), @@ -450,9 +451,9 @@ class _OverviewWidgetState extends ConsumerState { CrossAxisAlignment.start, children: [ _buildMarketColumn('24H High', - '${marketData!.high24h.toStringAsFixed(7)} USDC'), + '${formatAmountDisplay(marketData!.high24h.toString())} USDC'), _buildMarketColumn('24H Low', - '${marketData!.low24h.toStringAsFixed(7)} USDC'), + '${formatAmountDisplay(marketData!.low24h.toString())} USDC'), ], ), ), @@ -497,16 +498,16 @@ class _OverviewWidgetState extends ConsumerState { children: [ _buildMarketColumn( 'TFT Balance', - _selectedWallet - ?.stellarBalances['TFT']!), + formatAmountDisplay(_selectedWallet + ?.stellarBalances['TFT']! ?? '0')), _buildMarketColumn( 'USDC Balance', - _selectedWallet - ?.stellarBalances['USDC']!), + formatAmountDisplay(_selectedWallet + ?.stellarBalances['USDC']! ?? '0')), _buildMarketColumn( 'XLM Balance', - _selectedWallet - ?.stellarBalances['XLM']!), + formatAmountDisplay(_selectedWallet + ?.stellarBalances['XLM']! ?? '0')), ], ), ], diff --git a/app/lib/services/stellar_service.dart b/app/lib/services/stellar_service.dart index 8bc07ba25..694201618 100644 --- a/app/lib/services/stellar_service.dart +++ b/app/lib/services/stellar_service.dart @@ -6,6 +6,7 @@ import 'package:stellar_client/models/vesting_account.dart'; import 'package:stellar_client/stellar_client.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; import 'package:threebotlogin/models/market_data.dart'; import 'package:threebotlogin/models/offer.dart'; import 'package:threebotlogin/models/order_book.dart'; @@ -319,7 +320,7 @@ Future getAvailableUSDCBalance( return sum + double.parse(offer.amount); }); final balance = await getBalanceByClient(client); - return (double.parse(balance['USDC']!) - totalReserved).toStringAsFixed(7); + return formatAmountDisplay((double.parse(balance['USDC']!) - totalReserved).toString()); } String getAssetName(Asset asset) { diff --git a/app/lib/widgets/market/order_card.dart b/app/lib/widgets/market/order_card.dart index f5556db6f..85829291f 100644 --- a/app/lib/widgets/market/order_card.dart +++ b/app/lib/widgets/market/order_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/helpers/logger.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; import 'package:threebotlogin/models/offer.dart'; import 'package:intl/intl.dart'; import 'package:threebotlogin/models/wallet.dart' as Wallet; @@ -49,13 +50,13 @@ class _OrderCardWidgetState extends ConsumerState { List cardContent = []; cardContent = [ - _buildInfoRow(context, 'Amount:', '- ${amount.toStringAsFixed(2)} USDC', + _buildInfoRow(context, 'Amount:', '- ${formatAmountDisplay(amount.toString())} USDC', textColor: Theme.of(context).colorScheme.error), _buildInfoRow( - context, 'Price per TFT:', '${pricePerTFT.toStringAsFixed(4)} USDC', + context, 'Price per TFT:', '${formatAmountDisplay(pricePerTFT.toString())} USDC', isHighlighted: true), _buildInfoRow(context, 'Total Received:', - '+ ${(totalCost).toStringAsFixed(4)} TFT', + '+ ${formatAmountDisplay(totalCost.toString())} TFT', textColor: Theme.of(context).colorScheme.primary), ]; diff --git a/app/test/transaction_helpers_test.dart b/app/test/transaction_helpers_test.dart new file mode 100644 index 000000000..64ef3254d --- /dev/null +++ b/app/test/transaction_helpers_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:threebotlogin/helpers/transaction_helpers.dart'; + +void main() { + group('formatAmountDisplay', () { + test('handles large amounts (user-friendly)', () { + expect(formatAmountDisplay('1234.567890'), equals('1,234.57')); // 2 decimals + thousand separators + expect(formatAmountDisplay('5000.00'), equals('5,000')); // Remove trailing zeros + separators + expect(formatAmountDisplay('10000.123'), equals('10,000.12')); // 2 decimals max + separators + expect(formatAmountDisplay('1000000.00'), equals('1,000,000')); // Large numbers with separators + }); + + test('handles medium amounts (user-friendly)', () { + expect(formatAmountDisplay('123.456789'), equals('123.457')); // 3 decimals for medium amounts + expect(formatAmountDisplay('1.50'), equals('1.5')); // Remove trailing zeros + expect(formatAmountDisplay('99.999'), equals('99.999')); // Keep significant decimals + }); + + test('handles small amounts (user-friendly)', () { + expect(formatAmountDisplay('0.12345678'), equals('0.1235')); // 4 decimals for small amounts + expect(formatAmountDisplay('0.1000'), equals('0.1')); // Remove trailing zeros + }); + + test('handles very small amounts with approximation', () { + expect(formatAmountDisplay('0.00670241'), equals('~0.0067')); // Approximated with ~ symbol + expect(formatAmountDisplay('0.001234567'), equals('~0.0012')); // Approximated with ~ symbol + expect(formatAmountDisplay('0.0005'), equals('0.0005')); // Keep as-is if short enough + expect(formatAmountDisplay('0.000100'), equals('0.0001')); // Remove trailing zeros + }); + + test('handles tiny amounts with approximation', () { + expect(formatAmountDisplay('0.000012345678'), equals('~0.000012')); // Approximate tiny amounts + expect(formatAmountDisplay('0.00000123456789'), equals('~0.0000012')); // Show 2 significant digits + expect(formatAmountDisplay('0.00000006'), equals('0.00000006')); // Keep if short enough + }); + + test('handles whole numbers', () { + expect(formatAmountDisplay('5'), equals('5')); + expect(formatAmountDisplay('100'), equals('100')); + expect(formatAmountDisplay('1000'), equals('1,000')); // Thousand separator added + }); + + test('handles edge cases', () { + expect(formatAmountDisplay('0'), equals('0')); + expect(formatAmountDisplay('0.0'), equals('0')); + expect(formatAmountDisplay('0.00'), equals('0')); + }); + + test('removes trailing zeros consistently', () { + expect(formatAmountDisplay('1.50000000'), equals('1.5')); + expect(formatAmountDisplay('0.12345600'), equals('0.1235')); + expect(formatAmountDisplay('1000.00'), equals('1,000')); // Thousand separator added + expect(formatAmountDisplay('0.001200'), equals('0.0012')); + }); + }); +}