diff --git a/CHANGELOG.md b/CHANGELOG.md index f8411cea1c..55b389d788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 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/app_theme/lib/src/dark/theme_global_dark.dart b/app_theme/lib/src/dark/theme_global_dark.dart index 2d66380496..f12b56550a 100644 --- a/app_theme/lib/src/dark/theme_global_dark.dart +++ b/app_theme/lib/src/dark/theme_global_dark.dart @@ -70,7 +70,7 @@ ThemeData get themeGlobalDark { fontFamily: 'Manrope', scaffoldBackgroundColor: colorScheme.onSurface, cardColor: colorScheme.surface, - cardTheme: CardTheme( + cardTheme: CardThemeData( color: colorScheme.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(18)), @@ -83,7 +83,7 @@ ThemeData get themeGlobalDark { iconTheme: IconThemeData(color: colorScheme.primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: colorScheme.primary), - dialogTheme: const DialogTheme( + dialogTheme: const DialogThemeData( backgroundColor: Color.fromRGBO(14, 16, 27, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -144,7 +144,7 @@ ThemeData get themeGlobalDark { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), ), ), - tabBarTheme: TabBarTheme( + tabBarTheme: TabBarThemeData( labelColor: textColor, indicator: UnderlineTabIndicator( borderSide: BorderSide( diff --git a/app_theme/lib/src/light/theme_global_light.dart b/app_theme/lib/src/light/theme_global_light.dart index e4d476ad07..c910fb52e1 100644 --- a/app_theme/lib/src/light/theme_global_light.dart +++ b/app_theme/lib/src/light/theme_global_light.dart @@ -54,7 +54,7 @@ ThemeData get themeGlobalLight { fontFamily: 'Manrope', scaffoldBackgroundColor: colorScheme.onSurface, cardColor: colorScheme.surface, - cardTheme: CardTheme( + cardTheme: CardThemeData( color: colorScheme.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(18)), @@ -67,7 +67,7 @@ ThemeData get themeGlobalLight { iconTheme: IconThemeData(color: colorScheme.primary), progressIndicatorTheme: ProgressIndicatorThemeData(color: colorScheme.primary), - dialogTheme: const DialogTheme( + dialogTheme: const DialogThemeData( backgroundColor: Color.fromRGBO(255, 255, 255, 1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( @@ -152,7 +152,7 @@ ThemeData get themeGlobalLight { padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), ), ), - tabBarTheme: TabBarTheme( + tabBarTheme: TabBarThemeData( labelColor: textColor, indicator: UnderlineTabIndicator( borderSide: BorderSide( diff --git a/assets/translations/en.json b/assets/translations/en.json index 2ee49fef6c..5d1a046a8a 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -270,6 +270,8 @@ "cancelAll": "Cancel all", "type": "Type", "sell": "Sell", + "sellCrypto": "Sell Crypto", + "sellCryptoDescription": "Select a cryptocurrency to sell through our integrated Bitrefill service.", "buy": "Buy", "changingWalletPassword": "Changing your wallet password", "changingWalletPasswordDescription": "This password will be used for logging in to your wallet", @@ -639,7 +641,7 @@ "decimals": "Decimals", "onlySendToThisAddress": "Only send {} to this address", "scanTheQrCode": "Scan the QR code on any mobile device wallet", - "swapAddress": "Swap Address", + "tradingAddress": "Trading Address", "addresses": "Addresses", "creating": "Creating", "createAddress": "Create Address", @@ -671,5 +673,7 @@ "previewWithdrawal": "Preview Withdrawal", "createNewAddress": "Create New Address", "searchAddresses": "Search addresses", - "chart": "Chart" + "chart": "Chart", + "tradingDisabledTooltip": "Trading features are currently disabled", + "tradingDisabled": "Trading is currently unavailable" } \ No newline at end of file diff --git a/docs/FLUTTER_VERSION.md b/docs/FLUTTER_VERSION.md index ef592a7f7b..b3212c98b1 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` (latest stable release). We aim to keep the project up-to-date with the most recent stable Flutter versions. +This project supports Flutter `3.29.2`. We aim to keep the project up-to-date with the latest `stable` Flutter release. ## Recommended Approach: Multiple Flutter Versions diff --git a/ios/Flutter/ephemeral/flutter_lldb_helper.py b/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000000..a88caf99df --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/ios/Flutter/ephemeral/flutter_lldbinit b/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000000..e3ba6fbedc --- /dev/null +++ b/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 8c1d5c5161..d53aee3d1c 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -19,18 +19,18 @@ final Uri discordSupportChannelUrl = Uri.parse( 'https://discord.com/channels/412898016371015680/429676282196787200'); final Uri discordInviteUrl = Uri.parse('https://komodoplatform.com/discord'); -// Temporary feature flag to allow merging of the PR -// TODO: Remove this flag after the feature is finalized +/// Const to define if Bitrefill integration is enabled in the app. const bool isBitrefillIntegrationEnabled = false; -/// Const to define if trading is enabled in the app. Trading is only permitted -/// with test coins for development purposes while the regulatory compliance -/// framework is being developed. +/// Const to define whether to show trading warning dialogs and notices. +/// This can be used to control the display of trading-related warnings +/// throughout the application. /// -///! You are solely responsible for any losses/damage that may occur. Komodo -///! Platform does not condone the use of this app for trading purposes and -///! unequivocally forbids it. -const bool kIsWalletOnly = !kDebugMode; +///! You are solely responsible for any losses/damage that may occur due to +///! compliance issues, bugs, or other unforeseen circumstances. Komodo +///! Platform and its legal entities do not condone the use of this app for +///! trading purposes where it is not legally compliant. +const bool kShowTradingWarning = false; const Duration kPerformanceLogInterval = Duration(minutes: 1); diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index aecdb79438..e855d48209 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -45,7 +45,9 @@ import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; import 'package:web_dex/bloc/system_health/system_clock_repository.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_repository.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/bloc/trezor_bloc/trezor_repo.dart'; @@ -177,6 +179,7 @@ class AppBlocRoot extends StatelessWidget { RepositoryProvider( create: (_) => KmdRewardsBloc(coinsRepository, mm2Api), ), + RepositoryProvider(create: (_) => TradingStatusRepository()), ], child: MultiBlocProvider( providers: [ @@ -194,7 +197,7 @@ class AppBlocRoot extends StatelessWidget { komodoDefiSdk, )..add( const PriceChartStarted( - symbols: ['KMD'], + symbols: ['BTC'], period: Duration(days: 30), ), ), @@ -287,6 +290,12 @@ class AppBlocRoot extends StatelessWidget { ), ), ), + BlocProvider( + lazy: false, + create: (context) => TradingStatusBloc( + context.read(), + )..add(TradingStatusCheckRequested()), + ), BlocProvider( create: (_) => SystemHealthBloc(SystemClockRepository(), mm2Api) ..add(SystemHealthPeriodicCheckStarted()), diff --git a/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart b/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart index bcfbae80b6..df75c0dce2 100644 --- a/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart +++ b/lib/bloc/bitrefill/bloc/bitrefill_bloc.dart @@ -28,7 +28,7 @@ class BitrefillBloc extends Bloc { emit(const BitrefillLoadInProgress()); final String url = _bitrefillRepository.embeddedBitrefillUrl( coinAbbr: event.coin?.abbr, - refundAddress: event.coin?.address, + refundAddress: event.refundAddress ?? event.coin?.address, ); final List supportedCoins = diff --git a/lib/bloc/bitrefill/bloc/bitrefill_event.dart b/lib/bloc/bitrefill/bloc/bitrefill_event.dart index e8f0e9d13a..4776ad2350 100644 --- a/lib/bloc/bitrefill/bloc/bitrefill_event.dart +++ b/lib/bloc/bitrefill/bloc/bitrefill_event.dart @@ -10,12 +10,13 @@ sealed class BitrefillEvent extends Equatable { /// Request to load the bitrefill state with the url and supported coins /// from the bitrefill provider. final class BitrefillLoadRequested extends BitrefillEvent { - const BitrefillLoadRequested({this.coin}); + const BitrefillLoadRequested({this.coin, this.refundAddress}); final Coin? coin; + final String? refundAddress; @override - List get props => []; + List get props => [coin?.abbr ?? '', refundAddress ?? '']; } /// Request to open the Bitrefill widget to make a purchase 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 08ab9cf077..fe286f2e51 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,6 +11,7 @@ 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'; @@ -120,10 +121,13 @@ class PortfolioGrowthBloc await emit.forEach( // computation is omitted, so null-valued events are emitted on a set // interval. - Stream.periodic(event.updateFrequency) - .asyncMap((_) async => _fetchPortfolioGrowthChart(event)), + Stream.periodic(event.updateFrequency).asyncMap((_) async { + // Update prices before fetching chart data + await portfolioGrowthRepository.updatePrices(); + return _fetchPortfolioGrowthChart(event); + }), onData: (data) => - _handlePortfolioGrowthUpdate(data, event.selectedPeriod), + _handlePortfolioGrowthUpdate(data, event.selectedPeriod, event.coins), onError: (error, stackTrace) { _log.shout('Failed to load portfolio growth', error, stackTrace); return GrowthChartLoadFailure( @@ -164,10 +168,21 @@ 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, ); } @@ -205,20 +220,71 @@ class PortfolioGrowthBloc PortfolioGrowthState _handlePortfolioGrowthUpdate( ChartData growthChart, Duration selectedPeriod, + List coins, ) { if (growthChart.isEmpty && state is PortfolioGrowthChartLoadSuccess) { return state; } final percentageIncrease = growthChart.percentageIncrease; - - // TODO? Include the center value in the bloc state instead of - // calculating it in the UI + final totalBalance = _calculateTotalBalance(coins); + final totalChange24h = _calculateTotalChange24h(coins); + final percentageChange24h = _calculatePercentageChange24h(coins); 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 d369b95dab..63398b5f2d 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,9 +71,12 @@ class PortfolioGrowthRepository { /// The graph cache provider to store the portfolio growth graph data. final PersistenceProvider _graphCache; - final CoinsRepo _coinsRepository; + /// The SDK needed for connecting to blockchain nodes final KomodoDefiSdk _sdk; + /// The coins repository for detailed coin info + final CoinsRepo _coinsRepository; + final _log = Logger('PortfolioGrowthRepository'); static Future ensureInitialized() async { @@ -514,4 +517,38 @@ 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 bd53543cb4..a964bbca16 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,16 +19,25 @@ 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 f2ebc0b077..d502b9d18a 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,7 +23,8 @@ enum PriceChartPeriod { } } - String get intervalString { + // TODO: Localize this + String get formatted { 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 1dc84f0d64..2dc7500435 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,6 +23,9 @@ 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 b25561791e..7ea0ce8a77 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -658,4 +658,11 @@ 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/trading_status/trading_status_bloc.dart b/lib/bloc/trading_status/trading_status_bloc.dart new file mode 100644 index 0000000000..9dcf613459 --- /dev/null +++ b/lib/bloc/trading_status/trading_status_bloc.dart @@ -0,0 +1,32 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'trading_status_repository.dart'; + +part 'trading_status_event.dart'; +part 'trading_status_state.dart'; + +class TradingStatusBloc extends Bloc { + TradingStatusBloc(this._repository) : super(TradingStatusInitial()) { + on(_onCheckRequested); + } + + final TradingStatusRepository _repository; + + // TODO (@takenagain): Retry periodically if the failure was caused by a + // network issue. + Future _onCheckRequested( + TradingStatusCheckRequested event, + Emitter emit, + ) async { + emit(TradingStatusLoadInProgress()); + try { + final enabled = await _repository.isTradingEnabled(); + emit(enabled ? TradingEnabled() : TradingDisabled()); + + // This catch will never be triggered by the repository. This will require + // changes to meet the "TODO" above. + } catch (_) { + emit(TradingStatusLoadFailure()); + } + } +} diff --git a/lib/bloc/trading_status/trading_status_event.dart b/lib/bloc/trading_status/trading_status_event.dart new file mode 100644 index 0000000000..77fdfd8971 --- /dev/null +++ b/lib/bloc/trading_status/trading_status_event.dart @@ -0,0 +1,8 @@ +part of 'trading_status_bloc.dart'; + +abstract class TradingStatusEvent extends Equatable { + @override + List get props => []; +} + +class TradingStatusCheckRequested extends TradingStatusEvent {} diff --git a/lib/bloc/trading_status/trading_status_repository.dart b/lib/bloc/trading_status/trading_status_repository.dart new file mode 100644 index 0000000000..c3eafa5871 --- /dev/null +++ b/lib/bloc/trading_status/trading_status_repository.dart @@ -0,0 +1,32 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; + +class TradingStatusRepository { + TradingStatusRepository({http.Client? httpClient, Duration? timeout}) + : _httpClient = httpClient ?? http.Client(), + _timeout = timeout ?? const Duration(seconds: 10); + + final http.Client _httpClient; + final Duration _timeout; + + Future isTradingEnabled({bool? forceFail}) async { + try { + final uri = Uri.parse( + (forceFail ?? false) + ? 'https://defi-stats.komodo.earth/api/v3/utils/blacklist' + : 'https://defi-stats.komodo.earth/api/v3/utils/bouncer', + ); + final res = await _httpClient.get(uri).timeout(_timeout); + return res.statusCode == 200; + } catch (_) { + debugPrint('Network error: Trading status check failed'); + // Block trading features on network failure + return false; + } + } + + void dispose() { + _httpClient.close(); + } +} diff --git a/lib/bloc/trading_status/trading_status_state.dart b/lib/bloc/trading_status/trading_status_state.dart new file mode 100644 index 0000000000..03a85be8a1 --- /dev/null +++ b/lib/bloc/trading_status/trading_status_state.dart @@ -0,0 +1,18 @@ +part of 'trading_status_bloc.dart'; + +abstract class TradingStatusState extends Equatable { + @override + List get props => []; + + bool get isEnabled => this is TradingEnabled; +} + +class TradingStatusInitial extends TradingStatusState {} + +class TradingStatusLoadInProgress extends TradingStatusState {} + +class TradingEnabled extends TradingStatusState {} + +class TradingDisabled extends TradingStatusState {} + +class TradingStatusLoadFailure extends TradingStatusState {} diff --git a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart index 8eb1614615..28394cfe31 100644 --- a/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart +++ b/lib/bloc/trezor_init_bloc/trezor_init_bloc.dart @@ -1,10 +1,12 @@ 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'; @@ -18,18 +20,24 @@ 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); @@ -49,6 +57,7 @@ class TrezorInitBloc extends Bloc { final TrezorRepo _trezorRepo; final KomodoDefiSdk _kdfSdk; final CoinsRepo _coinsRepository; + final FlutterSecureStorage _secureStorage; Timer? _statusTimer; void _unsubscribeStatus() { @@ -273,35 +282,65 @@ class TrezorInitBloc extends Bloc { /// into a static 'hidden' wallet to init trezor Future _loginToTrezorWallet({ String walletName = 'My Trezor', - String password = 'hidden-login', + String? password, + AuthOptions authOptions = const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor, + ), }) 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: const AuthOptions(derivationMethod: DerivationMethod.iguana), + options: authOptions, ); await _kdfSdk.setWalletType(WalletType.trezor); await _kdfSdk.confirmSeedBackup(); + await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins); return; } await _kdfSdk.auth.register( walletName: walletName, password: password, - options: const AuthOptions(derivationMethod: DerivationMethod.iguana), + options: authOptions, ); await _kdfSdk.setWalletType(WalletType.trezor); await _kdfSdk.confirmSeedBackup(); + await _kdfSdk.addActivatedCoins(enabledByDefaultTrezorCoins); } Future _logout() async { diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 5243588ef2..1227304b6e 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -267,6 +267,8 @@ abstract class LocaleKeys { static const cancelAll = 'cancelAll'; static const type = 'type'; static const sell = 'sell'; + static const sellCrypto = 'sellCrypto'; + static const sellCryptoDescription = 'sellCryptoDescription'; static const buy = 'buy'; static const changingWalletPassword = 'changingWalletPassword'; static const changingWalletPasswordDescription = 'changingWalletPasswordDescription'; @@ -636,7 +638,7 @@ abstract class LocaleKeys { static const decimals = 'decimals'; static const onlySendToThisAddress = 'onlySendToThisAddress'; static const scanTheQrCode = 'scanTheQrCode'; - static const swapAddress = 'swapAddress'; + static const tradingAddress = 'tradingAddress'; static const addresses = 'addresses'; static const creating = 'creating'; static const createAddress = 'createAddress'; @@ -669,5 +671,7 @@ abstract class LocaleKeys { static const createNewAddress = 'createNewAddress'; static const searchAddresses = 'searchAddresses'; static const chart = 'chart'; + static const tradingDisabledTooltip = 'tradingDisabledTooltip'; + static const tradingDisabled = 'tradingDisabled'; } diff --git a/lib/model/cex_price.dart b/lib/model/cex_price.dart index ee2e6f9540..be9f247628 100644 --- a/lib/model/cex_price.dart +++ b/lib/model/cex_price.dart @@ -1,12 +1,7 @@ -import 'package:equatable/equatable.dart'; +import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' + as sdk_types; -enum CexDataProvider { - binance, - coingecko, - coinpaprika, - nomics, - unknown, -} +typedef CexDataProvider = sdk_types.CexDataProvider; CexDataProvider cexDataProvider(String string) { return CexDataProvider.values.firstWhere( @@ -14,69 +9,4 @@ CexDataProvider cexDataProvider(String string) { orElse: () => CexDataProvider.unknown); } -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, - ]; -} +typedef CexPrice = sdk_types.CexPrice; diff --git a/lib/model/coin.dart b/lib/model/coin.dart index 77191794b6..44e5dc347e 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -128,7 +128,6 @@ 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/model/main_menu_value.dart b/lib/model/main_menu_value.dart index 0132b5c605..b6a234130e 100644 --- a/lib/model/main_menu_value.dart +++ b/lib/model/main_menu_value.dart @@ -1,5 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; enum MainMenuValue { @@ -13,11 +13,10 @@ enum MainMenuValue { support, none; - static MainMenuValue defaultMenu() => - kIsWalletOnly ? MainMenuValue.wallet : MainMenuValue.dex; + static MainMenuValue defaultMenu() => MainMenuValue.dex; - bool isEnabledInCurrentMode() { - return !(kIsWalletOnly && isDisabledWhenWalletOnly); + bool isEnabledInCurrentMode({required bool tradingEnabled}) { + return tradingEnabled || !isDisabledWhenWalletOnly; } // Getter to determine if the item is disabled if the wallet is in wallet-only mode diff --git a/lib/router/navigators/page_content/page_content_router_delegate.dart b/lib/router/navigators/page_content/page_content_router_delegate.dart index 5f1fa6a5c9..9e50ab1a35 100644 --- a/lib/router/navigators/page_content/page_content_router_delegate.dart +++ b/lib/router/navigators/page_content/page_content_router_delegate.dart @@ -19,15 +19,6 @@ class PageContentRouterDelegate extends RouterDelegate @override Widget build(BuildContext context) { - // Redirect to the Wallet page if the selected menu is disabled in - // wallet-only mode and the wallet is in wallet-only mode. - if (routingState.selectedMenu.isDisabledWhenWalletOnly && kIsWalletOnly) { - return WalletPage( - coinAbbr: routingState.walletState.selectedCoin, - action: routingState.walletState.coinsManagerAction, - ); - } - switch (routingState.selectedMenu) { case MainMenuValue.fiat: return const FiatPage(); diff --git a/lib/router/parsers/root_route_parser.dart b/lib/router/parsers/root_route_parser.dart index f46246480e..e27fe39574 100644 --- a/lib/router/parsers/root_route_parser.dart +++ b/lib/router/parsers/root_route_parser.dart @@ -40,8 +40,7 @@ class RootRouteInformationParser extends RouteInformationParser { } BaseRouteParser _getRoutParser(Uri uri) { - final defaultRouteParser = - kIsWalletOnly ? _parsers[firstUriSegment.wallet]! : dexRouteParser; + final defaultRouteParser = dexRouteParser; if (uri.pathSegments.isEmpty) return defaultRouteParser; return _parsers[uri.pathSegments.first] ?? defaultRouteParser; diff --git a/lib/shared/ui/clock_warning_banner.dart b/lib/shared/ui/clock_warning_banner.dart index d98c1932e7..16f0f4fcd4 100644 --- a/lib/shared/ui/clock_warning_banner.dart +++ b/lib/shared/ui/clock_warning_banner.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class ClockWarningBanner extends StatelessWidget { @@ -12,9 +12,11 @@ class ClockWarningBanner extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, systemHealthState) { + final tradingEnabled = + context.watch().state is TradingEnabled; if (systemHealthState is SystemHealthLoadSuccess && !systemHealthState.isValid && - !kIsWalletOnly) { + tradingEnabled) { return _buildWarningBanner(); } return const SizedBox.shrink(); diff --git a/lib/shared/utils/password.dart b/lib/shared/utils/password.dart index 1c9f7b1df2..1230319fac 100644 --- a/lib/shared/utils/password.dart +++ b/lib/shared/utils/password.dart @@ -1,84 +1,9 @@ -import 'dart:math'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -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(''); -} +/// Generates a password that meets the KDF password policy requirements using +/// the device's secure random number generator. +String generatePassword() => SecurityUtils.generatePasswordSecure(16); /// unit tests: [testValidateRPCPassword] -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; -} +bool validateRPCPassword(String src) => + SecurityUtils.checkPasswordRequirements(src).isValid; diff --git a/lib/shared/widgets/coin_balance.dart b/lib/shared/widgets/coin_balance.dart index cf97be4b24..1376b171d9 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/shared/widgets/logout_popup.dart b/lib/shared/widgets/logout_popup.dart index de0c777fdf..32227ba5ff 100644 --- a/lib/shared/widgets/logout_popup.dart +++ b/lib/shared/widgets/logout_popup.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; @@ -39,7 +39,7 @@ class LogOutPopup extends StatelessWidget { if (currentWallet?.config.type == WalletType.iguana || currentWallet?.config.type == WalletType.hdwallet) SelectableText( - kIsWalletOnly + context.watch().state is! TradingEnabled ? LocaleKeys.logoutPopupDescriptionWalletOnly.tr() : LocaleKeys.logoutPopupDescription.tr(), style: const TextStyle( diff --git a/lib/views/bitrefill/bitrefill_button.dart b/lib/views/bitrefill/bitrefill_button.dart index 0000aeb68b..36a631f481 100644 --- a/lib/views/bitrefill/bitrefill_button.dart +++ b/lib/views/bitrefill/bitrefill_button.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui/komodo_ui.dart' show showAddressSearch; import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; import 'package:web_dex/bloc/bitrefill/models/bitrefill_event.dart'; import 'package:web_dex/bloc/bitrefill/models/bitrefill_event_factory.dart'; import 'package:web_dex/bloc/bitrefill/models/bitrefill_payment_intent_event.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/bitrefill/bitrefill_inappwebview_button.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; @@ -20,16 +23,22 @@ import 'package:get_it/get_it.dart'; /// /// The widget returns a payment intent event when the user completes a purchase. /// The event is passed to the [onPaymentRequested] callback. +/// +/// Multi-address support: When the user has multiple addresses, an address +/// selector dialog will be shown allowing them to choose which address to use +/// as the refund address for the Bitrefill transaction. class BitrefillButton extends StatefulWidget { const BitrefillButton({ required this.coin, required this.onPaymentRequested, super.key, this.windowTitle = 'Bitrefill', + this.tooltip, }); final Coin coin; final String windowTitle; + final String? tooltip; final void Function(BitrefillPaymentIntentEvent) onPaymentRequested; @override @@ -37,6 +46,8 @@ class BitrefillButton extends StatefulWidget { } class _BitrefillButtonState extends State { + String? _selectedRefundAddress; + @override void initState() { context @@ -67,14 +78,14 @@ class _BitrefillButtonState extends State { sdk.balances.lastKnown(widget.coin.id)?.spendable.toDouble() ?? 0.0; final bool hasNonZeroBalance = coinBalance > 0; - final bool isEnabled = bitrefillLoadSuccess && - isCoinSupported && - !widget.coin.isSuspended && - hasNonZeroBalance; + final isShown = + bitrefillLoadSuccess && isCoinSupported && !widget.coin.isSuspended; + + final isEnabled = isShown && hasNonZeroBalance || kDebugMode; final String url = state is BitrefillLoadSuccess ? state.url : ''; - if (!isEnabled) { + if (!isShown) { return const SizedBox.shrink(); } @@ -84,7 +95,11 @@ class _BitrefillButtonState extends State { windowTitle: widget.windowTitle, url: url, enabled: isEnabled, + tooltip: _getTooltipMessage( + hasNonZeroBalance, isEnabled, isCoinSupported), onMessage: handleMessage, + onPressed: () async => + _handleButtonPress(context, hasNonZeroBalance), ), ], ); @@ -92,6 +107,69 @@ class _BitrefillButtonState extends State { ); } + /// Gets the appropriate tooltip message based on balance and coin status + String? _getTooltipMessage( + bool hasNonZeroBalance, bool isEnabled, bool isCoinSupported) { + if (widget.tooltip != null) { + return widget.tooltip; + } + + // Show tooltip when button is disabled to explain why + if (!isEnabled) { + if (widget.coin.isSuspended) { + return '${widget.coin.abbr} is currently suspended'; + } + + if (!isCoinSupported) { + return '${widget.coin.abbr} is not supported by Bitrefill'; + } + + if (!hasNonZeroBalance) { + return 'No ${widget.coin.abbr} balance available for spending'; + } + } + + return null; + } + + /// Handles button press with address selection if needed + Future _handleButtonPress( + BuildContext context, + bool hasNonZeroBalance, + ) async { + if (!hasNonZeroBalance) { + return; // Button should be disabled anyway + } + + // Check if we need to show address selector + final addressesBloc = context.read(); + final addresses = addressesBloc.state.addresses; + + if (addresses.length > 1) { + // Show address selector if multiple addresses are available + final selectedAddress = await showAddressSearch( + context, + addresses: addresses, + assetNameLabel: widget.coin.abbr, + ); + + if (selectedAddress != null && context.mounted) { + setState(() { + _selectedRefundAddress = selectedAddress.address; + }); + + // Reload Bitrefill with new address + context.read().add( + BitrefillLoadRequested( + coin: widget.coin, + refundAddress: _selectedRefundAddress, + ), + ); + } + } + // If single address or no address selection needed, the button will work with existing URL + } + /// Handles messages from the Bitrefill widget. /// The message is a JSON string that contains the event name and event data. /// The event name is used to create a [BitrefillWidgetEvent] object. diff --git a/lib/views/bitrefill/bitrefill_button_view.dart b/lib/views/bitrefill/bitrefill_button_view.dart index 90e6084384..fff8c4afc8 100644 --- a/lib/views/bitrefill/bitrefill_button_view.dart +++ b/lib/views/bitrefill/bitrefill_button_view.dart @@ -10,14 +10,16 @@ class BitrefillButtonView extends StatelessWidget { const BitrefillButtonView({ super.key, required this.onPressed, + this.tooltip, }); final void Function()? onPressed; + final String? tooltip; @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( + final buttonWidget = UiPrimaryButton( height: isMobile ? 52 : 40, prefix: Container( padding: const EdgeInsets.only(right: 14), @@ -31,5 +33,16 @@ class BitrefillButtonView extends StatelessWidget { onPressed: onPressed, text: LocaleKeys.spend.tr(), ); + + // Always wrap with tooltip if provided, especially important for disabled buttons + if (tooltip != null && tooltip!.isNotEmpty) { + return Tooltip( + message: tooltip!, + preferBelow: false, + child: buttonWidget, + ); + } + + return buttonWidget; } } diff --git a/lib/views/bitrefill/bitrefill_inappwebview_button.dart b/lib/views/bitrefill/bitrefill_inappwebview_button.dart index 4adad88207..d24596bab7 100644 --- a/lib/views/bitrefill/bitrefill_inappwebview_button.dart +++ b/lib/views/bitrefill/bitrefill_inappwebview_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -18,12 +20,16 @@ class BitrefillInAppWebviewButton extends StatefulWidget { /// The [enabled] property determines if the button is clickable. /// The [windowTitle] property is used as the title of the window. /// The [url] property is the URL to open in the window. + /// The [tooltip] property is used to show a tooltip message when hovering or when button is disabled. + /// The [onPressed] property is called when the button is pressed, before opening the webview. const BitrefillInAppWebviewButton({ required this.url, required this.windowTitle, required this.enabled, required this.onMessage, super.key, + this.tooltip, + this.onPressed, }); /// The title of the pop-up browser window. @@ -39,6 +45,12 @@ class BitrefillInAppWebviewButton extends StatefulWidget { /// webview as a console message. final dynamic Function(String) onMessage; + /// Optional tooltip message to show when hovering or when button is disabled. + final String? tooltip; + + /// Optional callback that is called when the button is pressed, before opening the webview. + final Future Function()? onPressed; + @override BitrefillInAppWebviewButtonState createState() => BitrefillInAppWebviewButtonState(); @@ -63,11 +75,20 @@ class BitrefillInAppWebviewButtonState } }, child: BitrefillButtonView( - onPressed: widget.enabled ? _openDialog : null, + onPressed: widget.enabled ? _handlePress : null, + tooltip: widget.tooltip, ), ); } + Future _handlePress() async { + // Call the onPressed callback first if provided + await widget.onPressed; + + // Then open the dialog + await _openDialog(); + } + Future _openDialog() async { if (kIsWeb) { await _showWebDialog(); diff --git a/lib/views/bridge/bridge_confirmation.dart b/lib/views/bridge/bridge_confirmation.dart index a6ccd78dd2..9fda0a8d8f 100644 --- a/lib/views/bridge/bridge_confirmation.dart +++ b/lib/views/bridge/bridge_confirmation.dart @@ -9,6 +9,7 @@ import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/analytics/events/cross_chain_events.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/blocs/trading_entities_bloc.dart'; @@ -408,22 +409,26 @@ class _ConfirmButton extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingStatusState = context.watch().state; + final tradingEnabled = tradingStatusState.isEnabled; + return Flexible( - child: BlocSelector( - selector: (state) => state.inProgress, - builder: (context, inProgress) { - return Opacity( - opacity: inProgress ? 0.8 : 1, - child: UiPrimaryButton( - key: const Key('bridge-order-confirm-button'), - height: 40, - prefix: inProgress ? const _ProgressIndicator() : null, - text: LocaleKeys.confirm.tr(), - onPressed: inProgress ? null : onPressed, - ), - ); - }), - ); + child: BlocSelector( + selector: (state) => state.inProgress, + builder: (context, inProgress) { + return Opacity( + opacity: inProgress ? 0.8 : 1, + child: UiPrimaryButton( + key: const Key('bridge-order-confirm-button'), + height: 40, + prefix: inProgress ? const _ProgressIndicator() : null, + text: tradingEnabled + ? LocaleKeys.confirm.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), + onPressed: inProgress || !tradingEnabled ? null : onPressed, + ), + ); + })); } } diff --git a/lib/views/bridge/bridge_exchange_form.dart b/lib/views/bridge/bridge_exchange_form.dart index 6bea57f2e6..b2548b5d07 100644 --- a/lib/views/bridge/bridge_exchange_form.dart +++ b/lib/views/bridge/bridge_exchange_form.dart @@ -9,6 +9,7 @@ import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; import 'package:web_dex/bloc/bridge_form/bridge_state.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/widgets/connect_wallet/connect_wallet_wrapper.dart'; import 'package:web_dex/views/bridge/bridge_group.dart'; @@ -121,6 +122,9 @@ class _ExchangeButton extends StatelessWidget { final isSystemClockValid = systemHealthState is SystemHealthLoadSuccess && systemHealthState.isValid; + final tradingStatusState = context.watch().state; + final tradingEnabled = tradingStatusState.isEnabled; + return BlocSelector( selector: (state) => state.inProgress, builder: (context, inProgress) { @@ -136,8 +140,12 @@ class _ExchangeButton extends StatelessWidget { child: UiPrimaryButton( height: 40, prefix: inProgress ? const _Spinner() : null, - text: LocaleKeys.exchange.tr(), - onPressed: isDisabled ? null : () => _onPressed(context), + text: tradingEnabled + ? LocaleKeys.exchange.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), + onPressed: isDisabled || !tradingEnabled + ? null + : () => _onPressed(context), ), ), ), diff --git a/lib/views/bridge/bridge_page.dart b/lib/views/bridge/bridge_page.dart index b4cb4a7f49..6f8b54fe8c 100644 --- a/lib/views/bridge/bridge_page.dart +++ b/lib/views/bridge/bridge_page.dart @@ -50,7 +50,10 @@ class _BridgePageState extends State with TickerProviderStateMixin { }); } }, - child: _showSwap ? _buildTradingDetails() : _buildBridgePage(), + child: Builder(builder: (context) { + final page = _showSwap ? _buildTradingDetails() : _buildBridgePage(); + return page; + }), ); } diff --git a/lib/views/common/main_menu/main_menu_bar_mobile.dart b/lib/views/common/main_menu/main_menu_bar_mobile.dart index 9fbe70cf16..516ab5da8c 100644 --- a/lib/views/common/main_menu/main_menu_bar_mobile.dart +++ b/lib/views/common/main_menu/main_menu_bar_mobile.dart @@ -1,9 +1,12 @@ import 'package:app_theme/app_theme.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_state.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/main_menu_value.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/router/state/routing_state.dart'; @@ -18,6 +21,8 @@ class MainMenuBarMobile extends StatelessWidget { return BlocBuilder( builder: (context, state) { final bool isMMBotEnabled = state.mmBotSettings.isMMBotEnabled; + final bool tradingEnabled = + context.watch().state is TradingEnabled; return DecoratedBox( decoration: BoxDecoration( color: theme.currentGlobal.cardColor, @@ -36,43 +41,70 @@ class MainMenuBarMobile extends StatelessWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - MainMenuBarMobileItem( - value: MainMenuValue.wallet, - isActive: selected == MainMenuValue.wallet, + Expanded( + child: MainMenuBarMobileItem( + value: MainMenuValue.wallet, + isActive: selected == MainMenuValue.wallet, + ), ), - MainMenuBarMobileItem( - value: MainMenuValue.fiat, - enabled: currentWallet?.isHW != true, - isActive: selected == MainMenuValue.fiat, + Expanded( + child: MainMenuBarMobileItem( + value: MainMenuValue.fiat, + enabled: currentWallet?.isHW != true, + isActive: selected == MainMenuValue.fiat, + ), ), - MainMenuBarMobileItem( - value: MainMenuValue.dex, - enabled: currentWallet?.isHW != true, - isActive: selected == MainMenuValue.dex, + Expanded( + child: Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: MainMenuBarMobileItem( + value: MainMenuValue.dex, + enabled: currentWallet?.isHW != true, + isActive: selected == MainMenuValue.dex, + ), + ), ), - MainMenuBarMobileItem( - value: MainMenuValue.bridge, - enabled: currentWallet?.isHW != true, - isActive: selected == MainMenuValue.bridge, + Expanded( + child: Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: MainMenuBarMobileItem( + value: MainMenuValue.bridge, + enabled: currentWallet?.isHW != true, + isActive: selected == MainMenuValue.bridge, + ), + ), ), if (isMMBotEnabled) - MainMenuBarMobileItem( + Expanded( + child: Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: MainMenuBarMobileItem( + enabled: currentWallet?.isHW != true, + value: MainMenuValue.marketMakerBot, + isActive: selected == MainMenuValue.marketMakerBot, + ), + ), + ), + Expanded( + child: MainMenuBarMobileItem( + value: MainMenuValue.nft, enabled: currentWallet?.isHW != true, - value: MainMenuValue.marketMakerBot, - isActive: selected == MainMenuValue.marketMakerBot, + isActive: selected == MainMenuValue.nft, ), - MainMenuBarMobileItem( - value: MainMenuValue.nft, - enabled: currentWallet?.isHW != true, - isActive: selected == MainMenuValue.nft, ), - MainMenuBarMobileItem( - value: MainMenuValue.settings, - isActive: selected == MainMenuValue.settings, + Expanded( + child: MainMenuBarMobileItem( + value: MainMenuValue.settings, + isActive: selected == MainMenuValue.settings, + ), ), - ] - .where((element) => element.value.isEnabledInCurrentMode()) - .toList(), + ], ), ), ), diff --git a/lib/views/common/main_menu/main_menu_bar_mobile_item.dart b/lib/views/common/main_menu/main_menu_bar_mobile_item.dart index a6d23fad1b..2f33ba7e2a 100644 --- a/lib/views/common/main_menu/main_menu_bar_mobile_item.dart +++ b/lib/views/common/main_menu/main_menu_bar_mobile_item.dart @@ -18,46 +18,44 @@ class MainMenuBarMobileItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Expanded( - child: Opacity( - opacity: enabled ? 1 : 0.5, - child: Material( - type: MaterialType.transparency, - child: InkWell( - onTap: enabled - ? () { - routingState.selectedMenu = value; - } - : null, - highlightColor: Colors.transparent, - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - key: Key('main-menu-item-icon-${value.name}'), - padding: const EdgeInsets.only(bottom: 6.0), - child: NavIcon(item: value, isActive: isActive), - ), - AutoScrollText( - text: value.title, - style: isActive - ? theme.currentGlobal.bottomNavigationBarTheme - .selectedLabelStyle - ?.copyWith( - color: theme.currentGlobal.bottomNavigationBarTheme - .selectedItemColor, - ) - : theme.currentGlobal.bottomNavigationBarTheme - .unselectedLabelStyle - ?.copyWith( - color: theme.currentGlobal.bottomNavigationBarTheme - .unselectedItemColor, - ), - ), - ], - ), + return Opacity( + opacity: enabled ? 1 : 0.5, + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: enabled + ? () { + routingState.selectedMenu = value; + } + : null, + highlightColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + key: Key('main-menu-item-icon-${value.name}'), + padding: const EdgeInsets.only(bottom: 6.0), + child: NavIcon(item: value, isActive: isActive), + ), + AutoScrollText( + text: value.title, + style: isActive + ? theme.currentGlobal.bottomNavigationBarTheme + .selectedLabelStyle + ?.copyWith( + color: theme.currentGlobal.bottomNavigationBarTheme + .selectedItemColor, + ) + : theme.currentGlobal.bottomNavigationBarTheme + .unselectedLabelStyle + ?.copyWith( + color: theme.currentGlobal.bottomNavigationBarTheme + .unselectedItemColor, + ), + ), + ], ), ), ), diff --git a/lib/views/common/main_menu/main_menu_desktop.dart b/lib/views/common/main_menu/main_menu_desktop.dart index 1238113e44..fd0b8df87a 100644 --- a/lib/views/common/main_menu/main_menu_desktop.dart +++ b/lib/views/common/main_menu/main_menu_desktop.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/bloc/settings/settings_event.dart'; @@ -34,6 +35,8 @@ class _MainMenuDesktopState extends State { final bool isDarkTheme = settingsState.themeMode == ThemeMode.dark; final bool isMMBotEnabled = settingsState.mmBotSettings.isMMBotEnabled; + final bool tradingEnabled = + context.watch().state is TradingEnabled; final SettingsBloc settings = context.read(); final currentWallet = state.currentUser?.wallet; return Container( @@ -63,28 +66,43 @@ class _MainMenuDesktopState extends State { onTap: onTapItem, isSelected: _checkSelectedItem(MainMenuValue.fiat), ), - DesktopMenuDesktopItem( - key: const Key('main-menu-dex'), - enabled: currentWallet?.isHW != true, - menu: MainMenuValue.dex, - onTap: onTapItem, - isSelected: _checkSelectedItem(MainMenuValue.dex), - ), - DesktopMenuDesktopItem( - key: const Key('main-menu-bridge'), - enabled: currentWallet?.isHW != true, - menu: MainMenuValue.bridge, - onTap: onTapItem, - isSelected: _checkSelectedItem(MainMenuValue.bridge), + Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: DesktopMenuDesktopItem( + key: const Key('main-menu-dex'), + enabled: currentWallet?.isHW != true, + menu: MainMenuValue.dex, + onTap: onTapItem, + isSelected: _checkSelectedItem(MainMenuValue.dex), + ), ), - if (isMMBotEnabled && isAuthenticated) - DesktopMenuDesktopItem( - key: const Key('main-menu-market-maker-bot'), + Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: DesktopMenuDesktopItem( + key: const Key('main-menu-bridge'), enabled: currentWallet?.isHW != true, - menu: MainMenuValue.marketMakerBot, + menu: MainMenuValue.bridge, onTap: onTapItem, - isSelected: - _checkSelectedItem(MainMenuValue.marketMakerBot), + isSelected: _checkSelectedItem(MainMenuValue.bridge), + ), + ), + if (isMMBotEnabled && isAuthenticated) + Tooltip( + message: tradingEnabled + ? '' + : LocaleKeys.tradingDisabledTooltip.tr(), + child: DesktopMenuDesktopItem( + key: const Key('main-menu-market-maker-bot'), + enabled: currentWallet?.isHW != true, + menu: MainMenuValue.marketMakerBot, + onTap: onTapItem, + isSelected: + _checkSelectedItem(MainMenuValue.marketMakerBot), + ), ), DesktopMenuDesktopItem( key: const Key('main-menu-nft'), @@ -129,12 +147,7 @@ class _MainMenuDesktopState extends State { }), ), const SizedBox(height: 48), - ] - // Filter out disabled items - .where((item) => - item is! DesktopMenuDesktopItem || - item.menu.isEnabledInCurrentMode()) - .toList(), + ].toList(), ), ), ); diff --git a/lib/views/dex/dex_page.dart b/lib/views/dex/dex_page.dart index 8d969a2ee2..0224ea28cb 100644 --- a/lib/views/dex/dex_page.dart +++ b/lib/views/dex/dex_page.dart @@ -1,7 +1,6 @@ import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/app_config/app_config.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/dex_tab_bar/dex_tab_bar_bloc.dart'; @@ -43,15 +42,12 @@ class _DexPageState extends State { @override Widget build(BuildContext context) { - if (kIsWalletOnly) { - return const Placeholder(child: Text('You should not see this page')); - } final tradingEntitiesBloc = RepositoryProvider.of(context); final coinsRepository = RepositoryProvider.of(context); final myOrdersService = RepositoryProvider.of(context); - return MultiBlocProvider( + final pageContent = MultiBlocProvider( providers: [ BlocProvider( key: const Key('dex-page'), @@ -70,6 +66,7 @@ class _DexPageState extends State { ? TradingDetails(uuid: routingState.dexState.uuid) : _DexContent(), ); + return pageContent; } void _onRouteChange() { diff --git a/lib/views/dex/simple/confirm/maker_order_confirmation.dart b/lib/views/dex/simple/confirm/maker_order_confirmation.dart index b237d1e004..7c84b08b7e 100644 --- a/lib/views/dex/simple/confirm/maker_order_confirmation.dart +++ b/lib/views/dex/simple/confirm/maker_order_confirmation.dart @@ -9,6 +9,7 @@ import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/analytics/events/transaction_events.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -126,6 +127,9 @@ class _MakerOrderConfirmationState extends State { } Widget _buildConfirmButton() { + final tradingState = context.watch().state; + final bool tradingEnabled = tradingState.isEnabled; + return Opacity( opacity: _inProgress ? 0.8 : 1, child: UiPrimaryButton( @@ -141,8 +145,10 @@ class _MakerOrderConfirmationState extends State { ), ) : null, - onPressed: _inProgress ? null : _startSwap, - text: LocaleKeys.confirm.tr()), + onPressed: _inProgress || !tradingEnabled ? null : _startSwap, + text: tradingEnabled + ? LocaleKeys.confirm.tr() + : LocaleKeys.tradingDisabledTooltip.tr()), ); } diff --git a/lib/views/dex/simple/confirm/taker_order_confirmation.dart b/lib/views/dex/simple/confirm/taker_order_confirmation.dart index 80d91c4863..14b9bd7092 100644 --- a/lib/views/dex/simple/confirm/taker_order_confirmation.dart +++ b/lib/views/dex/simple/confirm/taker_order_confirmation.dart @@ -8,6 +8,7 @@ import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; @@ -130,6 +131,9 @@ class _TakerOrderConfirmationState extends State { } Widget _buildConfirmButton() { + final tradingStatusState = context.watch().state; + final bool tradingEnabled = tradingStatusState.isEnabled; + return BlocSelector( selector: (state) => state.inProgress, builder: (context, inProgress) { @@ -148,8 +152,12 @@ class _TakerOrderConfirmationState extends State { ), ) : null, - onPressed: inProgress ? null : () => _startSwap(context), - text: LocaleKeys.confirm.tr()), + onPressed: inProgress || !tradingEnabled + ? null + : () => _startSwap(context), + text: tradingEnabled + ? LocaleKeys.confirm.tr() + : LocaleKeys.tradingDisabledTooltip.tr()), ); }, ); diff --git a/lib/views/dex/simple/form/maker/maker_form_trade_button.dart b/lib/views/dex/simple/form/maker/maker_form_trade_button.dart index d0dbe8f6f8..1bda25ae86 100644 --- a/lib/views/dex/simple/form/maker/maker_form_trade_button.dart +++ b/lib/views/dex/simple/form/maker/maker_form_trade_button.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/blocs/maker_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -20,6 +21,9 @@ class MakerFormTradeButton extends StatelessWidget { systemHealthState is SystemHealthLoadSuccess && systemHealthState.isValid; + final tradingState = context.watch().state; + final isTradingEnabled = tradingState.isEnabled; + final makerFormBloc = RepositoryProvider.of(context); final authBloc = context.watch(); @@ -34,7 +38,9 @@ class MakerFormTradeButton extends StatelessWidget { opacity: disabled ? 0.8 : 1, child: UiPrimaryButton( key: const Key('make-order-button'), - text: LocaleKeys.makeOrder.tr(), + text: isTradingEnabled + ? LocaleKeys.makeOrder.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), prefix: inProgress ? Padding( padding: const EdgeInsets.only(right: 4), @@ -46,7 +52,7 @@ class MakerFormTradeButton extends StatelessWidget { ), ) : null, - onPressed: disabled + onPressed: disabled || !isTradingEnabled ? null : () async { while (!authBloc.state.isSignedIn) { diff --git a/lib/views/dex/simple/form/taker/taker_form_content.dart b/lib/views/dex/simple/form/taker/taker_form_content.dart index f66068b443..d010a12341 100644 --- a/lib/views/dex/simple/form/taker/taker_form_content.dart +++ b/lib/views/dex/simple/form/taker/taker_form_content.dart @@ -9,6 +9,7 @@ import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_light_button.dart'; @@ -125,6 +126,10 @@ class TradeButton extends StatelessWidget { systemHealthState is SystemHealthLoadSuccess && systemHealthState.isValid; + final tradingStatusState = context.watch().state; + + final isTradingEnabled = tradingStatusState.isEnabled; + return BlocSelector( selector: (state) => state.inProgress, builder: (context, inProgress) { @@ -134,9 +139,11 @@ class TradeButton extends StatelessWidget { opacity: disabled ? 0.8 : 1, child: UiPrimaryButton( key: const Key('take-order-button'), - text: LocaleKeys.swapNow.tr(), + text: isTradingEnabled + ? LocaleKeys.swapNow.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), prefix: inProgress ? const TradeButtonSpinner() : null, - onPressed: disabled + onPressed: disabled || !isTradingEnabled ? null : () => context.read().add(TakerFormSubmitClick()), diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 46453151d1..9768ad6e22 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/blocs/update_bloc.dart'; import 'package:web_dex/common/screen.dart'; @@ -33,7 +34,11 @@ class _MainLayoutState extends State { await AlphaVersionWarningService().run(); await updateBloc.init(); - if (!kIsWalletOnly && !await _hasAgreedNoTrading()) { + final tradingEnabled = + context.read().state is TradingEnabled; + if (tradingEnabled && + kShowTradingWarning && + !await _hasAgreedNoTrading()) { _showNoTradingWarning().ignore(); } }); diff --git a/lib/views/market_maker_bot/add_market_maker_bot_trade_button.dart b/lib/views/market_maker_bot/add_market_maker_bot_trade_button.dart index 0c81ff781a..0fe8a660c1 100644 --- a/lib/views/market_maker_bot/add_market_maker_bot_trade_button.dart +++ b/lib/views/market_maker_bot/add_market_maker_bot_trade_button.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/system_health/system_health_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class AddMarketMakerBotTradeButton extends StatelessWidget { @@ -19,12 +20,18 @@ class AddMarketMakerBotTradeButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, systemHealthState) { + final tradingStatusBloc = context.watch(); + + final bool tradingEnabled = tradingStatusBloc.state.isEnabled; + return Opacity( opacity: !enabled ? 0.8 : 1, child: UiPrimaryButton( key: const Key('make-order-button'), - text: LocaleKeys.makeOrder.tr(), - onPressed: !enabled ? null : () => onPressed(), + text: tradingEnabled + ? LocaleKeys.makeOrder.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), + onPressed: !enabled || !tradingEnabled ? null : () => onPressed(), height: 40, ), ); diff --git a/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart b/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart index 87f9ea25d2..a499afb3fd 100644 --- a/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart +++ b/lib/views/market_maker_bot/market_maker_bot_confirmation_form.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/bloc/market_maker_bot/market_maker_trade_form/market_maker_trade_form_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; @@ -144,6 +145,10 @@ class SwapActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { + final tradingStatusBloc = context.watch(); + + final bool tradingEnabled = tradingStatusBloc.state is TradingEnabled; + return Row( children: [ Flexible( @@ -156,8 +161,10 @@ class SwapActionButtons extends StatelessWidget { Flexible( child: UiPrimaryButton( key: const Key('market-maker-bot-order-confirm-button'), - onPressed: onCreateOrder, - text: LocaleKeys.confirm.tr(), + onPressed: !tradingEnabled ? null : onCreateOrder, + text: tradingEnabled + ? LocaleKeys.confirm.tr() + : LocaleKeys.tradingDisabledTooltip.tr(), ), ), ], diff --git a/lib/views/market_maker_bot/market_maker_bot_page.dart b/lib/views/market_maker_bot/market_maker_bot_page.dart index 7648e0d9e6..cfd7612cc6 100644 --- a/lib/views/market_maker_bot/market_maker_bot_page.dart +++ b/lib/views/market_maker_bot/market_maker_bot_page.dart @@ -52,7 +52,7 @@ class _MarketMakerBotPageState extends State { coinsRepository, ); - return MultiBlocProvider( + final pageContent = MultiBlocProvider( providers: [ BlocProvider( create: (BuildContext context) => DexTabBarBloc( @@ -90,6 +90,7 @@ class _MarketMakerBotPageState extends State { : MarketMakerBotView(), ), ); + return pageContent; } void _onRouteChange() { diff --git a/lib/views/settings/widgets/general_settings/general_settings.dart b/lib/views/settings/widgets/general_settings/general_settings.dart index 8ce028c380..809f5fd5b5 100644 --- a/lib/views/settings/widgets/general_settings/general_settings.dart +++ b/lib/views/settings/widgets/general_settings/general_settings.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/shared/widgets/hidden_with_wallet.dart'; import 'package:web_dex/shared/widgets/hidden_without_wallet.dart'; @@ -31,7 +32,7 @@ class GeneralSettings extends StatelessWidget { const SizedBox(height: 25), const SettingsManageWeakPasswords(), const SizedBox(height: 25), - if (!kIsWalletOnly) + if (context.watch().state is TradingEnabled) const HiddenWithoutWallet( child: SettingsManageTradingBot(), ), diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart index 3dfe14ea42..5817418432 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_addresses.dart @@ -464,7 +464,7 @@ class SwapAddressTag extends StatelessWidget { borderRadius: BorderRadius.circular(16.0), ), child: Text( - LocaleKeys.swapAddress.tr(), + LocaleKeys.tradingAddress.tr(), style: TextStyle(fontSize: isMobile ? 9 : 12), ), ), diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart index 10a42b9ce3..3857778f05 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_common_buttons.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -101,7 +102,7 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (isBitrefillIntegrationEnabled) + if (isBitrefillIntegrationEnabled) ...[ Flexible( child: BitrefillButton( key: Key( @@ -109,9 +110,11 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { ), coin: coin, onPaymentRequested: (_) => selectWidget(CoinPageType.send), + tooltip: _getBitrefillTooltip(coin), ), ), - if (isBitrefillIntegrationEnabled) const SizedBox(width: 15), + const SizedBox(width: 12), + ], if (!coin.walletOnly) Flexible( child: CoinDetailsSwapButton( @@ -167,7 +170,8 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { context: context, ), ), - if (!coin.walletOnly && !kIsWalletOnly) + if (!coin.walletOnly && + context.watch().state is TradingEnabled) Container( margin: const EdgeInsets.only(left: 21), constraints: const BoxConstraints(maxWidth: 120), @@ -188,6 +192,7 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { ), coin: coin, onPaymentRequested: (_) => selectWidget(CoinPageType.send), + tooltip: _getBitrefillTooltip(coin), ), ), Flexible( @@ -412,3 +417,13 @@ class CoinDetailsSwapButton extends StatelessWidget { ); } } + +/// Gets the appropriate tooltip message for the Bitrefill button +String? _getBitrefillTooltip(Coin coin) { + if (coin.isSuspended) { + return '${coin.abbr} is currently suspended'; + } + + // Check if coin has zero balance (this could be enhanced with actual balance check) + return null; // Let BitrefillButton handle the zero balance tooltip +} diff --git a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart index d8223e60f3..945d073c21 100644 --- a/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart +++ b/lib/views/wallet/coin_details/coin_details_info/coin_details_info.dart @@ -6,15 +6,14 @@ import 'package:flutter_svg/svg.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.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/portfolio_events.dart'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; -import 'package:web_dex/bloc/analytics/analytics_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_bloc.dart'; import 'package:web_dex/bloc/taker_form/taker_event.dart'; @@ -267,9 +266,10 @@ class _DesktopCoinDetails extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: false, selectWidget: setPageType, - onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() - ? null - : () => _goToSwap(context, coin), + onClickSwapButton: + context.watch().state is TradingEnabled + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), @@ -368,9 +368,10 @@ class _CoinDetailsInfoHeader extends StatelessWidget { child: CoinDetailsCommonButtons( isMobile: true, selectWidget: setPageType, - onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() - ? () => _goToSwap(context, coin) - : null, + onClickSwapButton: + context.watch().state is TradingEnabled + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), 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 1a5a4c9d7d..8d64c02996 100644 --- a/lib/views/wallet/wallet_page/common/asset_list_item.dart +++ b/lib/views/wallet/wallet_page/common/asset_list_item.dart @@ -14,12 +14,14 @@ 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 3d6b394285..ba3c56585e 100644 --- a/lib/views/wallet/wallet_page/common/assets_list.dart +++ b/lib/views/wallet/wallet_page/common/assets_list.dart @@ -47,6 +47,7 @@ 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 b195288f9d..90de8947fa 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,11 +2,12 @@ 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'; import 'package:komodo_ui/komodo_ui.dart'; -import 'package:web_dex/app_config/app_config.dart'; +import 'package:web_dex/bloc/trading_status/trading_status_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/model/coin.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -18,6 +19,9 @@ 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; @@ -73,12 +77,17 @@ 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: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + headerPadding: EdgeInsets.symmetric( + horizontal: horizontalPadding, vertical: verticalPadding), onTap: widget.onTap, - childrenMargin: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + childrenMargin: EdgeInsets.symmetric( + horizontal: horizontalPadding, vertical: verticalPadding), childrenDecoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(12), @@ -109,6 +118,79 @@ 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( @@ -163,62 +245,63 @@ class _AddressRow extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); - 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, - // ), + 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, ), - ), - if (isSwapAddress && !kIsWalletOnly) ...[ const SizedBox(width: 8), - const Chip( - label: Text( - 'Swap', - // style: theme.textTheme.labelSmall, + Material( + color: Colors.transparent, + child: IconButton( + iconSize: 16, + icon: const Icon(Icons.copy), + onPressed: onCopy, + visualDensity: VisualDensity.compact, ), - // backgroundColor: theme.colorScheme.primaryContainer, ), + if (isSwapAddress && + context.watch().state is TradingEnabled) ...[ + 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 e155ed0d98..2e56d6a47d 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,30 +56,29 @@ class ActiveCoinsList extends StatelessWidget { sorted = removeTestCoins(sorted); } - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final coin = sorted[index]; + return SliverList.builder( + itemCount: sorted.length, + itemBuilder: (context, index) { + final coin = sorted[index]; - // Fetch pubkeys if not already loaded - if (!state.pubkeys.containsKey(coin.abbr)) { - context.read().add(CoinsPubkeysRequested(coin.abbr)); - } + // 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)); + } - 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, - ), + 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), + ), + ); + }, ); }, ); @@ -214,7 +213,7 @@ class AddressBalanceCard extends StatelessWidget { AddressCopyButton(address: pubkey.address), if (pubkey.isActiveForSwap) Chip( - label: Text(LocaleKeys.swapAddress.tr()), + label: Text(LocaleKeys.tradingAddress.tr()), backgroundColor: Theme.of(context) .primaryColor .withOpacity(0.1), 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 e6e8ca52e6..5d0aa79b08 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/animated_portfolio_charts.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/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: 2, vsync: this); + _tabController = TabController(length: 3, vsync: this); } @override @@ -109,51 +109,143 @@ class _WalletMainState extends State header: isMobile ? PageHeader(title: LocaleKeys.wallet.tr()) : null, content: Expanded( - 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), + 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, + ), ), - 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, + SliverToBoxAdapter( + child: SizedBox(height: 8), ), - const Gap(8), - ], - ), - ), - SliverPersistentHeader( - pinned: true, - delegate: _SliverSearchBarDelegate( - withBalance: _showCoinWithBalance, - onSearchChange: _onSearchChange, - onWithBalanceChange: _onShowCoinsWithBalanceClick, - mode: authStateMode, + CoinListView( + mode: authStateMode, + searchPhrase: _searchKey, + withBalance: _showCoinWithBalance, + onActiveCoinItemTap: _onActiveCoinItemTap, + onAssetItemTap: _onAssetItemTap, + ), + ], + ), ), - ), - SliverToBoxAdapter( - child: SizedBox(height: 8), - ), - _buildCoinList(authStateMode), + ], ], ), ), @@ -223,39 +315,6 @@ 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; @@ -273,11 +332,6 @@ 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(); @@ -321,6 +375,57 @@ 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, @@ -336,7 +441,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { @override final double minExtent = 132; @override - final double maxExtent = 132; + final double maxExtent = 155; @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 e97fcd975c..3e0dcb1469 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,57 +29,56 @@ class WalletManageSection extends StatelessWidget { @override Widget build(BuildContext context) { - return isMobile - ? _buildMobileSection(context) - : _buildDesktopSection(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)); } bool get isAuthenticated => mode == AuthorizeMode.logIn; Widget _buildDesktopSection(BuildContext context) { final ThemeData theme = Theme.of(context); - 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), - ), + 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, ), - 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), + ], ), ); } @@ -95,7 +94,7 @@ class WalletManageSection extends StatelessWidget { Row( children: [ Text( - 'Currency', + 'Portfolio', style: theme.textTheme.titleLarge, ), Spacer(), @@ -103,7 +102,7 @@ class WalletManageSection extends StatelessWidget { UiPrimaryButton( buttonKey: const Key('asset-management-button'), onPressed: () => _onAddAssetsPress(context), - text: 'Asset management', + text: 'Add assets', height: 36, width: 147, borderRadius: 10, @@ -130,6 +129,7 @@ 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 e68805f747..2ed67e59cc 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -4,21 +4,32 @@ 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(); @@ -32,15 +43,24 @@ 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( @@ -51,52 +71,94 @@ class _WalletOverviewState extends State { _logged = true; } - 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), + // 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, ), - 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()}'), - ], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - ), + ); + }, + ), + ), + 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), ), ), - 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, + ), + 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), ), ), - ], - ); + 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(), + ); + } }, ); } @@ -112,4 +174,98 @@ 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 8146b205a2..5a09371c12 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: 42, + iconSize: 36, onPressed: onPressed, ), ), diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index e8ac3a0b35..e5af2c05a8 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -36,10 +36,10 @@ packages: dependency: transitive description: name: decimal - sha256: "28239b8b929c1bd8618702e6dbc96e2618cf99770bbe9cb040d6cf56a11e4ec3" + sha256: "6c2041df7caefc9393ae0b0dcc4abc700831014a2c252dd10e3952499673f0b2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" equatable: dependency: transitive description: @@ -95,7 +95,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" 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: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" 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: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" 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: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" url: "https://pub.dev" source: hosted - version: "6.0.7" + version: "7.0.1" path: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index a60797aa1b..b40d91a62b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -337,10 +337,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" flutter_driver: dependency: transitive description: flutter @@ -435,10 +435,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.28" flutter_secure_storage: dependency: transitive description: @@ -578,18 +578,18 @@ packages: dependency: transitive description: name: html - sha256: "9475be233c437f0e3637af55e7702cbbe5c23a68bd56e8a5fa2d426297b7c6c8" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5+1" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_multi_server: dependency: transitive description: @@ -648,16 +648,25 @@ packages: description: path: "packages/komodo_cex_market_data" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" + komodo_coin_updates: + dependency: transitive + description: + path: "packages/komodo_coin_updates" + ref: dev + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "1.0.0" komodo_coins: dependency: transitive description: path: "packages/komodo_coins" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -666,7 +675,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0" @@ -675,7 +684,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -684,7 +693,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -693,7 +702,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -702,7 +711,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -718,7 +727,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -734,7 +743,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "55176e13b1565f55b7efa4c6de595affe61eb688" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -782,10 +791,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74" + sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" url: "https://pub.dev" source: hosted - version: "1.0.48" + version: "1.0.49" local_auth_darwin: dependency: transitive description: @@ -862,10 +871,10 @@ packages: dependency: transitive description: name: mobile_scanner - sha256: "9cb9e371ee9b5b548714f9ab5fd33b530d799745c83d5729ecd1e8ab2935dbd1" + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" url: "https://pub.dev" source: hosted - version: "6.0.7" + version: "7.0.1" mutex: dependency: transitive description: @@ -950,10 +959,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1038,10 +1047,10 @@ packages: dependency: transitive description: name: provider - sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -1102,10 +1111,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: c2c8c46297b5d6a80bed7741ec1f2759742c77d272f1a1698176ae828f8e1a18 + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1415,6 +1424,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + very_good_analysis: + dependency: transitive + description: + name: very_good_analysis + sha256: c529563be4cbba1137386f2720fb7ed69e942012a28b13398d8a5e3e6ef551a7 + url: "https://pub.dev" + source: hosted + version: "8.0.0" video_player: dependency: "direct main" description: @@ -1515,10 +1532,10 @@ packages: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.0" window_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3bce361b41..456667bb7b 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.0+0 +version: 0.9.1+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.3.0 # dart.dev + http: 1.4.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