diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b389d788..f8411cea1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,3 @@ -# Komodo Wallet v0.9.1 Release Notes - -This is a hotfix release that addresses critical issues with Trezor hardware wallet login functionality. - -## 🐛 Bug Fixes - -- **Trezor Login Issues** - Fixed critical bugs in the Trezor hardware wallet login flow that were preventing users from accessing their wallets. - -**Full Changelog**: [0.9.0...0.9.1](https://github.com/KomodoPlatform/komodo-wallet/compare/0.9.0...0.9.1) - ---- - # Komodo Wallet v0.9.0 Release Notes We are excited to announce Komodo Wallet v0.9.0. This release introduces HD wallet functionality, cross-platform fiat on-ramp improvements, a new feedback provider, and numerous bug fixes and dependency upgrades. diff --git a/docs/FLUTTER_VERSION.md b/docs/FLUTTER_VERSION.md index b3212c98b1..ef592a7f7b 100644 --- a/docs/FLUTTER_VERSION.md +++ b/docs/FLUTTER_VERSION.md @@ -2,7 +2,7 @@ ## Supported Flutter Version -This project supports Flutter `3.29.2`. We aim to keep the project up-to-date with the latest `stable` Flutter release. +This project supports Flutter `3.29.2` (latest stable release). We aim to keep the project up-to-date with the most recent stable Flutter versions. ## Recommended Approach: Multiple Flutter Versions diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 05c682d0ad..aecdb79438 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -194,7 +194,7 @@ class AppBlocRoot extends StatelessWidget { komodoDefiSdk, )..add( const PriceChartStarted( - symbols: ['BTC'], + symbols: ['KMD'], period: Duration(days: 30), ), ), diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart index fe286f2e51..08ab9cf077 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart @@ -11,7 +11,6 @@ import 'package:web_dex/bloc/cex_market_data/sdk_auth_activation_extension.dart' import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/shared/utils/extensions/legacy_coin_migration_extensions.dart'; part 'portfolio_growth_event.dart'; part 'portfolio_growth_state.dart'; @@ -121,13 +120,10 @@ class PortfolioGrowthBloc await emit.forEach( // computation is omitted, so null-valued events are emitted on a set // interval. - Stream.periodic(event.updateFrequency).asyncMap((_) async { - // Update prices before fetching chart data - await portfolioGrowthRepository.updatePrices(); - return _fetchPortfolioGrowthChart(event); - }), + Stream.periodic(event.updateFrequency) + .asyncMap((_) async => _fetchPortfolioGrowthChart(event)), onData: (data) => - _handlePortfolioGrowthUpdate(data, event.selectedPeriod, event.coins), + _handlePortfolioGrowthUpdate(data, event.selectedPeriod), onError: (error, stackTrace) { _log.shout('Failed to load portfolio growth', error, stackTrace); return GrowthChartLoadFailure( @@ -168,21 +164,10 @@ class PortfolioGrowthBloc return state; } - // Fetch prices before calculating total change - // This ensures we have the latest prices in the cache - await portfolioGrowthRepository.updatePrices(); - - final totalBalance = _calculateTotalBalance(coins); - final totalChange24h = _calculateTotalChange24h(coins); - final percentageChange24h = _calculatePercentageChange24h(coins); - return PortfolioGrowthChartLoadSuccess( portfolioGrowth: chart, percentageIncrease: chart.percentageIncrease, selectedPeriod: event.selectedPeriod, - totalBalance: totalBalance, - totalChange24h: totalChange24h, - percentageChange24h: percentageChange24h, ); } @@ -220,71 +205,20 @@ class PortfolioGrowthBloc PortfolioGrowthState _handlePortfolioGrowthUpdate( ChartData growthChart, Duration selectedPeriod, - List coins, ) { if (growthChart.isEmpty && state is PortfolioGrowthChartLoadSuccess) { return state; } final percentageIncrease = growthChart.percentageIncrease; - final totalBalance = _calculateTotalBalance(coins); - final totalChange24h = _calculateTotalChange24h(coins); - final percentageChange24h = _calculatePercentageChange24h(coins); + + // TODO? Include the center value in the bloc state instead of + // calculating it in the UI return PortfolioGrowthChartLoadSuccess( portfolioGrowth: growthChart, percentageIncrease: percentageIncrease, selectedPeriod: selectedPeriod, - totalBalance: totalBalance, - totalChange24h: totalChange24h, - percentageChange24h: percentageChange24h, - ); - } - - /// Calculate the total balance of all coins in USD - double _calculateTotalBalance(List coins) { - double total = coins.fold( - 0, - (prev, coin) => prev + (coin.lastKnownUsdBalance(sdk) ?? 0), ); - - // Return at least 0.01 if total is positive but very small - if (total > 0 && total < 0.01) { - return 0.01; - } - - return total; - } - - /// Calculate the total 24h change in USD value - double _calculateTotalChange24h(List coins) { - // Calculate the 24h change by summing the change percentage of each coin - // multiplied by its USD balance and divided by 100 (to convert percentage to decimal) - return coins.fold( - 0.0, - (sum, coin) { - // Use the price change from the CexPrice if available - final usdBalance = coin.lastKnownUsdBalance(sdk) ?? 0.0; - // Get the coin price from the repository's prices cache - final price = portfolioGrowthRepository - .getCachedPrice(coin.id.symbol.configSymbol.toUpperCase()); - final change24h = price?.change24h ?? 0.0; - return sum + (change24h * usdBalance / 100); - }, - ); - } - - /// Calculate the percentage change over 24h for the entire portfolio - double _calculatePercentageChange24h(List coins) { - final double totalBalance = _calculateTotalBalance(coins); - final double totalChange = _calculateTotalChange24h(coins); - - // Avoid division by zero or very small balances - if (totalBalance <= 0.01) { - return 0.0; - } - - // Return the percentage change - return (totalChange / totalBalance) * 100; } } diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart index 63398b5f2d..d369b95dab 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_repository.dart @@ -71,11 +71,8 @@ class PortfolioGrowthRepository { /// The graph cache provider to store the portfolio growth graph data. final PersistenceProvider _graphCache; - /// The SDK needed for connecting to blockchain nodes - final KomodoDefiSdk _sdk; - - /// The coins repository for detailed coin info final CoinsRepo _coinsRepository; + final KomodoDefiSdk _sdk; final _log = Logger('PortfolioGrowthRepository'); @@ -517,38 +514,4 @@ class PortfolioGrowthRepository { } Future clearCache() => _graphCache.deleteAll(); - - /// Calculate the total 24h change in USD value for a list of coins - /// - /// This method fetches the current prices for all coins and calculates - /// the 24h change by multiplying each coin's percentage change by its USD balance - Future calculateTotalChange24h(List coins) async { - // Fetch current prices including 24h change data - final prices = await _coinsRepository.fetchCurrentPrices() ?? {}; - - // Calculate the 24h change by summing the change percentage of each coin - // multiplied by its USD balance and divided by 100 (to convert percentage to decimal) - double totalChange = 0.0; - for (final coin in coins) { - final price = prices[coin.id.symbol.configSymbol.toUpperCase()]; - final change24h = price?.change24h ?? 0.0; - final usdBalance = coin.lastKnownUsdBalance(_sdk) ?? 0.0; - totalChange += (change24h * usdBalance / 100); - } - return totalChange; - } - - /// Get the cached price for a given coin symbol - /// - /// This is used to avoid fetching prices for every calculation - CexPrice? getCachedPrice(String symbol) { - return _coinsRepository.getCachedPrice(symbol); - } - - /// Update prices for all coins by fetching from market data - /// - /// This method ensures we have up-to-date price data before calculations - Future updatePrices() async { - await _coinsRepository.fetchCurrentPrices(); - } } diff --git a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_state.dart b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_state.dart index a964bbca16..bd53543cb4 100644 --- a/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_state.dart +++ b/lib/bloc/cex_market_data/portfolio_growth/portfolio_growth_state.dart @@ -19,25 +19,16 @@ final class PortfolioGrowthChartLoadSuccess extends PortfolioGrowthState { required this.portfolioGrowth, required this.percentageIncrease, required super.selectedPeriod, - required this.totalBalance, - required this.totalChange24h, - required this.percentageChange24h, }); final ChartData portfolioGrowth; final double percentageIncrease; - final double totalBalance; - final double totalChange24h; - final double percentageChange24h; @override List get props => [ portfolioGrowth, percentageIncrease, selectedPeriod, - totalBalance, - totalChange24h, - percentageChange24h, ]; } diff --git a/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart b/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart index d502b9d18a..f2ebc0b077 100644 --- a/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart +++ b/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart @@ -23,8 +23,7 @@ enum PriceChartPeriod { } } - // TODO: Localize this - String get formatted { + String get intervalString { switch (this) { case PriceChartPeriod.oneHour: return '1h'; diff --git a/lib/bloc/cex_market_data/price_chart/models/time_period.dart b/lib/bloc/cex_market_data/price_chart/models/time_period.dart index 2dc7500435..1dc84f0d64 100644 --- a/lib/bloc/cex_market_data/price_chart/models/time_period.dart +++ b/lib/bloc/cex_market_data/price_chart/models/time_period.dart @@ -23,9 +23,6 @@ enum TimePeriod { } } - // TODO: Localize - String formatted() => name; - Duration get duration { switch (this) { case TimePeriod.oneHour: diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 7ea0ce8a77..b25561791e 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -658,11 +658,4 @@ class CoinsRepo { result: withdrawDetails, ); } - - /// Get a cached price for a given coin symbol - /// - /// This returns the price from the cache without fetching new data - CexPrice? getCachedPrice(String symbol) { - return _pricesCache[symbol]; - } } diff --git a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart index 28394cfe31..8eb1614615 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart @@ -1,12 +1,10 @@ import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; -import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/shared/utils/password.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -20,24 +18,18 @@ import 'package:web_dex/model/kdf_auth_metadata_extension.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/utils.dart'; -import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart' - show PrivateKeyPolicy; part 'trezor_init_event.dart'; part 'trezor_init_state.dart'; -const String _trezorPasswordKey = 'trezor_wallet_password'; - class TrezorInitBloc extends Bloc { TrezorInitBloc({ required KomodoDefiSdk kdfSdk, required TrezorRepo trezorRepo, required CoinsRepo coinsRepository, - FlutterSecureStorage? secureStorage, }) : _trezorRepo = trezorRepo, _kdfSdk = kdfSdk, _coinsRepository = coinsRepository, - _secureStorage = secureStorage ?? const FlutterSecureStorage(), super(TrezorInitState.initial()) { on(_onSubscribeStatus); on(_onInit); @@ -57,7 +49,6 @@ class TrezorInitBloc extends Bloc { final TrezorRepo _trezorRepo; final KomodoDefiSdk _kdfSdk; final CoinsRepo _coinsRepository; - final FlutterSecureStorage _secureStorage; Timer? _statusTimer; void _unsubscribeStatus() { @@ -282,65 +273,35 @@ class TrezorInitBloc extends Bloc { /// into a static 'hidden' wallet to init trezor Future _loginToTrezorWallet({ String walletName = 'My Trezor', - String? password, - AuthOptions authOptions = const AuthOptions( - derivationMethod: DerivationMethod.hdWallet, - privKeyPolicy: PrivateKeyPolicy.trezor, - ), + String password = 'hidden-login', }) async { - try { - password ??= await _secureStorage.read(key: _trezorPasswordKey); - } catch (e, s) { - log( - 'Failed to read trezor password from secure storage: $e', - path: 'trezor_init_bloc => _loginToTrezorWallet', - isError: true, - trace: s, - ).ignore(); - // If reading fails, password will remain null and a new one will be generated - } - - if (password == null) { - password = generatePassword(); - try { - await _secureStorage.write(key: _trezorPasswordKey, value: password); - } catch (e, s) { - log( - 'Failed to write trezor password to secure storage: $e', - path: 'trezor_init_bloc => _loginToTrezorWallet', - isError: true, - trace: s, - ).ignore(); - // Continue with generated password even if storage write fails - } - } - final bool mm2SignedIn = await _kdfSdk.auth.isSignedIn(); if (state.kdfUser != null && mm2SignedIn) { return; } + // final walletName = state.status?.trezorStatus.name ?? 'My Trezor'; + // final password = + // state.status?.details.deviceDetails?.deviceId ?? 'hidden-login'; final existingWallets = await _kdfSdk.auth.getUsers(); if (existingWallets.any((wallet) => wallet.walletId.name == walletName)) { await _kdfSdk.auth.signIn( walletName: walletName, password: password, - options: authOptions, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), ); await _kdfSdk.setWalletType(WalletType.trezor); await _kdfSdk.confirmSeedBackup(); - await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins); return; } await _kdfSdk.auth.register( walletName: walletName, password: password, - options: authOptions, + options: const AuthOptions(derivationMethod: DerivationMethod.iguana), ); await _kdfSdk.setWalletType(WalletType.trezor); await _kdfSdk.confirmSeedBackup(); - await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins); } Future _logout() async { diff --git a/lib/model/cex_price.dart b/lib/model/cex_price.dart index be9f247628..ee2e6f9540 100644 --- a/lib/model/cex_price.dart +++ b/lib/model/cex_price.dart @@ -1,7 +1,12 @@ -import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' - as sdk_types; +import 'package:equatable/equatable.dart'; -typedef CexDataProvider = sdk_types.CexDataProvider; +enum CexDataProvider { + binance, + coingecko, + coinpaprika, + nomics, + unknown, +} CexDataProvider cexDataProvider(String string) { return CexDataProvider.values.firstWhere( @@ -9,4 +14,69 @@ CexDataProvider cexDataProvider(String string) { orElse: () => CexDataProvider.unknown); } -typedef CexPrice = sdk_types.CexPrice; +class CexPrice extends Equatable { + const CexPrice({ + required this.ticker, + required this.price, + this.lastUpdated, + this.priceProvider, + this.change24h, + this.changeProvider, + this.volume24h, + this.volumeProvider, + }); + + final String ticker; + final double price; + final DateTime? lastUpdated; + final CexDataProvider? priceProvider; + final double? volume24h; + final CexDataProvider? volumeProvider; + final double? change24h; + final CexDataProvider? changeProvider; + + @override + String toString() { + return 'CexPrice(ticker: $ticker, price: $price)'; + } + + factory CexPrice.fromJson(Map json) { + return CexPrice( + ticker: json['ticker'] as String, + price: (json['price'] as num).toDouble(), + lastUpdated: json['lastUpdated'] == null + ? null + : DateTime.parse(json['lastUpdated'] as String), + priceProvider: cexDataProvider(json['priceProvider'] as String), + volume24h: (json['volume24h'] as num?)?.toDouble(), + volumeProvider: cexDataProvider(json['volumeProvider'] as String), + change24h: (json['change24h'] as num?)?.toDouble(), + changeProvider: cexDataProvider(json['changeProvider'] as String), + ); + } + + Map toJson() { + return { + 'ticker': ticker, + 'price': price, + 'lastUpdated': lastUpdated?.toIso8601String(), + 'priceProvider': priceProvider?.toString(), + 'volume24h': volume24h, + 'volumeProvider': volumeProvider?.toString(), + 'change24h': change24h, + 'changeProvider': changeProvider?.toString(), + }; + } + + @override + List get props => [ + ticker, + price, + lastUpdated, + priceProvider, + volume24h, + volumeProvider, + change24h, + changeProvider, + ]; +} diff --git a/lib/model/coin.dart b/lib/model/coin.dart index 44e5dc347e..77191794b6 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -128,6 +128,7 @@ class Coin { bool get hasTrezorSupport { if (excludedAssetListTrezor.contains(abbr)) return false; + if (checkSegwitByAbbr(abbr)) return false; if (type == CoinType.utxo) return true; if (type == CoinType.smartChain) return true; diff --git a/lib/shared/utils/password.dart b/lib/shared/utils/password.dart index 1230319fac..1c9f7b1df2 100644 --- a/lib/shared/utils/password.dart +++ b/lib/shared/utils/password.dart @@ -1,9 +1,84 @@ -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'dart:math'; -/// Generates a password that meets the KDF password policy requirements using -/// the device's secure random number generator. -String generatePassword() => SecurityUtils.generatePasswordSecure(16); +String generatePassword() { + final List passwords = []; + + final rng = Random.secure(); + + const String lowerCase = 'abcdefghijklmnopqrstuvwxyz'; + const String upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const String digit = '0123456789'; + const String punctuation = '*.!@#\$%^(){}:;\',.?/~`_+\\-=|'; + + final string = [lowerCase, upperCase, digit, punctuation]; + + final length = rng.nextInt(24) + 8; + + final List tab = []; + + while (true) { + // This loop make sure the new RPC password will contains all the requirement + // characters type in password, it generate automatically the position. + tab.clear(); + for (var x = 0; x < length; x++) { + tab.add(string[rng.nextInt(4)]); + } + + if (tab.contains(lowerCase) && + tab.contains(upperCase) && + tab.contains(digit) && + tab.contains(punctuation)) { + break; + } + } + + for (int i = 0; i < tab.length; i++) { + // Here we constitute new RPC password, and check the repetition. + final chars = tab[i]; + final character = chars[rng.nextInt(chars.length)]; + final count = passwords.where((c) => c == character).toList().length; + if (count < 2) { + passwords.add(character); + } else { + tab.add(chars); + } + } + + return passwords.join(''); +} /// unit tests: [testValidateRPCPassword] -bool validateRPCPassword(String src) => - SecurityUtils.checkPasswordRequirements(src).isValid; +bool validateRPCPassword(String src) { + if (src.isEmpty) return false; + + // Password can't contain word 'password' + if (src.toLowerCase().contains('password')) return false; + + // Password must contain one digit, one lowercase letter, one uppercase letter, + // one special character and its length must be between 8 and 32 characters + final RegExp exp = RegExp( + r'^(?:(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[^A-Za-z0-9])).{8,32}$', + ); + if (!src.contains(exp)) return false; + + // Password can't contain same character three time in a row, + // so some code below to check that: + + // MRC: Divide the password into all possible 3 character blocks + final pieces = []; + for (int start = 0, end = 3; end <= src.length; start += 1, end += 1) { + pieces.add(src.substring(start, end)); + } + + // If, for any block, all 3 character are the same, block doesn't fit criteria + for (String p in pieces) { + final src = p[0]; + int count = 1; + if (p[1] == src) count += 1; + if (p[2] == src) count += 1; + + if (count == 3) return false; + } + + return true; +} diff --git a/lib/shared/widgets/coin_balance.dart b/lib/shared/widgets/coin_balance.dart index 1376b171d9..cf97be4b24 100644 --- a/lib/shared/widgets/coin_balance.dart +++ b/lib/shared/widgets/coin_balance.dart @@ -50,7 +50,7 @@ class CoinBalance extends StatelessWidget { ), child: Row( children: [ - Text(' (', style: balanceStyle), + Text('(', style: balanceStyle), CoinFiatBalance( coin, isAutoScrollEnabled: true, diff --git a/lib/views/wallet/wallet_page/common/asset_list_item.dart b/lib/views/wallet/wallet_page/common/asset_list_item.dart index 8d64c02996..1a5a4c9d7d 100644 --- a/lib/views/wallet/wallet_page/common/asset_list_item.dart +++ b/lib/views/wallet/wallet_page/common/asset_list_item.dart @@ -14,14 +14,12 @@ class AssetListItem extends StatelessWidget { required this.backgroundColor, required this.onTap, this.isActivating = false, - this.priceChangePercentage24h, }); final AssetId assetId; final Color backgroundColor; final void Function(AssetId) onTap; final bool isActivating; - final double? priceChangePercentage24h; @override Widget build(BuildContext context) { diff --git a/lib/views/wallet/wallet_page/common/assets_list.dart b/lib/views/wallet/wallet_page/common/assets_list.dart index ba3c56585e..3d6b394285 100644 --- a/lib/views/wallet/wallet_page/common/assets_list.dart +++ b/lib/views/wallet/wallet_page/common/assets_list.dart @@ -47,7 +47,6 @@ class AssetsList extends StatelessWidget { assetId: asset, backgroundColor: backgroundColor, onTap: onAssetItemTap, - priceChangePercentage24h: priceChangePercentages[asset.id], ); }, childCount: filteredAssets.length, diff --git a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart index 54f892848e..b195288f9d 100644 --- a/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart +++ b/lib/views/wallet/wallet_page/common/expandable_coin_list_item.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; @@ -19,9 +18,6 @@ import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; import 'package:web_dex/views/wallet/common/wallet_helper.dart'; import 'package:get_it/get_it.dart'; -/// Widget for showing an authenticated user's balance and anddresses for a -/// given coin -// TODO: Refactor to `AssetId` and migrate to the SDK UI library. class ExpandableCoinListItem extends StatefulWidget { final Coin coin; final AssetPubkeys? pubkeys; @@ -77,17 +73,12 @@ class _ExpandableCoinListItemState extends State { ..sort((a, b) => b.balance.spendable.compareTo(a.balance.spendable))) : null; - final horizontalPadding = isMobile ? 16.0 : 18.0; - final verticalPadding = isMobile ? 12.0 : 12.0; - return CollapsibleCard( key: PageStorageKey('coin_${widget.coin.abbr}'), borderRadius: BorderRadius.circular(12), - headerPadding: EdgeInsets.symmetric( - horizontal: horizontalPadding, vertical: verticalPadding), + headerPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), onTap: widget.onTap, - childrenMargin: EdgeInsets.symmetric( - horizontal: horizontalPadding, vertical: verticalPadding), + childrenMargin: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), childrenDecoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(12), @@ -118,79 +109,6 @@ class _ExpandableCoinListItemState extends State { Widget _buildTitle(BuildContext context) { final theme = Theme.of(context); - if (isMobile) { - return _buildMobileTitle(context, theme); - } else { - return _buildDesktopTitle(context, theme); - } - } - - Widget _buildMobileTitle(BuildContext context, ThemeData theme) { - return Container( - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Top row: Asset info and additional info (like BEP-20) - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: CoinItem(coin: widget.coin, size: CoinItemSize.large), - ), - ], - ), - const SizedBox(height: 8), - // Bottom row: Balance and 24h change - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Balance', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 2), - CoinBalance(coin: widget.coin), - ], - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '24h %', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 2), - TrendPercentageText( - percentage: getTotal24Change( - [widget.coin], - context.sdk, - ) ?? - 0, - iconSize: 16, - spacing: 4, - textStyle: theme.textTheme.bodyMedium, - ), - ], - ), - ], - ), - ], - ), - ); - } - - Widget _buildDesktopTitle(BuildContext context, ThemeData theme) { return Container( alignment: Alignment.centerLeft, child: Row( @@ -245,62 +163,62 @@ class _AddressRow extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: ListTile( - onTap: onTap, - contentPadding: - const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - leading: CircleAvatar( - radius: 16, - backgroundColor: theme.colorScheme.surfaceContainerHigh, - child: const Icon(Icons.person_outline), - ), - title: Row( - children: [ - Text( - pubkey.addressShort, - style: theme.textTheme.bodyMedium, + return ListTile( + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + leading: CircleAvatar( + radius: 16, + backgroundColor: theme.colorScheme.surfaceContainerHigh, + child: const Icon(Icons.person_outline), + ), + title: Row( + children: [ + Text( + pubkey.addressShort, + style: theme.textTheme.bodyMedium, + ), + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: IconButton( + iconSize: 16, + icon: const Icon(Icons.copy), + onPressed: onCopy, + visualDensity: VisualDensity.compact, + // constraints: const BoxConstraints( + // minWidth: 32, + // minHeight: 32, + // ), ), + ), + if (isSwapAddress && !kIsWalletOnly) ...[ const SizedBox(width: 8), - Material( - color: Colors.transparent, - child: IconButton( - iconSize: 16, - icon: const Icon(Icons.copy), - onPressed: onCopy, - visualDensity: VisualDensity.compact, + const Chip( + label: Text( + 'Swap', + // style: theme.textTheme.labelSmall, ), + // backgroundColor: theme.colorScheme.primaryContainer, ), - if (isSwapAddress && !kIsWalletOnly) ...[ - const SizedBox(width: 8), - const Chip( - label: Text( - 'Swap', - // style: theme.textTheme.labelSmall, - ), - // backgroundColor: theme.colorScheme.primaryContainer, - ), - ], ], - ), - trailing: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '${doubleToString(pubkey.balance.spendable.toDouble())} ${coin.abbr}', - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), + ], + ), + trailing: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '${doubleToString(pubkey.balance.spendable.toDouble())} ${coin.abbr}', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, ), - CoinFiatBalance( - coin, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + ), + CoinFiatBalance( + coin, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, ), - ], - ), + ), + ], ), ); } diff --git a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart index 3d97275d31..e155ed0d98 100644 --- a/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/active_coins_list.dart @@ -56,29 +56,30 @@ class ActiveCoinsList extends StatelessWidget { sorted = removeTestCoins(sorted); } - return SliverList.builder( - itemCount: sorted.length, - itemBuilder: (context, index) { - final coin = sorted[index]; + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final coin = sorted[index]; - // Fetch pubkeys if not already loaded - if (!state.pubkeys.containsKey(coin.abbr)) { - // TODO: Investigate if this is causing performance issues - context.read().add(CoinsPubkeysRequested(coin.abbr)); - } + // Fetch pubkeys if not already loaded + if (!state.pubkeys.containsKey(coin.abbr)) { + context.read().add(CoinsPubkeysRequested(coin.abbr)); + } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: ExpandableCoinListItem( - // Changed from ExpandableCoinListItem - key: Key('coin-list-item-${coin.abbr.toLowerCase()}'), - coin: coin, - pubkeys: state.pubkeys[coin.abbr], - isSelected: false, - onTap: () => onCoinItemTap(coin), - ), - ); - }, + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: ExpandableCoinListItem( + // Changed from ExpandableCoinListItem + key: Key('coin-list-item-${coin.abbr.toLowerCase()}'), + coin: coin, + pubkeys: state.pubkeys[coin.abbr], + isSelected: false, + onTap: () => onCoinItemTap(coin), + ), + ); + }, + childCount: sorted.length, + ), ); }, ); diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart index 5d0aa79b08..e6e8ca52e6 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -29,9 +29,9 @@ import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; +import 'package:web_dex/bloc/analytics/analytics_event.dart'; import 'package:web_dex/analytics/events.dart'; -import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart'; -import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; import 'package:web_dex/views/wallet/wallet_page/common/assets_list.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/active_coins_list.dart'; @@ -70,7 +70,7 @@ class _WalletMainState extends State _loadWalletData(authBloc.state.currentUser!.wallet.id).ignore(); } - _tabController = TabController(length: 3, vsync: this); + _tabController = TabController(length: 2, vsync: this); } @override @@ -109,143 +109,51 @@ class _WalletMainState extends State header: isMobile ? PageHeader(title: LocaleKeys.wallet.tr()) : null, content: Expanded( - child: Column( - children: [ - if (authStateMode == AuthorizeMode.logIn) ...[ - WalletOverview( - key: const Key('wallet-overview'), - onPortfolioGrowthPressed: () => - _tabController.animateTo(1), - onPortfolioProfitLossPressed: () => - _tabController.animateTo(2), - onAssetsPressed: () => _tabController.animateTo(0), - ), - const Gap(8), - // Tab structure with charts and coins list using NestedScrollView - Expanded( - child: NestedScrollView( - headerSliverBuilder: - (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Card( - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: TabBar( - controller: _tabController, - tabs: [ - Tab(text: LocaleKeys.assets.tr()), - Tab( - text: LocaleKeys.portfolioGrowth - .tr()), - Tab( - text: - LocaleKeys.profitAndLoss.tr()), - ], - ), - ), - ), - ), - ]; - }, - body: TabBarView( - // // Clamp to horizontal scrolling - // physics: const NeverScrollableScrollPhysics(), - controller: _tabController, - children: [ - // Coins List Tab - CustomScrollView( - // physics: const ClampingScrollPhysics(), - key: const Key('wallet-page-scroll-view'), - controller: _scrollController, - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _SliverSearchBarDelegate( - withBalance: _showCoinWithBalance, - onSearchChange: _onSearchChange, - onWithBalanceChange: - _onShowCoinsWithBalanceClick, - mode: authStateMode, - ), - ), - SliverToBoxAdapter( - child: SizedBox(height: 8), - ), - CoinListView( - mode: authStateMode, - searchPhrase: _searchKey, - withBalance: _showCoinWithBalance, - onActiveCoinItemTap: _onActiveCoinItemTap, - onAssetItemTap: _onAssetItemTap, - ), - ], - ), - // Portfolio Growth Chart Tab - SingleChildScrollView( - child: Container( - width: double.infinity, - height: 340, - child: PortfolioGrowthChart( - initialCoins: walletCoinsFiltered, - ), - ), - ), - // Profit/Loss Chart Tab - SingleChildScrollView( - child: Container( - width: double.infinity, - height: 340, - child: PortfolioProfitLossChart( - initialCoins: walletCoinsFiltered, - ), - ), - ), - ], - ), - ), - ), - ] else ...[ - // For non-logged in users, show the price chart and coins list - const SizedBox( - width: double.infinity, - height: 340, - child: PriceChartPage(key: Key('price-chart')), - ), - const Gap(8), - Expanded( - child: CustomScrollView( - key: const Key('wallet-page-scroll-view'), - controller: _scrollController, - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _SliverSearchBarDelegate( - withBalance: _showCoinWithBalance, - onSearchChange: _onSearchChange, - onWithBalanceChange: - _onShowCoinsWithBalanceClick, - mode: authStateMode, - ), - ), - SliverToBoxAdapter( - child: SizedBox(height: 8), - ), - CoinListView( - mode: authStateMode, - searchPhrase: _searchKey, - withBalance: _showCoinWithBalance, - onActiveCoinItemTap: _onActiveCoinItemTap, - onAssetItemTap: _onAssetItemTap, + child: CustomScrollView( + key: const Key('wallet-page-scroll-view'), + controller: _scrollController, + slivers: [ + SliverToBoxAdapter( + child: Column( + children: [ + if (authStateMode == AuthorizeMode.logIn) ...[ + WalletOverview( + onPortfolioGrowthPressed: () => + _tabController.animateTo(0), + onPortfolioProfitLossPressed: () => + _tabController.animateTo(1), ), + const Gap(8), ], - ), + if (authStateMode != AuthorizeMode.logIn) + const SizedBox( + width: double.infinity, + height: 340, + child: PriceChartPage(key: Key('price-chart')), + ) + else + AnimatedPortfolioCharts( + key: const Key('animated_portfolio_charts'), + tabController: _tabController, + walletCoinsFiltered: walletCoinsFiltered, + ), + const Gap(8), + ], + ), + ), + SliverPersistentHeader( + pinned: true, + delegate: _SliverSearchBarDelegate( + withBalance: _showCoinWithBalance, + onSearchChange: _onSearchChange, + onWithBalanceChange: _onShowCoinsWithBalanceClick, + mode: authStateMode, ), - ], + ), + SliverToBoxAdapter( + child: SizedBox(height: 8), + ), + _buildCoinList(authStateMode), ], ), ), @@ -315,6 +223,39 @@ class _WalletMainState extends State assetOverviewBloc.add(const AssetOverviewClearRequested()); } + Widget _buildCoinList(AuthorizeMode mode) { + switch (mode) { + case AuthorizeMode.logIn: + return ActiveCoinsList( + searchPhrase: _searchKey, + withBalance: _showCoinWithBalance, + onCoinItemTap: _onActiveCoinItemTap, + ); + case AuthorizeMode.hiddenLogin: + case AuthorizeMode.noLogin: + return AssetsList( + useGroupedView: true, + assets: context + .read() + .state + .coins + .values + .map((coin) => coin.assetId) + .toList(), + withBalance: false, + searchPhrase: _searchKey, + onAssetItemTap: (assetId) => _onAssetItemTap( + context + .read() + .state + .coins + .values + .firstWhere((coin) => coin.assetId == assetId), + ), + ); + } + } + void _onShowCoinsWithBalanceClick(bool? value) { setState(() { _showCoinWithBalance = value ?? false; @@ -332,6 +273,11 @@ class _WalletMainState extends State routingState.walletState.action = coinsManagerRouteAction.none; } + void _onCoinItemTap(Coin coin) { + _popupDispatcher = _createPopupDispatcher(); + _popupDispatcher!.show(); + } + void _onAssetItemTap(Coin coin) { _popupDispatcher = _createPopupDispatcher(); _popupDispatcher!.show(); @@ -375,57 +321,6 @@ class _WalletMainState extends State } } -class CoinListView extends StatelessWidget { - const CoinListView({ - super.key, - required this.mode, - required this.searchPhrase, - required this.withBalance, - required this.onActiveCoinItemTap, - required this.onAssetItemTap, - }); - - final AuthorizeMode mode; - final String searchPhrase; - final bool withBalance; - final Function(Coin) onActiveCoinItemTap; - final Function(Coin) onAssetItemTap; - - @override - Widget build(BuildContext context) { - switch (mode) { - case AuthorizeMode.logIn: - return ActiveCoinsList( - searchPhrase: searchPhrase, - withBalance: withBalance, - onCoinItemTap: onActiveCoinItemTap, - ); - case AuthorizeMode.hiddenLogin: - case AuthorizeMode.noLogin: - return AssetsList( - useGroupedView: true, - assets: context - .read() - .state - .coins - .values - .map((coin) => coin.assetId) - .toList(), - withBalance: false, - searchPhrase: searchPhrase, - onAssetItemTap: (assetId) => onAssetItemTap( - context - .read() - .state - .coins - .values - .firstWhere((coin) => coin.assetId == assetId), - ), - ); - } - } -} - class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { _SliverSearchBarDelegate({ required this.withBalance, @@ -441,7 +336,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { @override final double minExtent = 132; @override - final double maxExtent = 155; + final double maxExtent = 132; @override Widget build( diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart index 3e0dcb1469..e97fcd975c 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart @@ -29,56 +29,57 @@ class WalletManageSection extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - clipBehavior: Clip.antiAlias, - color: Theme.of(context).colorScheme.surface, - margin: const EdgeInsets.all(0), - elevation: pinned ? 2 : 0, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: isMobile - ? _buildMobileSection(context) - : _buildDesktopSection(context)); + return isMobile + ? _buildMobileSection(context) + : _buildDesktopSection(context); } bool get isAuthenticated => mode == AuthorizeMode.logIn; Widget _buildDesktopSection(BuildContext context) { final ThemeData theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Flexible( - child: Container( - alignment: Alignment.centerLeft, - constraints: const BoxConstraints(maxWidth: 300), - child: WalletManagerSearchField(onChange: onSearchChange), - ), - ), - if (isAuthenticated ) ...[ - Spacer(), - CoinsWithBalanceCheckbox( - withBalance: withBalance, - onWithBalanceChange: onWithBalanceChange, - ), - SizedBox(width: 24), - UiPrimaryButton( - buttonKey: const Key('add-assets-button'), - onPressed: () => _onAddAssetsPress(context), - text: LocaleKeys.addAssets.tr(), - height: 36, - width: 147, - borderRadius: 10, - textStyle: theme.textTheme.bodySmall, + return Card( + clipBehavior: Clip.antiAlias, + color: theme.colorScheme.surface, + margin: const EdgeInsets.all(0), + elevation: pinned ? 2 : 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Flexible( + child: Container( + alignment: Alignment.centerLeft, + constraints: const BoxConstraints(maxWidth: 300), + child: WalletManagerSearchField(onChange: onSearchChange), + ), ), + if (isAuthenticated) ...[ + Spacer(), + CoinsWithBalanceCheckbox( + withBalance: withBalance, + onWithBalanceChange: onWithBalanceChange, + ), + SizedBox(width: 24), + UiPrimaryButton( + buttonKey: const Key('add-assets-button'), + onPressed: () => _onAddAssetsPress(context), + text: LocaleKeys.addAssets.tr(), + height: 36, + width: 147, + borderRadius: 10, + textStyle: theme.textTheme.bodySmall, + ), + ], ], - ], - ), - Spacer(), - CoinsListHeader(isAuth: mode == AuthorizeMode.logIn), - ], + ), + Spacer(), + CoinsListHeader(isAuth: mode == AuthorizeMode.logIn), + ], + ), ), ); } @@ -94,7 +95,7 @@ class WalletManageSection extends StatelessWidget { Row( children: [ Text( - 'Portfolio', + 'Currency', style: theme.textTheme.titleLarge, ), Spacer(), @@ -102,7 +103,7 @@ class WalletManageSection extends StatelessWidget { UiPrimaryButton( buttonKey: const Key('asset-management-button'), onPressed: () => _onAddAssetsPress(context), - text: 'Add assets', + text: 'Asset management', height: 36, width: 147, borderRadius: 10, @@ -129,7 +130,6 @@ class WalletManageSection extends StatelessWidget { ), ], ), - Spacer(), ], ), ); diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart index 2ed67e59cc..e68805f747 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -4,32 +4,21 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui/komodo_ui.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/assets_overview/bloc/asset_overview_bloc.dart'; -import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; -import 'package:web_dex/bloc/cex_market_data/price_chart/models/time_period.dart'; -import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/analytics/events/portfolio_events.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/utils/utils.dart'; -// TODO(@takenagain): Please clean up the widget structure and bloc usage for -// the wallet overview. It may be better to split this into a separate bloc -// instead of the changes we've made to the existing PortfolioGrowthBloc since -// that bloc is primarily focused on chart data. class WalletOverview extends StatefulWidget { const WalletOverview({ super.key, this.onPortfolioGrowthPressed, this.onPortfolioProfitLossPressed, - this.onAssetsPressed, }); final VoidCallback? onPortfolioGrowthPressed; final VoidCallback? onPortfolioProfitLossPressed; - final VoidCallback? onAssetsPressed; @override State createState() => _WalletOverviewState(); @@ -43,24 +32,15 @@ class _WalletOverviewState extends State { return BlocBuilder( builder: (context, state) { if (state.coins.isEmpty) return _buildSpinner(); + final portfolioAssetsOverviewBloc = context.watch(); final int assetCount = state.walletCoins.length; - - // Get the portfolio growth bloc to access balance and 24h change - final portfolioGrowthBloc = context.watch(); - final portfolioGrowthState = portfolioGrowthBloc.state; - - // Get total balance from the PortfolioGrowthBloc if available, otherwise calculate - final double totalBalance = - portfolioGrowthState is PortfolioGrowthChartLoadSuccess - ? portfolioGrowthState.totalBalance - : _getTotalBalance(state.walletCoins.values, context); - final stateWithData = portfolioAssetsOverviewBloc.state is PortfolioAssetsOverviewLoadSuccess ? portfolioAssetsOverviewBloc.state as PortfolioAssetsOverviewLoadSuccess : null; + if (!_logged && stateWithData != null) { context.read().logEvent( PortfolioViewedEventData( @@ -71,94 +51,52 @@ class _WalletOverviewState extends State { _logged = true; } - // Create the statistic cards - final List statisticCards = [ - StatisticCard( - key: const Key('overview-current-value'), - caption: Text(LocaleKeys.yourBalance.tr()), - value: totalBalance, - actionIcon: const Icon(Icons.copy), - onPressed: () { - final formattedValue = - NumberFormat.currency(symbol: '\$').format(totalBalance); - copyToClipBoard(context, formattedValue); - }, - footer: BlocBuilder( - builder: (context, state) { - final double totalChange = - state is PortfolioGrowthChartLoadSuccess - ? state.percentageChange24h - : 0.0; - - return Chip( - visualDensity: const VisualDensity(vertical: -4), - label: TrendPercentageText( - percentage: totalChange, - suffix: Text(TimePeriod.oneDay.formatted()), - precision: 2, + return Wrap( + runSpacing: 16, + children: [ + FractionallySizedBox( + widthFactor: isMobile ? 1 : 0.5, + child: StatisticCard( + key: const Key('overview-total-balance'), + caption: Text(LocaleKeys.allTimeInvestment.tr()), + value: stateWithData?.totalInvestment.value ?? 0, + actionIcon: const Icon(CustomIcons.fiatIconCircle), + onPressed: widget.onPortfolioGrowthPressed, + footer: Container( + height: 28, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(28), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.pie_chart, + size: 16, + ), + const SizedBox(width: 4), + Text('$assetCount ${LocaleKeys.assets.tr()}'), + ], ), - ); - }, - ), - ), - StatisticCard( - key: const Key('overview-all-time-investment'), - caption: Text(LocaleKeys.allTimeInvestment.tr()), - value: stateWithData?.totalInvestment.value ?? 0, - actionIcon: const Icon(CustomIcons.fiatIconCircle), - onPressed: widget.onPortfolioGrowthPressed, - footer: ActionChip( - avatar: Icon( - Icons.pie_chart, - ), - onPressed: widget.onAssetsPressed, - visualDensity: const VisualDensity(vertical: -4), - label: Text( - LocaleKeys.assetNumber.plural(assetCount), - style: Theme.of(context).textTheme.bodyLarge, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + ), ), ), - ), - StatisticCard( - key: const Key('overview-all-time-profit'), - caption: Text(LocaleKeys.allTimeProfit.tr()), - value: stateWithData?.profitAmount.value ?? 0, - footer: Chip( - visualDensity: const VisualDensity(vertical: -4), - label: TrendPercentageText( - percentage: stateWithData?.profitIncreasePercentage ?? 0, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + FractionallySizedBox( + widthFactor: isMobile ? 1 : 0.5, + child: StatisticCard( + caption: Text(LocaleKeys.allTimeProfit.tr()), + value: stateWithData?.profitAmount.value ?? 0, + footer: TrendPercentageText( + percentage: stateWithData?.profitIncreasePercentage ?? 0, + ), + actionIcon: const Icon(Icons.trending_up), + onPressed: widget.onPortfolioProfitLossPressed, ), ), - actionIcon: const Icon(Icons.trending_up), - onPressed: widget.onPortfolioProfitLossPressed, - ), - ]; - - // Use carousel for mobile and wrap for desktop - // TODO: `Wrap` is currently redundant. Enforce a minimum width for - // the cards instead. - if (isMobile) { - return StatisticsCarousel(cards: statisticCards); - } else { - return Wrap( - runSpacing: 16, - children: statisticCards.map((card) { - return FractionallySizedBox( - widthFactor: 1 / 3, - child: card, - ); - }).toList(), - ); - } + ], + ); }, ); } @@ -174,98 +112,4 @@ class _WalletOverviewState extends State { ], ); } - - // TODO: Migrate these values to a new/existing bloc e.g. PortfolioGrowthBloc - double _getTotalBalance(Iterable coins, BuildContext context) { - double total = coins.fold( - 0, (prev, coin) => prev + (coin.usdBalance(context.sdk) ?? 0)); - - if (total > 0.01) { - return total; - } - - return total != 0 ? 0.01 : 0; - } -} - -/// A carousel widget that displays statistics cards with page indicators -class StatisticsCarousel extends StatefulWidget { - final List cards; - - const StatisticsCarousel({ - super.key, - required this.cards, - }); - - @override - State createState() => _StatisticsCarouselState(); -} - -// TODO: Refactor into a generic card carousel widget and move to `komodo_ui` -class _StatisticsCarouselState extends State { - final PageController _pageController = PageController(); - int _currentPage = 0; - - @override - void initState() { - super.initState(); - _pageController.addListener(() { - int next = _pageController.page?.round() ?? 0; - if (_currentPage != next) { - setState(() { - _currentPage = next; - }); - } - }); - } - - @override - void dispose() { - _pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - SizedBox( - height: 160, - child: PageView.builder( - controller: _pageController, - itemCount: widget.cards.length, - physics: const ClampingScrollPhysics(), - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: widget.cards[index], - ); - }, - ), - ), - const SizedBox(height: 16), - // Page indicators - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - widget.cards.length, - (index) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: 8, - width: _currentPage == index ? 24 : 8, - decoration: BoxDecoration( - color: _currentPage == index - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - ), - ], - ); - } } diff --git a/packages/komodo_ui_kit/lib/src/display/statistic_card.dart b/packages/komodo_ui_kit/lib/src/display/statistic_card.dart index 5a09371c12..8146b205a2 100644 --- a/packages/komodo_ui_kit/lib/src/display/statistic_card.dart +++ b/packages/komodo_ui_kit/lib/src/display/statistic_card.dart @@ -93,7 +93,7 @@ class StatisticCard extends StatelessWidget { child: IconButton.filledTonal( isSelected: false, icon: actionIcon, - iconSize: 36, + iconSize: 42, onPressed: onPressed, ), ), diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index 23b4fe6067..e8ac3a0b35 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -95,7 +95,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -104,7 +104,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -113,7 +113,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -145,10 +145,10 @@ packages: dependency: transitive description: name: mobile_scanner - sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.0.7" path: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index a2f28f578e..a60797aa1b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,10 +337,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.0" flutter_driver: dependency: transitive description: flutter @@ -586,10 +586,10 @@ packages: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" http_multi_server: dependency: transitive description: @@ -648,7 +648,7 @@ packages: description: path: "packages/komodo_cex_market_data" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -657,7 +657,7 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -666,7 +666,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0" @@ -675,7 +675,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -684,7 +684,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -693,7 +693,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -702,7 +702,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -718,7 +718,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -734,7 +734,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: f63bebb0288db26f2a369579109e1fcc93e19b67 + resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -862,10 +862,10 @@ packages: dependency: transitive description: name: mobile_scanner - sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.0.7" mutex: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 456667bb7b..3bce361b41 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.9.1+0 +version: 0.9.0+0 environment: # TODO: Upgrade mininum Dart version to 3.7.0 only after the release is concluded because @@ -58,7 +58,7 @@ dependencies: ## ---- Dart.dev, Flutter.dev args: ^2.7.0 # dart.dev flutter_markdown: ^0.7.7 # flutter.dev - http: 1.4.0 # dart.dev + http: 1.3.0 # dart.dev intl: 0.20.2 # dart.dev js: ">=0.6.7 <=0.7.2" # dart.dev url_launcher: 6.3.1 # flutter.dev