From 5e096b000d6fbccae3edc2ddd60864755d7d4e88 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:25:10 +0100 Subject: [PATCH] feat(hd): HD withdrawals + breaking SDK changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Migrate withdrawals to the SDK and add HD wallet support. - Migrate withdrawal-related widgets to the SDK’s `komodo_ui` package. - Fix breaking changes for the SDK’s `0.2.0` version for multi-SDK support. - Formatting fixes for files (mostly) only touched in this PR using `use_key_in_widget_constructors` and `require_trailing_commas` --- .gitignore | 3 + app_theme/lib/src/dark/theme_global_dark.dart | 34 +- assets/translations/en.json | 2 +- lib/app_config/app_config.dart | 7 + lib/bloc/app_bloc_root.dart | 15 +- lib/bloc/bridge_form/bridge_repository.dart | 14 +- lib/bloc/cex_market_data/charts.dart | 2 +- .../mock_portfolio_growth_repository.dart | 3 + .../mock_transaction_history_repository.dart | 3 +- .../portfolio_growth_repository.dart | 5 +- .../models/price_chart_interval.dart | 4 +- .../price_chart/models/time_period.dart | 4 +- .../demo_profit_loss_repository.dart | 3 + .../profit_loss_transaction_extension.dart | 2 +- .../models/price_stamped_transaction.dart | 2 +- .../profit_loss/models/profit_loss.dart | 2 +- .../profit_loss/profit_loss_bloc.dart | 11 +- .../profit_loss/profit_loss_calculator.dart | 2 +- .../profit_loss/profit_loss_repository.dart | 3 + .../bloc/coin_addresses_bloc.dart | 62 +- lib/bloc/coins_bloc/asset_coin_extension.dart | 6 +- lib/bloc/coins_bloc/coins_repo.dart | 3 +- lib/bloc/dex_repository.dart | 25 +- .../fiat_onramp_form/fiat_form_state.dart | 21 +- lib/bloc/fiat/models/fiat_buy_order_info.dart | 2 +- lib/bloc/fiat/models/fiat_order.dart | 1 + lib/bloc/fiat/models/i_currency.dart | 2 + .../transaction_history_bloc.dart | 297 ++++-- .../transaction_history_event.dart | 7 +- .../transaction_history_repo.dart | 27 +- .../transaction_history_state.dart | 2 +- .../withdraw_form/withdraw_form_bloc.dart | 918 ++++++++---------- .../withdraw_form/withdraw_form_event.dart | 129 +-- .../withdraw_form/withdraw_form_state.dart | 343 ++++--- .../withdraw_form/withdraw_form_step.dart | 6 +- lib/generated/codegen_loader.g.dart | 114 ++- lib/main.dart | 2 +- lib/mm2/mm2.dart | 2 +- lib/mm2/mm2_api/mm2_api.dart | 44 +- lib/mm2/mm2_api/mm2_api_nft.dart | 64 +- lib/mm2/mm2_api/mm2_api_trezor.dart | 2 +- .../mm2_api/rpc/best_orders/best_orders.dart | 2 +- lib/model/coin.dart | 16 +- lib/model/swap.dart | 2 +- lib/model/wallet.dart | 4 +- .../navigators/app_router_delegate.dart | 24 +- .../ui/custom_numeric_text_form_field.dart | 2 +- .../utils/extensions/async_extensions.dart | 1 - lib/shared/utils/formatters.dart | 19 +- lib/shared/utils/password.dart | 5 +- lib/shared/utils/utils.dart | 54 +- lib/shared/utils/zip.dart | 2 +- lib/shared/widgets/html_parser.dart | 22 +- .../launch_native_explorer_button.dart | 2 +- lib/shared/widgets/simple_copyable_link.dart | 2 +- .../dex_list_filter_coins_list_mobile.dart | 96 +- .../history/swap_history_sort_mixin.dart | 9 +- .../maker_order/maker_order_details_page.dart | 47 +- .../swap/swap_details_step.dart | 37 +- .../swap/swap_recover_button.dart | 5 +- .../simple/form/taker/taker_form_content.dart | 67 +- .../simple/form/taker/taker_form_layout.dart | 8 +- lib/views/fiat/webview_dialog.dart | 2 +- lib/views/main_layout/main_layout.dart | 25 +- ...t_maker_form_error_message_extensions.dart | 4 +- .../trade_pair_list_item.dart | 4 +- lib/views/qr_scanner.dart | 101 -- .../general_settings/app_version_number.dart | 7 +- .../widgets/support_page/support_page.dart | 166 ++-- .../wallet/coin_details/coin_details.dart | 11 +- .../charts/portfolio_growth_chart.dart | 11 +- .../charts/portfolio_profit_loss_chart.dart | 13 +- .../coin_details_info/coin_addresses.dart | 168 ++-- .../coin_details_common_buttons.dart | 3 +- .../coin_details_info/coin_details_info.dart | 8 +- .../contract_address_button.dart | 21 +- .../faucet/cubit/faucet_cubit.dart | 4 +- .../rewards/kmd_reward_info_header.dart | 4 +- .../rewards/kmd_rewards_info.dart | 306 +++--- .../transactions/transaction_details.dart | 23 +- .../transactions/transaction_list.dart | 2 +- .../transactions/transaction_list_item.dart | 110 ++- .../transactions/transaction_table.dart | 30 +- .../withdraw_form/pages/failed_page.dart | 22 +- .../withdraw_form/pages/fill_form_page.dart | 68 -- .../buttons/convert_address_button.dart | 4 +- .../fill_form/buttons/sell_max_button.dart | 4 +- .../custom_fee/custom_fee_field_evm.dart | 102 +- .../custom_fee/custom_fee_field_utxo.dart | 43 +- .../custom_fee/fill_form_custom_fee.dart | 45 +- .../widgets/fill_form/fields/fields.dart | 632 ++++++++++++ .../fill_form/fields/fill_form_amount.dart | 10 +- .../fill_form/fields/fill_form_memo.dart | 21 +- .../fields/fill_form_recipient_address.dart | 66 +- .../fill_form_trezor_sender_address.dart | 3 +- .../widgets/fill_form/fill_form_error.dart | 60 +- .../widgets/fill_form/fill_form_footer.dart | 7 +- .../send_complete_form.dart | 70 +- .../send_complete_form_buttons.dart | 72 +- .../send_confirm_buttons.dart | 57 +- .../send_confirm_footer.dart | 7 +- .../send_confirm_form/send_confirm_form.dart | 21 +- .../send_confirm_form_error.dart | 30 +- .../widgets/withdraw_form_header.dart | 20 +- .../withdraw_form/withdraw_form.dart | 689 +++++++++++-- .../withdraw_form/withdraw_form_index.dart | 92 -- .../coins_manager/coins_manager_controls.dart | 4 +- .../wallet_page/charts/coin_prices_chart.dart | 21 +- .../wallet_main/wallet_overview.dart | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + packages/komodo_ui_kit/lib/komodo_ui_kit.dart | 2 - .../lib/src/buttons/divided_button.dart | 62 -- .../market_chart_header_controls.dart | 7 +- .../controls/selected_coin_graph_control.dart | 13 +- .../src/display/trend_percentage_text.dart | 41 - .../lib/src/images/coin_icon.dart | 9 +- .../lib/src/inputs/coin_search_dropdown.dart | 417 +------- .../lib/src/inputs/percentage_input.dart | 8 +- .../lib/src/inputs/ui_text_form_field.dart | 188 ++-- packages/komodo_ui_kit/pubspec.lock | 100 +- packages/komodo_ui_kit/pubspec.yaml | 20 +- pubspec.lock | 336 ++++--- pubspec.yaml | 14 +- .../profit_loss_repository_test.dart | 2 +- 124 files changed, 4016 insertions(+), 3025 deletions(-) delete mode 100644 lib/views/qr_scanner.dart delete mode 100644 lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart create mode 100644 lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart delete mode 100644 lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart delete mode 100644 packages/komodo_ui_kit/lib/src/buttons/divided_button.dart delete mode 100644 packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart diff --git a/.gitignore b/.gitignore index f647132e55..a8f5a5ae6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# First-party +sdk/ + # Miscellaneous *.class *.log diff --git a/app_theme/lib/src/dark/theme_global_dark.dart b/app_theme/lib/src/dark/theme_global_dark.dart index 21d7d24196..6e69ad0d59 100644 --- a/app_theme/lib/src/dark/theme_global_dark.dart +++ b/app_theme/lib/src/dark/theme_global_dark.dart @@ -7,7 +7,8 @@ ThemeData get themeGlobalDark { SnackBarThemeData snackBarThemeLight() => const SnackBarThemeData( elevation: 12.0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4))), + borderRadius: BorderRadius.all(Radius.circular(4)), + ), actionTextColor: Colors.green, behavior: SnackBarBehavior.floating, ); @@ -32,16 +33,29 @@ ThemeData get themeGlobalDark { final TextTheme textTheme = TextTheme( headlineMedium: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w700, color: textColor), + fontSize: 16, + fontWeight: FontWeight.w700, + color: textColor, + ), headlineSmall: const TextStyle( - fontSize: 40, fontWeight: FontWeight.w700, color: textColor), + fontSize: 40, + fontWeight: FontWeight.w700, + color: textColor, + ), titleLarge: const TextStyle( - fontSize: 26.0, color: textColor, fontWeight: FontWeight.w700), + fontSize: 26.0, + color: textColor, + fontWeight: FontWeight.w700, + ), titleSmall: const TextStyle(fontSize: 18.0, color: textColor), bodyMedium: const TextStyle( - fontSize: 16.0, color: textColor, fontWeight: FontWeight.w300), + fontSize: 16.0, + color: textColor, + fontWeight: FontWeight.w300, + ), labelLarge: const TextStyle(fontSize: 16.0, color: textColor), - bodyLarge: TextStyle(fontSize: 14.0, color: textColor.withValues(alpha: 0.5)), + bodyLarge: + TextStyle(fontSize: 14.0, color: textColor.withValues(alpha: 0.5)), bodySmall: TextStyle( fontSize: 12.0, color: textColor.withValues(alpha: 0.8), @@ -80,7 +94,8 @@ ThemeData get themeGlobalDark { snackBarTheme: snackBarThemeLight(), textSelectionTheme: TextSelectionThemeData( cursorColor: const Color.fromRGBO(57, 161, 238, 1), - selectionColor: const Color.fromRGBO(57, 161, 238, 1).withValues(alpha: 0.3), + selectionColor: + const Color.fromRGBO(57, 161, 238, 1).withValues(alpha: 0.3), selectionHandleColor: const Color.fromRGBO(57, 161, 238, 1), ), inputDecorationTheme: InputDecorationTheme( @@ -157,8 +172,9 @@ ThemeData get themeGlobalDark { ), textTheme: textTheme, scrollbarTheme: ScrollbarThemeData( - thumbColor: - WidgetStateProperty.all(colorScheme.primary.withValues(alpha: 0.8)), + thumbColor: WidgetStateProperty.all( + colorScheme.primary.withValues(alpha: 0.8), + ), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( // remove icons shift diff --git a/assets/translations/en.json b/assets/translations/en.json index 90dcefbeb9..1bb2add6f9 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -403,7 +403,7 @@ "missingDerivationPath": "Missing derivation path configuration", "protocolNotSupported": "Protocol does not support multiple addresses", "derivationModeNotSupported": "Current wallet mode does not support multiple addresses", - "hdWalletModeSwitchTitle": "HD Wallet?", + "hdWalletModeSwitchTitle": "Multi-address Wallet?", "hdWalletModeSwitchSubtitle": "Enabling HD wallet allows you to create multiple addresses for each coin. However, your addresses and balances will change. You can easily switch between the modes when logging in.", "hdWalletModeSwitchTooltip": "HD wallets require a valid BIP39 seed phrase.", "noActiveWallet": "No active wallet - please sign in first", diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index 1203a73656..a5878f54a3 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -18,6 +18,13 @@ const String coinsAssetsPath = 'packages/komodo_defi_framework/assets'; // TODO: Remove this flag after the feature is finalized 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. +/// +///! 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; const Duration kPerformanceLogInterval = Duration(minutes: 1); diff --git a/lib/bloc/app_bloc_root.dart b/lib/bloc/app_bloc_root.dart index 55aa223ca2..2475734b66 100644 --- a/lib/bloc/app_bloc_root.dart +++ b/lib/bloc/app_bloc_root.dart @@ -67,9 +67,9 @@ import 'package:web_dex/shared/widgets/coin_icon.dart'; class AppBlocRoot extends StatelessWidget { const AppBlocRoot({ - Key? key, required this.storedPrefs, required this.komodoDefiSdk, + super.key, }); final StoredSettings storedPrefs; @@ -135,6 +135,7 @@ class AppBlocRoot extends StatelessWidget { demoMode: performanceMode, coinsRepository: coinsRepository, mm2Api: mm2Api, + sdk: komodoDefiSdk, ); final portfolioGrowthRepo = PortfolioGrowthRepository.withDefaults( @@ -143,6 +144,7 @@ class AppBlocRoot extends StatelessWidget { demoMode: performanceMode, coinsRepository: coinsRepository, mm2Api: mm2Api, + sdk: komodoDefiSdk, ); _clearCachesIfPerformanceModeChanged( @@ -158,10 +160,11 @@ class AppBlocRoot extends StatelessWidget { return MultiRepositoryProvider( providers: [ RepositoryProvider( - create: (_) => NftsRepo( - api: mm2Api.nft, - coinsRepo: coinsRepository, - )), + create: (_) => NftsRepo( + api: mm2Api.nft, + coinsRepo: coinsRepository, + ), + ), RepositoryProvider(create: (_) => tradingEntitiesBloc), RepositoryProvider(create: (_) => dexRepository), RepositoryProvider( @@ -219,7 +222,7 @@ class AppBlocRoot extends StatelessWidget { ), BlocProvider( create: (BuildContext ctx) => TransactionHistoryBloc( - repo: transactionsRepo, + sdk: komodoDefiSdk, ), ), BlocProvider( diff --git a/lib/bloc/bridge_form/bridge_repository.dart b/lib/bloc/bridge_form/bridge_repository.dart index ba3733186f..5b728a7a69 100644 --- a/lib/bloc/bridge_form/bridge_repository.dart +++ b/lib/bloc/bridge_form/bridge_repository.dart @@ -16,7 +16,7 @@ class BridgeRepository { final KomodoDefiSdk _kdfSdk; final CoinsRepo _coinsRepository; - Future getSellCoins( CoinsByTicker? tickers) async { + Future getSellCoins(CoinsByTicker? tickers) async { if (tickers == null) return null; final List? depths = await _getDepths(tickers); @@ -26,9 +26,11 @@ class BridgeRepository { tickers.entries.fold({}, (previousValue, entry) { final List coins = previousValue[entry.key] ?? []; final List tickerDepths = depths - .where((depth) => - (abbr2Ticker(depth.source.abbr) == entry.key) && - (abbr2Ticker(depth.target.abbr) == entry.key)) + .where( + (depth) => + (abbr2Ticker(depth.source.abbr) == entry.key) && + (abbr2Ticker(depth.target.abbr) == entry.key), + ) .toList(); if (tickerDepths.isEmpty) return previousValue; @@ -66,7 +68,9 @@ class BridgeRepository { return multiProtocolCoins; } else { return removeTokensWithEmptyOrderbook( - multiProtocolCoins, orderBookDepths); + multiProtocolCoins, + orderBookDepths, + ); } } diff --git a/lib/bloc/cex_market_data/charts.dart b/lib/bloc/cex_market_data/charts.dart index 2cb886cd95..7463ca3c1b 100644 --- a/lib/bloc/cex_market_data/charts.dart +++ b/lib/bloc/cex_market_data/charts.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; typedef ChartData = List>; diff --git a/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart b/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart index 0a506d2b78..68567d22e3 100644 --- a/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart +++ b/lib/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart @@ -1,5 +1,6 @@ import 'package:http/http.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/generator.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart'; @@ -25,6 +26,7 @@ class MockPortfolioGrowthRepository extends PortfolioGrowthRepository { required this.performanceMode, required CoinsRepo coinsRepository, required Mm2Api mm2Api, + required KomodoDefiSdk sdk, }) : super( cexRepository: BinanceRepository( binanceProvider: const BinanceProvider(), @@ -34,6 +36,7 @@ class MockPortfolioGrowthRepository extends PortfolioGrowthRepository { client: Client(), performanceMode: performanceMode, demoDataGenerator: DemoDataCache.withDefaults(), + sdk: sdk, ), cacheProvider: HiveLazyBoxProvider( name: GraphType.balanceGrowth.tableName, diff --git a/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart b/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart index 9003a73f20..77a7f2643f 100644 --- a/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart +++ b/lib/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart @@ -15,7 +15,8 @@ class MockTransactionHistoryRepo extends TransactionHistoryRepo { required Client client, required this.performanceMode, required this.demoDataGenerator, - }) : super(); + required super.sdk, + }); // TODO: SDK Port needed, not sure about this part Future> fetchTransactions(Coin coin) async { 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 819955b931..933443b10d 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 @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:hive/hive.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' as cex; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/charts.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/mock_portfolio_growth_repository.dart'; @@ -12,7 +13,7 @@ import 'package:web_dex/bloc/cex_market_data/models/models.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/coin.dart'; /// A repository for fetching the growth chart for the portfolio and coins. @@ -36,6 +37,7 @@ class PortfolioGrowthRepository { required cex.CexRepository cexRepository, required CoinsRepo coinsRepository, required Mm2Api mm2Api, + required KomodoDefiSdk sdk, PerformanceMode? demoMode, }) { if (demoMode != null) { @@ -43,6 +45,7 @@ class PortfolioGrowthRepository { performanceMode: demoMode, coinsRepository: coinsRepository, mm2Api: mm2Api, + sdk: sdk, ); } 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 63ab49685b..f2ebc0b077 100644 --- a/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart +++ b/lib/bloc/cex_market_data/price_chart/models/price_chart_interval.dart @@ -20,7 +20,7 @@ enum PriceChartPeriod { return '1M'; case PriceChartPeriod.oneYear: return '1Y'; - } + } } String get intervalString { @@ -35,6 +35,6 @@ enum PriceChartPeriod { return '1M'; case PriceChartPeriod.oneYear: return '1y'; - } + } } } 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 ec29f28b2c..1dc84f0d64 100644 --- a/lib/bloc/cex_market_data/price_chart/models/time_period.dart +++ b/lib/bloc/cex_market_data/price_chart/models/time_period.dart @@ -20,7 +20,7 @@ enum TimePeriod { return '1M'; case TimePeriod.oneYear: return '1Y'; - } + } } Duration get duration { @@ -35,6 +35,6 @@ enum TimePeriod { return const Duration(days: 30); case TimePeriod.oneYear: return const Duration(days: 365); - } + } } } diff --git a/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart b/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart index ba6af9ccb3..4669f51feb 100644 --- a/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart +++ b/lib/bloc/cex_market_data/profit_loss/demo_profit_loss_repository.dart @@ -1,5 +1,6 @@ import 'package:http/http.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/generator.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/mock_transaction_history_repository.dart'; @@ -26,6 +27,7 @@ class MockProfitLossRepository extends ProfitLossRepository { required PerformanceMode performanceMode, required CoinsRepo coinsRepository, required Mm2Api mm2Api, + required KomodoDefiSdk sdk, String cacheTableName = 'mock_profit_loss', }) { return MockProfitLossRepository( @@ -40,6 +42,7 @@ class MockProfitLossRepository extends ProfitLossRepository { client: Client(), performanceMode: performanceMode, demoDataGenerator: DemoDataCache.withDefaults(), + sdk: sdk, ), profitLossCalculator: RealisedProfitLossCalculator( BinanceRepository( diff --git a/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart b/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart index d44be74c54..1ebfbdd7ed 100644 --- a/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart +++ b/lib/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart @@ -1,4 +1,4 @@ -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; extension ProfitLossTransactionExtension on Transaction { /// The total amount of the coin transferred in the transaction as a double. diff --git a/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart b/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart index 882ff690cc..0e6079fcee 100644 --- a/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart +++ b/lib/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart @@ -1,5 +1,5 @@ import 'package:web_dex/bloc/cex_market_data/profit_loss/models/fiat_value.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; class PriceStampedTransaction extends Transaction { final FiatValue fiatValue; diff --git a/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart b/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart index 9ef2c79c84..e54c8e6b99 100644 --- a/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart +++ b/lib/bloc/cex_market_data/profit_loss/models/profit_loss.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/fiat_value.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Represents a profit/loss for a specific coin. class ProfitLoss extends Equatable { diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart index 6903b102ab..6e41f3e126 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart @@ -208,13 +208,14 @@ class ProfitLossBloc extends Bloc { useCache: useCache, ); - profitLosses.removeRange( - 0, - profitLosses.indexOf( - profitLosses.firstWhere((element) => element.profitLoss != 0), - ), + final startIndex = profitLosses.indexOf( + profitLosses.firstWhere((element) => element.profitLoss != 0), ); + if (startIndex == -1) { + profitLosses.removeRange(0, startIndex); + } + return profitLosses.toChartData(); } catch (e) { logger.log( diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart index 75045cd2e2..231b38b49a 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_calculator.dart @@ -2,7 +2,7 @@ import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/extensions/profit_loss_transaction_extension.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/price_stamped_transaction.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/models/profit_loss.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; class ProfitLossCalculator { ProfitLossCalculator(this._cexRepository); diff --git a/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart b/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart index 7cb387dbbb..7e63b92e8f 100644 --- a/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart +++ b/lib/bloc/cex_market_data/profit_loss/profit_loss_repository.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:hive/hive.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart' as cex; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_persistence_layer/komodo_persistence_layer.dart'; import 'package:web_dex/bloc/cex_market_data/charts.dart'; import 'package:web_dex/bloc/cex_market_data/mockup/performance_mode.dart'; @@ -57,6 +58,7 @@ class ProfitLossRepository { required cex.CexRepository cexRepository, required CoinsRepo coinsRepository, required Mm2Api mm2Api, + required KomodoDefiSdk sdk, PerformanceMode? demoMode, }) { if (demoMode != null) { @@ -65,6 +67,7 @@ class ProfitLossRepository { coinsRepository: coinsRepository, cacheTableName: 'mock_${cacheTableName}_${demoMode.name}', mm2Api: mm2Api, + sdk: sdk, ); } diff --git a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart index a4bc160d85..f9435f0a37 100644 --- a/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart +++ b/lib/bloc/coin_addresses/bloc/coin_addresses_bloc.dart @@ -5,7 +5,7 @@ import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_state.dart'; import 'package:web_dex/shared/utils/utils.dart'; class CoinAddressesBloc extends Bloc { - final KomodoDefiSdk? sdk; + final KomodoDefiSdk sdk; final String assetId; CoinAddressesBloc(this.sdk, this.assetId) @@ -16,54 +16,64 @@ class CoinAddressesBloc extends Bloc { } Future _onSubmitCreateAddress( - SubmitCreateAddressEvent event, Emitter emit) async { + SubmitCreateAddressEvent event, + Emitter emit, + ) async { emit(state.copyWith(createAddressStatus: () => FormStatus.submitting)); try { - if (sdk == null) { - throw Exception('Coin Addresses KDF SDK is null'); - } - - await sdk!.pubkeys.createNewPubkey(getSdkAsset(sdk, assetId)); + await sdk.pubkeys.createNewPubkey(getSdkAsset(sdk, assetId)); add(const LoadAddressesEvent()); - emit(state.copyWith( - createAddressStatus: () => FormStatus.success, - )); + emit( + state.copyWith( + createAddressStatus: () => FormStatus.success, + ), + ); } catch (e) { - emit(state.copyWith( - createAddressStatus: () => FormStatus.failure, - errorMessage: () => e.toString(), - )); + emit( + state.copyWith( + createAddressStatus: () => FormStatus.failure, + errorMessage: () => e.toString(), + ), + ); } } Future _onLoadAddresses( - LoadAddressesEvent event, Emitter emit) async { + LoadAddressesEvent event, + Emitter emit, + ) async { emit(state.copyWith(status: () => FormStatus.submitting)); try { final asset = getSdkAsset(sdk, assetId); - final addresses = (await asset.getPubkeys()).keys; + final addresses = (await asset.getPubkeys(sdk)).keys; final reasons = await asset.getCantCreateNewAddressReasons(sdk); - emit(state.copyWith( - status: () => FormStatus.success, - addresses: () => addresses, - cantCreateNewAddressReasons: () => reasons, - )); + emit( + state.copyWith( + status: () => FormStatus.success, + addresses: () => addresses, + cantCreateNewAddressReasons: () => reasons, + ), + ); } catch (e) { - emit(state.copyWith( - status: () => FormStatus.failure, - errorMessage: () => e.toString(), - )); + emit( + state.copyWith( + status: () => FormStatus.failure, + errorMessage: () => e.toString(), + ), + ); } } void _onUpdateHideZeroBalance( - UpdateHideZeroBalanceEvent event, Emitter emit) { + UpdateHideZeroBalanceEvent event, + Emitter emit, + ) { emit(state.copyWith(hideZeroBalance: () => event.hideZeroBalance)); } } diff --git a/lib/bloc/coins_bloc/asset_coin_extension.dart b/lib/bloc/coins_bloc/asset_coin_extension.dart index 902c02a4fc..2b3fb45b16 100644 --- a/lib/bloc/coins_bloc/asset_coin_extension.dart +++ b/lib/bloc/coins_bloc/asset_coin_extension.dart @@ -1,4 +1,5 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/coin_type.dart'; @@ -14,7 +15,10 @@ extension AssetCoinExtension on Asset { final CoinType? type = _getCoinTypeFromProtocol(protocol); if (type == null) { throw ArgumentError.value( - protocol.subClass, 'protocol type', 'Unsupported protocol type'); + protocol.subClass, + 'protocol type', + 'Unsupported protocol type', + ); } // temporary measure to get metadata, like `wallet_only`, that isn't exposed diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index d0ad4f5f7b..568da5f34b 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:http/http.dart' as http; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart' @@ -420,7 +419,7 @@ class CoinsRepo { } Future getBalanceInfo(String abbr) async { - final pubkeys = await getSdkAsset(_kdfSdk, abbr).getPubkeys(); + final pubkeys = await getSdkAsset(_kdfSdk, abbr).getPubkeys(_kdfSdk); return pubkeys.balance; } diff --git a/lib/bloc/dex_repository.dart b/lib/bloc/dex_repository.dart index 2c073ddc19..096fff1c14 100644 --- a/lib/bloc/dex_repository.dart +++ b/lib/bloc/dex_repository.dart @@ -37,8 +37,13 @@ class DexRepository { } Future> getTradePreimage( - String base, String rel, Rational price, String swapMethod, - [Rational? volume, bool max = false]) async { + String base, + String rel, + Rational price, + String swapMethod, [ + Rational? volume, + bool max = false, + ]) async { final request = TradePreimageRequest( base: base, rel: rel, @@ -48,7 +53,8 @@ class DexRepository { max: max, ); final ApiResponse> response = await _mm2Api.getTradePreimage(request); + Map> response = + await _mm2Api.getTradePreimage(request); final Map? error = response.error; final TradePreimageResponseResult? result = response.result; @@ -62,8 +68,11 @@ class DexRepository { } try { return DataFromService( - data: mapTradePreimageResponseResultToTradePreimage( - result, response.request)); + data: mapTradePreimageResponseResultToTradePreimage( + result, + response.request, + ), + ); } catch (e, s) { log( e.toString(), @@ -127,8 +136,10 @@ class DexRepository { log('Error parsing best_orders response: $e', trace: s, isError: true); return BestOrders( - error: TextError( - error: 'Something went wrong! Unexpected response format.')); + error: TextError( + error: 'Something went wrong! Unexpected response format.', + ), + ); } } diff --git a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart index f34e3aa544..3cc916900d 100644 --- a/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart +++ b/lib/bloc/fiat/fiat_onramp_form/fiat_form_state.dart @@ -42,32 +42,45 @@ final class FiatFormState extends Equatable with FormzMixin { /// The selected fiat currency to use to purchase [selectedCoin]. final CurrencyInput selectedFiat; + /// The selected crypto currency to purchase. final CurrencyInput selectedCoin; + /// The amount of [selectedFiat] to use to purchase [selectedCoin]. final FiatAmountInput fiatAmount; + /// The selected payment method to use to purchase [selectedCoin]. final FiatPaymentMethod selectedPaymentMethod; + /// The account reference to use to purchase [selectedCoin]. final String accountReference; + /// The crypto receive address to use to purchase [selectedCoin]. final String coinReceiveAddress; + /// The callback url to return to once checkout is completed. final String checkoutUrl; + /// The order id for the fiat purchase (Only supported by Banxa). final String orderId; + /// The current status of the form (loading, success, failure). final FiatFormStatus status; - /// The list of payment methods available for the [selectedFiat], + + /// The list of payment methods available for the [selectedFiat], /// [selectedCoin], and [fiatAmount]. final Iterable paymentMethods; + /// The list of fiat currencies that can be used to purchase [selectedCoin]. final Iterable fiatList; + /// The list of crypto currencies that can be purchased. final Iterable coinList; + /// The current status of the fiat order. final FiatOrderStatus fiatOrderStatus; - /// The current mode of the fiat form (onramp, offramp). This is currently + + /// The current mode of the fiat form (onramp, offramp). This is currently /// used to determine the tab to show. The implementation will likely change /// once the order history tab is implemented final FiatMode fiatMode; @@ -75,8 +88,10 @@ final class FiatFormState extends Equatable with FormzMixin { /// Gets the transaction limit from the selected payment method FiatTransactionLimit? get transactionLimit => selectedPaymentMethod.transactionLimits.firstOrNull; + /// The minimum fiat amount that is allowed for the selected payment method double? get minFiatAmount => transactionLimit?.min; + /// The maximum fiat amount that is allowed for the selected payment method double? get maxFiatAmount => transactionLimit?.max; bool get isLoadingCurrencies => fiatList.length < 2 || coinList.length < 2; @@ -84,7 +99,7 @@ final class FiatFormState extends Equatable with FormzMixin { bool get canSubmit => !isLoading && accountReference.isNotEmpty && - status != FiatFormStatus.failure && + status != FiatFormStatus.failure && !fiatOrderStatus.isSubmitting && isValid; diff --git a/lib/bloc/fiat/models/fiat_buy_order_info.dart b/lib/bloc/fiat/models/fiat_buy_order_info.dart index a4bdb67482..aad2288ab1 100644 --- a/lib/bloc/fiat/models/fiat_buy_order_info.dart +++ b/lib/bloc/fiat/models/fiat_buy_order_info.dart @@ -76,7 +76,7 @@ class FiatBuyOrderInfo extends Equatable { network: data['network'] as String? ?? '', paymentCode: data['payment_code'] as String? ?? '', checkoutUrl: data['checkout_url'] as String? ?? '', - createdAt: assertString(data['created_at']) ?? '', + createdAt: assertString(data['created_at']) ?? '', error: data['errors'] != null ? FiatBuyOrderError.fromJson(data['errors'] as Map) : const FiatBuyOrderError.none(), diff --git a/lib/bloc/fiat/models/fiat_order.dart b/lib/bloc/fiat/models/fiat_order.dart index e69de29bb2..8b13789179 100644 --- a/lib/bloc/fiat/models/fiat_order.dart +++ b/lib/bloc/fiat/models/fiat_order.dart @@ -0,0 +1 @@ + diff --git a/lib/bloc/fiat/models/i_currency.dart b/lib/bloc/fiat/models/i_currency.dart index 54c697ce62..983d1c19b1 100644 --- a/lib/bloc/fiat/models/i_currency.dart +++ b/lib/bloc/fiat/models/i_currency.dart @@ -10,11 +10,13 @@ abstract class ICurrency { /// Returns true if the currency is a fiat currency (e.g. USD) bool get isFiat; + /// Returns true if the currency is a crypto currency (e.g. BTC) bool get isCrypto; /// Returns the abbreviation of the currency (e.g. BTC, USD). String getAbbr() => symbol; + /// Returns the full name of the currency (e.g. Bitcoin). String formatNameShort() => name; } diff --git a/lib/bloc/transaction_history/transaction_history_bloc.dart b/lib/bloc/transaction_history/transaction_history_bloc.dart index eff4d271f0..072ece57d1 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -1,12 +1,13 @@ import 'dart:async'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_event.dart'; -import 'package:web_dex/bloc/transaction_history/transaction_history_repo.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/utils/utils.dart'; @@ -14,50 +15,129 @@ import 'package:web_dex/shared/utils/utils.dart'; class TransactionHistoryBloc extends Bloc { TransactionHistoryBloc({ - required TransactionHistoryRepo repo, - }) : _repo = repo, + required KomodoDefiSdk sdk, + }) : _sdk = sdk, super(const TransactionHistoryState.initial()) { - on(_onSubscribe); - on(_onUnsubscribe); + on(_onSubscribe, transformer: restartable()); on(_onStartedLoading); on(_onUpdated); on(_onFailure); } - final TransactionHistoryRepo _repo; - Timer? _updateTransactionsTimer; - final _updateTime = const Duration(seconds: 10); + final KomodoDefiSdk _sdk; + StreamSubscription>? _historySubscription; + StreamSubscription? _newTransactionsSubscription; + + // TODO: Remove or move to SDK + final Set _processedTxIds = {}; + + @override + Future close() async { + await _historySubscription?.cancel(); + await _newTransactionsSubscription?.cancel(); + return super.close(); + } Future _onSubscribe( TransactionHistorySubscribe event, Emitter emit, ) async { - if (!hasTxHistorySupport(event.coin)) { - return; - } + if (!hasTxHistorySupport(event.coin)) return; + + await _historySubscription?.cancel(); + await _newTransactionsSubscription?.cancel(); + _processedTxIds.clear(); + emit(const TransactionHistoryState.initial()); - await _update(event.coin); - _stopTimers(); - _updateTransactionsTimer = Timer.periodic(_updateTime, (_) async { - await _update(event.coin); - }); + + try { + add(const TransactionHistoryStartedLoading()); + final asset = getSdkAsset(_sdk, event.coin.abbr); + + // Subscribe to historical transactions + _historySubscription = + _sdk.transactions.getTransactionsStreamed(asset).listen( + (newTransactions) { + // Filter out any transactions we've already processed + final uniqueTransactions = newTransactions.where((tx) { + final isNew = !_processedTxIds.contains(tx.internalId); + if (isNew) { + _processedTxIds.add(tx.internalId); + } + return isNew; + }).toList(); + + if (uniqueTransactions.isEmpty) return; + + final updatedTransactions = List.of(state.transactions) + ..addAll(uniqueTransactions) + ..sort(_sortTransactions); + + if (event.coin.isErcType) { + _flagTransactions(updatedTransactions, event.coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, + onDone: () { + // Once historical load is complete, start watching for new transactions + _subscribeToNewTransactions(asset, event.coin); + }, + ); + } catch (e) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + } } - void _onUnsubscribe( - TransactionHistoryUnsubscribe event, - Emitter emit, - ) { - _stopTimers(); + void _subscribeToNewTransactions(Asset asset, Coin coin) { + _newTransactionsSubscription = + _sdk.transactions.watchTransactions(asset).listen( + (newTransaction) { + if (_processedTxIds.contains(newTransaction.internalId)) return; + + _processedTxIds.add(newTransaction.internalId); + + final updatedTransactions = List.of(state.transactions) + ..add(newTransaction) + ..sort(_sortTransactions); + + if (coin.isErcType) { + _flagTransactions(updatedTransactions, coin); + } + + add(TransactionHistoryUpdated(transactions: updatedTransactions)); + }, + onError: (error) { + add( + TransactionHistoryFailure( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ), + ); + }, + ); } void _onUpdated( TransactionHistoryUpdated event, Emitter emit, ) { - emit(state.copyWith( - transactions: event.transactions, - loading: false, - )); + emit( + state.copyWith( + transactions: event.transactions, + loading: false, + ), + ); } void _onStartedLoading( @@ -71,79 +151,126 @@ class TransactionHistoryBloc TransactionHistoryFailure event, Emitter emit, ) { - emit(state.copyWith( - loading: false, - error: event.error, - )); + emit( + state.copyWith( + loading: false, + error: event.error, + ), + ); } +} - Future _update(Coin coin) async { - if (isClosed) { - return; - } +int _sortTransactions(Transaction tx1, Transaction tx2) { + if (tx2.timestamp == DateTime.now()) { + return 1; + } else if (tx1.timestamp == DateTime.now()) { + return -1; + } + return tx2.timestamp.compareTo(tx1.timestamp); +} - try { - add(const TransactionHistoryStartedLoading()); - final transactions = await _repo.fetch(coin); - if (isClosed) { - return; - } +void _flagTransactions(List transactions, Coin coin) { + if (!coin.isErcType) return; + transactions + .removeWhere((tx) => tx.balanceChanges.totalAmount.toDouble() == 0.0); +} - if (transactions == null) { - add( - TransactionHistoryFailure( - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ), - ); - return; - } +class Pagination { + Pagination({ + this.fromId, + this.pageNumber, + }); + final String? fromId; + final int? pageNumber; - transactions.sort(_sortTransactions); - _flagTransactions(transactions, coin); + Map toJson() => { + if (fromId != null) 'FromId': fromId, + if (pageNumber != null) 'PageNumber': pageNumber, + }; +} - add(TransactionHistoryUpdated(transactions: transactions)); - } catch (e) { - add( - TransactionHistoryFailure( - error: TextError(error: LocaleKeys.somethingWrong.tr()), - ), - ); - return; - } - } +/// Represents different ways to paginate transaction history +sealed class TransactionPagination { + const TransactionPagination(); + + /// Get the limit of transactions to return, if applicable + int? get limit; +} + +/// Standard page-based pagination +class PagePagination extends TransactionPagination { + const PagePagination({ + required this.pageNumber, + required this.itemsPerPage, + }); + + final int pageNumber; + final int itemsPerPage; @override - Future close() { - _stopTimers(); + int get limit => itemsPerPage; +} - return super.close(); - } +/// Pagination from a specific transaction ID +class TransactionBasedPagination extends TransactionPagination { + const TransactionBasedPagination({ + required this.fromId, + required this.itemCount, + }); - void _stopTimers() { - _updateTransactionsTimer?.cancel(); - _updateTransactionsTimer = null; - } + final String fromId; + final int itemCount; + + @override + int get limit => itemCount; } -int _sortTransactions(Transaction tx1, Transaction tx2) { - if (tx2.timestamp == DateTime.fromMillisecondsSinceEpoch(0)) { - return 1; - } else if (tx1.timestamp == DateTime.fromMillisecondsSinceEpoch(0)) { - return -1; - } - return tx2.timestamp.compareTo(tx1.timestamp); +/// Pagination by block range +class BlockRangePagination extends TransactionPagination { + const BlockRangePagination({ + required this.fromBlock, + required this.toBlock, + this.maxItems, + }); + + final int fromBlock; + final int toBlock; + final int? maxItems; + + @override + int? get limit => maxItems; } -void _flagTransactions(List transactions, Coin coin) { - // First response to https://trezor.io/support/a/address-poisoning-attacks, - // need to be refactored. - // ref: https://github.com/KomodoPlatform/komodowallet/issues/1091 +/// Pagination by timestamp range +class TimestampRangePagination extends TransactionPagination { + const TimestampRangePagination({ + required this.fromTimestamp, + required this.toTimestamp, + this.maxItems, + }); - if (!coin.isErcType) return; + final DateTime fromTimestamp; + final DateTime toTimestamp; + final int? maxItems; - for (final Transaction tx in List.from(transactions)) { - if (tx.balanceChanges.totalAmount.toDouble() == 0.0) { - transactions.remove(tx); - } - } + @override + int? get limit => maxItems; +} + +/// Contract-specific pagination (e.g., for ERC20 token transfers) +class ContractEventPagination extends TransactionPagination { + const ContractEventPagination({ + required this.contractAddress, + required this.fromBlock, + this.toBlock, + this.maxItems, + }); + + final String contractAddress; + final int fromBlock; + final int? toBlock; + final int? maxItems; + + @override + int? get limit => maxItems; } diff --git a/lib/bloc/transaction_history/transaction_history_event.dart b/lib/bloc/transaction_history/transaction_history_event.dart index fd6ccb05bf..55343ed847 100644 --- a/lib/bloc/transaction_history/transaction_history_event.dart +++ b/lib/bloc/transaction_history/transaction_history_event.dart @@ -1,6 +1,6 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; abstract class TransactionHistoryEvent { const TransactionHistoryEvent(); @@ -11,11 +11,6 @@ class TransactionHistorySubscribe extends TransactionHistoryEvent { final Coin coin; } -class TransactionHistoryUnsubscribe extends TransactionHistoryEvent { - const TransactionHistoryUnsubscribe({required this.coin}); - final Coin coin; -} - class TransactionHistoryUpdated extends TransactionHistoryEvent { const TransactionHistoryUpdated({required this.transactions}); final List? transactions; diff --git a/lib/bloc/transaction_history/transaction_history_repo.dart b/lib/bloc/transaction_history/transaction_history_repo.dart index 3bc8ec7ba7..66e1f348f0 100644 --- a/lib/bloc/transaction_history/transaction_history_repo.dart +++ b/lib/bloc/transaction_history/transaction_history_repo.dart @@ -1,28 +1,35 @@ import 'dart:async'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; class TransactionHistoryRepo { TransactionHistoryRepo({ - KomodoDefiSdk? sdk, + required KomodoDefiSdk sdk, }) : _sdk = sdk; - final KomodoDefiSdk? _sdk; + final KomodoDefiSdk _sdk; - Future?> fetch(Coin coin) async { + Future?> fetch(Coin coin, [String? fromId]) async { final asset = getSdkAsset(_sdk, coin.abbr); try { - final transactionHistory = await _sdk?.transactions.getTransactionHistory( + final transactionHistory = await _sdk.transactions.getTransactionHistory( asset, - pagination: const PagePagination( - pageNumber: 1, - itemsPerPage: 200, - ), + pagination: fromId == null + ? const PagePagination( + pageNumber: 1, + // TODO: Handle cases with more than 2000 transactions and/or + // adopt a pagination strategy. Migrate form + itemsPerPage: 2000, + ) + : TransactionBasedPagination( + fromId: fromId, + itemCount: 2000, + ), ); - return transactionHistory?.transactions; + return transactionHistory.transactions; } catch (e) { return null; } diff --git a/lib/bloc/transaction_history/transaction_history_state.dart b/lib/bloc/transaction_history/transaction_history_state.dart index 0db2ff8488..7bd23d7d61 100644 --- a/lib/bloc/transaction_history/transaction_history_state.dart +++ b/lib/bloc/transaction_history/transaction_history_state.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; final class TransactionHistoryState extends Equatable { const TransactionHistoryState({ diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index 6b0bbc1a35..045d4e8c81 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -1,617 +1,493 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_event.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_state.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/convert_address/convert_address_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/trezor/withdraw/trezor_withdraw/trezor_withdraw_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/validateaddress/validateaddress_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/fee/fee_request.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_request.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; -import 'package:web_dex/model/fee_type.dart'; -import 'package:web_dex/model/hw_wallet/trezor_progress_status.dart'; import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; -import 'package:web_dex/shared/utils/utils.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_event.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_state.dart'; export 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; +import 'package:decimal/decimal.dart'; + class WithdrawFormBloc extends Bloc { + final KomodoDefiSdk _sdk; + WithdrawFormBloc({ - required Coin coin, - required CoinsRepo coinsRepository, - required Mm2Api api, - required this.goBack, - }) : _coinsRepo = coinsRepository, - _mm2Api = api, - super(WithdrawFormState.initial(coin, coinsRepository)) { - on(_onAddressChanged); + required Asset asset, + required KomodoDefiSdk sdk, + }) : _sdk = sdk, + super( + WithdrawFormState( + asset: asset, + step: WithdrawFormStep.fill, + recipientAddress: '', + amount: '0', + ), + ) { + on(_onRecipientChanged); on(_onAmountChanged); - on(_onCustomFeeChanged); - on(_onCustomEvmFeeChanged); - on(_onSenderAddressChanged); - on(_onMaxTapped); - on(_onSubmitted); - on(_onWithdrawSuccess); - on(_onWithdrawFailed); - on(_onSendRawTransaction); + on(_onSourceChanged); + on(_onMaxAmountEnabled); on(_onCustomFeeEnabled); - on(_onCustomFeeDisabled); - on(_onConvertMixedCaseAddress); - on(_onStepReverted); - on(_onWithdrawFormReset); - on(_onTrezorProgressUpdated); - on(_onMemoUpdated); + on(_onFeeChanged); + on(_onMemoChanged); + on(_onIbcTransferEnabled); + on(_onIbcChannelChanged); + on(_onPreviewSubmitted); + on(_onSubmitted); + on(_onCancelled); + on(_onReset); + on(_onSourcesLoadRequested); + on(_onConvertAddress); + + add(const WithdrawFormSourcesLoadRequested()); } - // will use actual CoinsRepo when implemented - final CoinsRepo _coinsRepo; - final Mm2Api _mm2Api; - final VoidCallback goBack; + Future _onSourcesLoadRequested( + WithdrawFormSourcesLoadRequested event, + Emitter emit, + ) async { + try { + final pubkeys = await state.asset.getPubkeys(_sdk); + if (pubkeys.keys.isNotEmpty) { + emit( + state.copyWith( + pubkeys: () => pubkeys, + networkError: () => null, + selectedSourceAddress: state.selectedSourceAddress == null + ? null + : () => pubkeys.keys.firstOrNull, + ), + ); + } else { + emit( + state.copyWith( + networkError: () => TextError( + error: 'No addresses found for ${state.asset.id.name}', + ), + ), + ); + } + } catch (e) { + emit( + state.copyWith( + networkError: () => TextError(error: 'Failed to load addresses: $e'), + ), + ); + } + } - // Event handlers - void _onAddressChanged( - WithdrawFormAddressChanged event, - Emitter emitter, - ) { - emitter(state.copyWith(address: event.address)); + FeeInfo? _getDefaultFee() { + final protocol = state.asset.protocol; + if (protocol is Erc20Protocol) { + return FeeInfo.ethGas( + coin: state.asset.id.id, + gasPrice: Decimal.one, + gas: 21000, + ); + } else if (protocol is UtxoProtocol) { + return FeeInfo.utxoFixed( + coin: state.asset.id.id, + amount: Decimal.fromInt(protocol.txFee ?? 10000), + ); + } + return null; + } + + Future _onRecipientChanged( + WithdrawFormRecipientChanged event, + Emitter emit, + ) async { + try { + final validation = await _sdk.addresses.validateAddress( + asset: state.asset, + address: event.address, + ); + + if (!validation.isValid) { + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: validation.invalidReason ?? 'Invalid address'), + isMixedCaseAddress: false, + ), + ); + return; + } + + // Check for mixed case if EVM address + if (state.asset.protocol is Erc20Protocol) { + try { + // Try to convert to checksum to detect mixed case + final result = await _sdk.addresses.convertFormat( + asset: state.asset, + address: event.address, + format: const AddressFormat(format: 'checksummed', network: ''), + ); + + final isMixedCase = result.convertedAddress != event.address; + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => null, + isMixedCaseAddress: isMixedCase, + ), + ); + return; + } catch (e) { + // If conversion fails, treat as normal address validation error + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: 'Invalid EVM address: $e'), + isMixedCaseAddress: false, + ), + ); + return; + } + } + + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => null, + isMixedCaseAddress: false, + ), + ); + } catch (e) { + emit( + state.copyWith( + recipientAddress: event.address, + recipientAddressError: () => + TextError(error: 'Address validation failed: $e'), + isMixedCaseAddress: false, + ), + ); + } } void _onAmountChanged( WithdrawFormAmountChanged event, - Emitter emitter, + Emitter emit, ) { - emitter(state.copyWith(amount: event.amount, isMaxAmount: false)); - } + if (state.isMaxAmount) return; + + try { + final amount = Decimal.parse(event.amount); + final balance = state.selectedSourceAddress?.balance.spendable ?? + state.pubkeys?.balance.spendable; + + if (balance != null && amount > balance) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => TextError(error: 'Insufficient funds'), + ), + ); + return; + } - void _onCustomFeeEnabled( - WithdrawFormCustomFeeEnabled event, - Emitter emitter, - ) { - emitter(state.copyWith( - isCustomFeeEnabled: true, customFee: FeeRequest(type: _customFeeType))); - } + if (amount <= Decimal.zero) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => + TextError(error: 'Amount must be greater than 0'), + ), + ); + return; + } - void _onCustomFeeDisabled( - WithdrawFormCustomFeeDisabled event, - Emitter emitter, - ) { - emitter(state.copyWith( - isCustomFeeEnabled: false, - customFee: FeeRequest(type: _customFeeType), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - )); + emit( + state.copyWith( + amount: event.amount, + amountError: () => null, + ), + ); + } catch (e) { + emit( + state.copyWith( + amount: event.amount, + amountError: () => TextError(error: 'Invalid amount'), + ), + ); + } } - void _onCustomFeeChanged( - WithdrawFormCustomFeeChanged event, - Emitter emitter, + void _onSourceChanged( + WithdrawFormSourceChanged event, + Emitter emit, ) { - emitter(state.copyWith( - customFee: FeeRequest( - type: feeType.utxoFixed, - amount: event.amount, + emit( + state.copyWith( + selectedSourceAddress: () => event.address, + networkError: () => null, ), - )); + ); } - void _onCustomEvmFeeChanged( - WithdrawFormCustomEvmFeeChanged event, - Emitter emitter, + void _onMaxAmountEnabled( + WithdrawFormMaxAmountEnabled event, + Emitter emit, ) { - emitter(state.copyWith( - customFee: FeeRequest( - type: feeType.ethGas, - gas: event.gas, - gasPrice: event.gasPrice, + final balance = + state.selectedSourceAddress?.balance ?? state.pubkeys?.balance; + emit( + state.copyWith( + isMaxAmount: event.isEnabled, + amount: event.isEnabled ? balance?.spendable.toString() : '0', + amountError: () => null, ), - )); + ); } - void _onSenderAddressChanged( - WithdrawFormSenderAddressChanged event, - Emitter emitter, + void _onCustomFeeEnabled( + WithdrawFormCustomFeeEnabled event, + Emitter emit, ) { - emitter( + // If enabling custom fees, set a default fee or reuse from `_getDefaultFee()` + emit( state.copyWith( - selectedSenderAddress: event.address, - amount: state.isMaxAmount - ? doubleToString( - state.coin.getHdAddress(event.address)?.balance.spendable ?? - 0.0) - : state.amount, + isCustomFeeEnabled: event.isEnabled, + customFee: event.isEnabled ? () => _getDefaultFee() : () => null, + customFeeError: () => null, ), ); } - void _onMaxTapped( - WithdrawFormMaxTapped event, - Emitter emitter, + void _onFeeChanged( + WithdrawFormCustomFeeChanged event, + Emitter emit, ) { - emitter(state.copyWith( - amount: event.isEnabled ? doubleToString(state.senderAddressBalance) : '', - isMaxAmount: event.isEnabled, - )); - } - - Future _onSubmitted( - WithdrawFormSubmitted event, - Emitter emitter, - ) async { - if (state.isSending) return; - emitter(state.copyWith( - isSending: true, - trezorProgressStatus: null, - sendError: TextError.empty(), - amountError: TextError.empty(), - addressError: TextError.empty(), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - )); - - bool isValid = await _validateEnterForm(emitter); - if (!isValid) { - return; - } - - isValid = await _additionalValidate(emitter); - if (!isValid) { - emitter(state.copyWith(isSending: false)); - return; + try { + // Validate the new fee, e.g. if it's EthGas => check gasPrice, gas > 0, etc. + if (event.fee is FeeInfoEthGas) { + _validateEvmFee(event.fee as FeeInfoEthGas); + } else if (event.fee is FeeInfoUtxoFixed) { + _validateUtxoFee(event.fee as FeeInfoUtxoFixed); + } + emit( + state.copyWith( + customFee: () => event.fee, + customFeeError: () => null, + ), + ); + } catch (e) { + emit( + state.copyWith( + customFeeError: () => TextError(error: e.toString()), + ), + ); } + } - final withdrawResponse = state.coin.enabledType == WalletType.trezor - ? await _coinsRepo.trezor.withdraw( - TrezorWithdrawRequest( - coin: state.coin, - from: state.selectedSenderAddress, - to: state.address, - amount: state.isMaxAmount - ? state.senderAddressBalance - : double.parse(state.amount), - max: state.isMaxAmount, - fee: state.isCustomFeeEnabled ? state.customFee : null, - ), - onProgressUpdated: (TrezorProgressStatus? status) { - add(WithdrawFormTrezorStatusUpdated(status: status)); - }, - ) - : await _coinsRepo.withdraw(WithdrawRequest( - to: state.address, - coin: state.coin.abbr, - max: state.isMaxAmount, - amount: state.isMaxAmount ? null : state.amount, - memo: state.memo, - fee: state.isCustomFeeEnabled - ? state.customFee - : state.coin.type == CoinType.cosmos || - state.coin.type == CoinType.iris - ? FeeRequest( - type: feeType.cosmosGas, - gasLimit: 150000, - gasPrice: 0.05, - ) - : null, - )); - - final BaseError? error = withdrawResponse.error; - final WithdrawDetails? result = withdrawResponse.result; - - if (error != null) { - add(WithdrawFormWithdrawFailed(error: error)); - log('WithdrawFormBloc: withdraw error: ${error.message}', isError: true); - return; + void _validateEvmFee(FeeInfoEthGas fee) { + if (fee.gasPrice <= Decimal.zero) { + throw Exception('Gas price must be greater than 0'); } - - if (result == null) { - emitter(state.copyWith( - sendError: TextError(error: LocaleKeys.somethingWrong.tr()), - isSending: false, - )); - return; + if (fee.gas <= 0) { + throw Exception('Gas limit must be greater than 0'); } - - add(WithdrawFormWithdrawSuccessful(details: result)); } - void _onWithdrawSuccess( - WithdrawFormWithdrawSuccessful event, - Emitter emitter, - ) { - emitter(state.copyWith( - isSending: false, - withdrawDetails: event.details, - step: WithdrawFormStep.confirm, - )); + void _validateUtxoFee(FeeInfoUtxoFixed fee) { + if (fee.amount <= Decimal.zero) { + throw Exception('Fee amount must be greater than 0'); + } } - void _onWithdrawFailed( - WithdrawFormWithdrawFailed event, - Emitter emitter, + void _onMemoChanged( + WithdrawFormMemoChanged event, + Emitter emit, ) { - final error = event.error; - - emitter(state.copyWith( - sendError: error, - isSending: false, - step: WithdrawFormStep.failed, - )); + emit(state.copyWith(memo: () => event.memo)); } - void _onTrezorProgressUpdated( - WithdrawFormTrezorStatusUpdated event, - Emitter emitter, + void _onIbcTransferEnabled( + WithdrawFormIbcTransferEnabled event, + Emitter emit, ) { - String? message; - - switch (event.status) { - case TrezorProgressStatus.waitingForUserToConfirmSigning: - message = LocaleKeys.confirmOnTrezor.tr(); - break; - default: - } - - if (state.trezorProgressStatus != message) { - emitter(state.copyWith(trezorProgressStatus: message)); - } - } - - Future _onConvertMixedCaseAddress( - WithdrawFormConvertAddress event, - Emitter emitter, - ) async { - final result = await _mm2Api.convertLegacyAddress( - ConvertAddressRequest( - coin: state.coin.abbr, - from: state.address, - isErc: state.coin.isErcType, + emit( + state.copyWith( + isIbcTransfer: event.isEnabled, + ibcChannel: event.isEnabled ? () => state.ibcChannel : () => null, + ibcChannelError: () => null, ), ); - - add(WithdrawFormAddressChanged(address: result ?? '')); } - Future _onSendRawTransaction( - WithdrawFormSendRawTx event, - Emitter emitter, - ) async { - if (state.isSending) return; - emitter(state.copyWith(isSending: true, sendError: TextError.empty())); - final BaseError? parentCoinError = _checkParentCoinErrors( - coin: state.coin, - fee: state.withdrawDetails.feeValue, - ); - if (parentCoinError != null) { - emitter(state.copyWith( - isSending: false, - sendError: parentCoinError, - )); + void _onIbcChannelChanged( + WithdrawFormIbcChannelChanged event, + Emitter emit, + ) { + if (event.channel.isEmpty) { + emit( + state.copyWith( + ibcChannel: () => event.channel, + ibcChannelError: () => TextError(error: 'Channel ID is required'), + ), + ); return; } - final response = await _mm2Api.sendRawTransaction( - SendRawTransactionRequest( - coin: state.withdrawDetails.coin, - txHex: state.withdrawDetails.txHex, + emit( + state.copyWith( + ibcChannel: () => event.channel, + ibcChannelError: () => null, ), ); - - final BaseError? responseError = response.error; - final String? txHash = response.txHash; - - if (responseError != null) { - log( - 'WithdrawFormBloc: sendRawTransaction error: ${responseError.message}', - isError: true, - ); - emitter(state.copyWith( - isSending: false, - sendError: responseError, - step: WithdrawFormStep.failed, - )); - return; - } - - if (txHash == null) { - emitter(state.copyWith( - isSending: false, - sendError: TextError(error: LocaleKeys.somethingWrong.tr()), - step: WithdrawFormStep.failed, - )); - return; - } - emitter(state.copyWith(step: WithdrawFormStep.success)); } - void _onStepReverted( - WithdrawFormStepReverted event, - Emitter emitter, - ) { - if (event.step == WithdrawFormStep.confirm) { - emitter( + Future _onPreviewSubmitted( + WithdrawFormPreviewSubmitted event, + Emitter emit, + ) async { + if (state.hasValidationErrors) return; + + try { + emit( state.copyWith( - step: WithdrawFormStep.fill, - withdrawDetails: WithdrawDetails.empty(), + isSending: true, + previewError: () => null, ), ); - } - } - - void _onMemoUpdated( - WithdrawFormMemoUpdated event, - Emitter emitter, - ) { - emitter(state.copyWith(memo: event.text)); - } - - void _onWithdrawFormReset( - WithdrawFormReset event, - Emitter emitter, - ) { - emitter(WithdrawFormState.initial(state.coin, _coinsRepo)); - } - - String get _customFeeType => - state.coin.type == CoinType.smartChain || state.coin.type == CoinType.utxo - ? feeType.utxoFixed - : feeType.ethGas; - - // Validators - Future _additionalValidate(Emitter emitter) async { - final BaseError? parentCoinError = _checkParentCoinErrors(coin: state.coin); - if (parentCoinError != null) { - emitter(state.copyWith(sendError: parentCoinError)); - return false; - } - return true; - } - - Future _validateEnterForm(Emitter emitter) async { - final bool isAddressValid = await _validateAddress(emitter); - final bool isAmountValid = _validateAmount(emitter); - final bool isCustomFeeValid = _validateCustomFee(emitter); - return isAddressValid && isAmountValid && isCustomFeeValid; - } - - Future _validateAddress(Emitter emitter) async { - final String address = state.address; - if (address.isEmpty) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - if (state.coin.enabledType == WalletType.trezor && - state.selectedSenderAddress.isEmpty) { - emitter(state.copyWith( - isSending: false, - addressError: TextError(error: LocaleKeys.noSenderAddress.tr()), - )); - return false; - } - - final Map? validateRawResponse = - await _mm2Api.validateAddress( - state.coin.abbr, - state.address, - ); - if (validateRawResponse == null) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - - final ValidateAddressResponse validateResponse = - ValidateAddressResponse.fromJson(validateRawResponse); - - final reason = validateResponse.reason ?? ''; - final isNonMixed = _isErcNonMixedCase(reason); - final isValid = validateResponse.isValid; - - if (isNonMixed) { - emitter(state.copyWith( - isSending: false, - addressError: MixedCaseAddressError(), - )); - return false; - } else if (!isValid) { - emitter(state.copyWith( - isSending: false, - addressError: TextError( - error: LocaleKeys.invalidAddress.tr(args: [state.coin.abbr])), - )); - return false; - } - - emitter(state.copyWith( - addressError: TextError.empty(), - amountError: state.amountError, - )); - return true; - } - - bool _isErcNonMixedCase(String error) { - if (!state.coin.isErcType) return false; - if (!error.contains(LocaleKeys.invalidAddressChecksum.tr())) return false; - return true; - } + final preview = await _sdk.withdrawals.previewWithdrawal( + state.toWithdrawParameters(), + ); - bool _validateAmount(Emitter emitter) { - if (state.amount.isEmpty) { - emitter(state.copyWith( + emit( + state.copyWith( + preview: () => preview, + step: WithdrawFormStep.confirm, isSending: false, - amountError: TextError( - error: LocaleKeys.enterAmountToSend.tr(args: [state.coin.abbr]), - ))); - return false; - } - final double? parsedValue = double.tryParse(state.amount); - - if (parsedValue == null) { - emitter(state.copyWith( + ), + ); + } catch (e) { + emit( + state.copyWith( + previewError: () => + TextError(error: 'Failed to generate preview: $e'), isSending: false, - amountError: TextError( - error: LocaleKeys.enterAmountToSend.tr(args: [state.coin.abbr]), - ))); - return false; + ), + ); } + } - if (parsedValue == 0) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.inferiorSendAmount.tr(args: [state.coin.abbr]), - ))); - return false; - } + Future _onSubmitted( + WithdrawFormSubmitted event, + Emitter emit, + ) async { + if (state.hasValidationErrors) return; - final double formattedBalance = - double.parse(doubleToString(state.senderAddressBalance)); + try { + emit( + state.copyWith( + isSending: true, + transactionError: () => null, + ), + ); - if (parsedValue > formattedBalance) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.notEnoughBalance.tr(), - ))); - return false; - } + await for (final progress in _sdk.withdrawals.withdraw( + state.toWithdrawParameters(), + )) { + if (progress.status == WithdrawalStatus.complete) { + emit( + state.copyWith( + step: WithdrawFormStep.success, + result: () => progress.withdrawalResult, + isSending: false, + ), + ); + return; + } - if (state.isCustomFeeEnabled && - !state.isMaxAmount && - state.customFee.type == feeType.utxoFixed) { - final double feeValue = - double.tryParse(state.customFee.amount ?? '0.0') ?? 0.0; - if ((parsedValue + feeValue) > formattedBalance) { - emitter(state.copyWith( - isSending: false, - amountError: TextError( - error: LocaleKeys.notEnoughBalance.tr(), - ))); - return false; + if (progress.status == WithdrawalStatus.error) { + throw Exception(progress.errorMessage); + } } - } - - return true; - } - - bool _validateCustomFee(Emitter emitter) { - final customFee = state.customFee; - if (!state.isCustomFeeEnabled) { - return true; - } - if (customFee.type == feeType.utxoFixed) { - return _validateUtxoCustomFee(emitter); - } - if (customFee.type == feeType.ethGas) { - return _validateEvmCustomFee(emitter); - } - return true; - } - - bool _validateUtxoCustomFee(Emitter emitter) { - final value = state.customFee.amount; - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + } catch (e) { + emit( + state.copyWith( + transactionError: () => TextError(error: 'Transaction failed: $e'), + step: WithdrawFormStep.failed, isSending: false, - utxoCustomFeeError: - TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; + ), + ); } - final double amountToSend = state.amountToSendDouble; + } - if (feeAmount > amountToSend) { - emitter(state.copyWith( - isSending: false, - utxoCustomFeeError: - TextError(error: LocaleKeys.customFeeHigherAmount.tr()))); - return false; - } + void _onCancelled( + WithdrawFormCancelled event, + Emitter emit, + ) { + // TODO: Cancel withdrawal if in progress - return true; + add(const WithdrawFormReset()); } - bool _validateEvmCustomFee(Emitter emitter) { - final bool isGasLimitValid = _gasLimitValidator(emitter); - final bool isGasPriceValid = _gasPriceValidator(emitter); - return isGasLimitValid && isGasPriceValid; + void _onReset( + WithdrawFormReset event, + Emitter emit, + ) { + emit( + WithdrawFormState( + asset: state.asset, + step: WithdrawFormStep.fill, + recipientAddress: '', + amount: '0', + pubkeys: state.pubkeys, + selectedSourceAddress: state.pubkeys?.keys.first, + ), + ); } - BaseError? _checkParentCoinErrors({required Coin? coin, String? fee}) { - final Coin? parentCoin = coin?.parentCoin; - if (parentCoin == null) return null; + bool _hasEthAddressMixedCase(String address) { + if (!address.startsWith('0x')) return false; + final chars = address.substring(2).split(''); + return chars.any((c) => c.toLowerCase() != c) && + chars.any((c) => c.toUpperCase() != c); + } - if (!parentCoin.isActive) { - return TextError( - error: - LocaleKeys.withdrawNoParentCoinError.tr(args: [parentCoin.abbr])); - } + Future _onConvertAddress( + WithdrawFormConvertAddressRequested event, + Emitter emit, + ) async { + if (!state.isMixedCaseAddress) return; - final double balance = parentCoin.balance; + try { + emit(state.copyWith(isSending: true)); - if (balance == 0) { - return TextError( - error: LocaleKeys.withdrawTopUpBalanceError.tr(args: [parentCoin.abbr]), - ); - } else if (fee != null && parentCoin.balance < double.parse(fee)) { - return TextError( - error: LocaleKeys.withdrawNotEnoughBalanceForGasError - .tr(args: [parentCoin.abbr]), + // For EVM addresses, we want to convert to checksum format + final result = await _sdk.addresses.convertFormat( + asset: state.asset, + address: state.recipientAddress, + format: const AddressFormat(format: 'checksummed', network: ''), ); - } - - return null; - } - bool _gasLimitValidator(Emitter emitter) { - final value = state.customFee.gas.toString(); - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + emit( + state.copyWith( + recipientAddress: result.convertedAddress, + isMixedCaseAddress: false, + recipientAddressError: () => null, isSending: false, - gasLimitError: TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; - } - return true; - } - - bool _gasPriceValidator(Emitter emitter) { - final value = state.customFee.gasPrice; - final double? feeAmount = _valueToAmount(value); - if (feeAmount == null || feeAmount < 0) { - emitter(state.copyWith( + ), + ); + } catch (e) { + emit( + state.copyWith( + recipientAddressError: () => + TextError(error: 'Failed to convert address: $e'), isSending: false, - gasPriceError: TextError(error: LocaleKeys.pleaseInputData.tr()))); - return false; + ), + ); } - return true; - } - - double? _valueToAmount(String? value) { - if (value == null) return null; - value = value.replaceAll(',', '.'); - return double.tryParse(value); } } diff --git a/lib/bloc/withdraw_form/withdraw_form_event.dart b/lib/bloc/withdraw_form/withdraw_form_event.dart index 4a8982ae8b..54ed172596 100644 --- a/lib/bloc/withdraw_form/withdraw_form_event.dart +++ b/lib/bloc/withdraw_form/withdraw_form_event.dart @@ -1,131 +1,78 @@ -import 'package:equatable/equatable.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/model/hw_wallet/trezor_progress_status.dart'; -import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; -abstract class WithdrawFormEvent extends Equatable { +sealed class WithdrawFormEvent { const WithdrawFormEvent(); - - @override - List get props => []; } -class WithdrawFormAddressChanged extends WithdrawFormEvent { - const WithdrawFormAddressChanged({required this.address}); +class WithdrawFormRecipientChanged extends WithdrawFormEvent { final String address; - - @override - List get props => [address]; + const WithdrawFormRecipientChanged(this.address); } class WithdrawFormAmountChanged extends WithdrawFormEvent { - const WithdrawFormAmountChanged({required this.amount}); - final String amount; - - @override - List get props => [amount]; -} - -class WithdrawFormCustomFeeChanged extends WithdrawFormEvent { - const WithdrawFormCustomFeeChanged({required this.amount}); final String amount; - - @override - List get props => [amount]; + const WithdrawFormAmountChanged(this.amount); } -class WithdrawFormCustomEvmFeeChanged extends WithdrawFormEvent { - const WithdrawFormCustomEvmFeeChanged({this.gasPrice, this.gas}); - final String? gasPrice; - final int? gas; - - @override - List get props => [gasPrice, gas]; +class WithdrawFormSourceChanged extends WithdrawFormEvent { + final PubkeyInfo address; + const WithdrawFormSourceChanged(this.address); } -class WithdrawFormSenderAddressChanged extends WithdrawFormEvent { - const WithdrawFormSenderAddressChanged({required this.address}); - final String address; - - @override - List get props => [address]; +class WithdrawFormMaxAmountEnabled extends WithdrawFormEvent { + final bool isEnabled; + const WithdrawFormMaxAmountEnabled(this.isEnabled); } -class WithdrawFormMaxTapped extends WithdrawFormEvent { - const WithdrawFormMaxTapped({required this.isEnabled}); +class WithdrawFormCustomFeeEnabled extends WithdrawFormEvent { final bool isEnabled; - - @override - List get props => [isEnabled]; + const WithdrawFormCustomFeeEnabled(this.isEnabled); } -class WithdrawFormWithdrawSuccessful extends WithdrawFormEvent { - const WithdrawFormWithdrawSuccessful({required this.details}); - final WithdrawDetails details; - - @override - List get props => [details]; +class WithdrawFormCustomFeeChanged extends WithdrawFormEvent { + final FeeInfo fee; + const WithdrawFormCustomFeeChanged(this.fee); } -class WithdrawFormWithdrawFailed extends WithdrawFormEvent { - const WithdrawFormWithdrawFailed({required this.error}); - final BaseError error; - - @override - List get props => [error]; +class WithdrawFormMemoChanged extends WithdrawFormEvent { + final String? memo; + const WithdrawFormMemoChanged(this.memo); } -class WithdrawFormTrezorStatusUpdated extends WithdrawFormEvent { - const WithdrawFormTrezorStatusUpdated({required this.status}); - final TrezorProgressStatus? status; - - @override - List get props => [status]; +class WithdrawFormPreviewSubmitted extends WithdrawFormEvent { + const WithdrawFormPreviewSubmitted(); } -class WithdrawFormSendRawTx extends WithdrawFormEvent { - const WithdrawFormSendRawTx(); - - @override - List get props => []; +class WithdrawFormSubmitted extends WithdrawFormEvent { + const WithdrawFormSubmitted(); } -class WithdrawFormCustomFeeDisabled extends WithdrawFormEvent { - const WithdrawFormCustomFeeDisabled(); - - @override - List get props => []; +class WithdrawFormCancelled extends WithdrawFormEvent { + const WithdrawFormCancelled(); } -class WithdrawFormCustomFeeEnabled extends WithdrawFormEvent { - const WithdrawFormCustomFeeEnabled(); - - @override - List get props => []; +class WithdrawFormReset extends WithdrawFormEvent { + const WithdrawFormReset(); } -class WithdrawFormConvertAddress extends WithdrawFormEvent { - const WithdrawFormConvertAddress(); - - @override - List get props => []; +class WithdrawFormIbcTransferEnabled extends WithdrawFormEvent { + final bool isEnabled; + WithdrawFormIbcTransferEnabled(this.isEnabled); } -class WithdrawFormSubmitted extends WithdrawFormEvent { - const WithdrawFormSubmitted(); +class WithdrawFormIbcChannelChanged extends WithdrawFormEvent { + final String channel; + WithdrawFormIbcChannelChanged(this.channel); } -class WithdrawFormReset extends WithdrawFormEvent { - const WithdrawFormReset(); +class WithdrawFormSourcesLoadRequested extends WithdrawFormEvent { + const WithdrawFormSourcesLoadRequested(); } class WithdrawFormStepReverted extends WithdrawFormEvent { - const WithdrawFormStepReverted({required this.step}); - final WithdrawFormStep step; + const WithdrawFormStepReverted(); } -class WithdrawFormMemoUpdated extends WithdrawFormEvent { - const WithdrawFormMemoUpdated({required this.text}); - final String? text; +class WithdrawFormConvertAddressRequested extends WithdrawFormEvent { + const WithdrawFormConvertAddressRequested(); } diff --git a/lib/bloc/withdraw_form/withdraw_form_state.dart b/lib/bloc/withdraw_form/withdraw_form_state.dart index 92801d8d14..9fe0f9a004 100644 --- a/lib/bloc/withdraw_form/withdraw_form_state.dart +++ b/lib/bloc/withdraw_form/withdraw_form_state.dart @@ -1,207 +1,198 @@ +import 'package:decimal/decimal.dart'; import 'package:equatable/equatable.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:flutter/foundation.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_step.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/withdraw/fee/fee_request.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/hd_account/hd_account.dart'; import 'package:web_dex/model/text_error.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/model/withdraw_details/withdraw_details.dart'; -import 'package:web_dex/shared/utils/utils.dart'; class WithdrawFormState extends Equatable { + final Asset asset; + final AssetPubkeys? pubkeys; + final WithdrawFormStep step; + + // Form fields + final String recipientAddress; + final String amount; + final PubkeyInfo? selectedSourceAddress; + final bool isMaxAmount; + final bool isCustomFee; + final bool isCustomFeeEnabled; + final FeeInfo? customFee; + final String? memo; + final bool isIbcTransfer; + final String? ibcChannel; + + // Transaction state + final WithdrawalPreview? preview; + final bool isSending; + final WithdrawalResult? result; + + // Validation errors + final TextError? recipientAddressError; // Basic address validation + final bool isMixedCaseAddress; // EVM mixed case specific error + final TextError? amountError; // Amount validation (insufficient funds etc) + final TextError? customFeeError; // Fee validation for custom fees + final TextError? ibcChannelError; // IBC channel validation + + // Network/Transaction errors + final TextError? previewError; // Errors during preview generation + final TextError? transactionError; // Errors during transaction submission + final TextError? networkError; // Network connectivity errors + + bool get isCustomFeeSupported => + asset.protocol is UtxoProtocol || asset.protocol is Erc20Protocol; + + bool get hasPreviewError => previewError != null; + bool get hasTransactionError => transactionError != null; + bool get hasAddressError => + recipientAddressError != null || isMixedCaseAddress; + bool get hasValidationErrors => + hasAddressError || + amountError != null || + customFeeError != null || + ibcChannelError != null; + const WithdrawFormState({ - required this.coin, + required this.asset, + this.pubkeys, required this.step, - required this.address, + required this.recipientAddress, required this.amount, - required this.senderAddresses, - required this.selectedSenderAddress, - required bool isMaxAmount, - required this.customFee, - required this.withdrawDetails, - required this.isSending, - required this.trezorProgressStatus, - required this.sendError, - required this.addressError, - required this.amountError, - required this.utxoCustomFeeError, - required this.gasLimitError, - required this.gasPriceError, - required this.isCustomFeeEnabled, - required this.memo, - required CoinsRepo coinsRepository, - }) : _isMaxAmount = isMaxAmount, - _coinsRepo = coinsRepository; - - static WithdrawFormState initial(Coin coin, CoinsRepo coinsRepository) { - final List initSenderAddresses = coin.nonEmptyHdAddresses(); - final String selectedSenderAddress = - initSenderAddresses.isNotEmpty ? initSenderAddresses.first.address : ''; - - return WithdrawFormState( - coin: coin, - step: WithdrawFormStep.fill, - address: '', - amount: '', - senderAddresses: initSenderAddresses, - selectedSenderAddress: selectedSenderAddress, - isMaxAmount: false, - customFee: FeeRequest(type: ''), - withdrawDetails: WithdrawDetails.empty(), - isSending: false, - isCustomFeeEnabled: false, - trezorProgressStatus: null, - sendError: TextError.empty(), - addressError: TextError.empty(), - amountError: TextError.empty(), - utxoCustomFeeError: TextError.empty(), - gasLimitError: TextError.empty(), - gasPriceError: TextError.empty(), - memo: null, - coinsRepository: coinsRepository, - ); - } + this.selectedSourceAddress, + this.isMaxAmount = false, + this.isCustomFee = false, + this.isCustomFeeEnabled = false, + this.customFee, + this.memo, + this.isIbcTransfer = false, + this.ibcChannel, + this.preview, + this.isSending = false, + this.result, + // Error states + this.recipientAddressError, + this.isMixedCaseAddress = false, + this.amountError, + this.customFeeError, + this.ibcChannelError, + this.previewError, + this.transactionError, + this.networkError, + }); WithdrawFormState copyWith({ - Coin? coin, - String? address, - String? amount, + Asset? asset, + ValueGetter? pubkeys, WithdrawFormStep? step, - FeeRequest? customFee, - List? senderAddresses, - String? selectedSenderAddress, + String? recipientAddress, + String? amount, + ValueGetter? selectedSourceAddress, bool? isMaxAmount, - BaseError? sendError, - BaseError? addressError, - BaseError? amountError, - BaseError? utxoCustomFeeError, - BaseError? gasLimitError, - BaseError? gasPriceError, - WithdrawDetails? withdrawDetails, - bool? isSending, + bool? isCustomFee, bool? isCustomFeeEnabled, - String? trezorProgressStatus, - String? memo, - CoinsRepo? coinsRepository, + ValueGetter? customFee, + ValueGetter? memo, + bool? isIbcTransfer, + ValueGetter? ibcChannel, + ValueGetter? preview, + bool? isSending, + ValueGetter? result, + // Error states + ValueGetter? recipientAddressError, + bool? isMixedCaseAddress, + ValueGetter? amountError, + ValueGetter? customFeeError, + ValueGetter? ibcChannelError, + ValueGetter? previewError, + ValueGetter? transactionError, + ValueGetter? networkError, }) { return WithdrawFormState( - coin: coin ?? this.coin, - address: address ?? this.address, - amount: amount ?? this.amount, + asset: asset ?? this.asset, + pubkeys: pubkeys != null ? pubkeys() : this.pubkeys, step: step ?? this.step, - customFee: customFee ?? this.customFee, + recipientAddress: recipientAddress ?? this.recipientAddress, + amount: amount ?? this.amount, + selectedSourceAddress: selectedSourceAddress != null + ? selectedSourceAddress() + : this.selectedSourceAddress, isMaxAmount: isMaxAmount ?? this.isMaxAmount, - senderAddresses: senderAddresses ?? this.senderAddresses, - selectedSenderAddress: - selectedSenderAddress ?? this.selectedSenderAddress, - sendError: sendError ?? this.sendError, - withdrawDetails: withdrawDetails ?? this.withdrawDetails, - isSending: isSending ?? this.isSending, - addressError: addressError ?? this.addressError, - amountError: amountError ?? this.amountError, - gasLimitError: gasLimitError ?? this.gasLimitError, - gasPriceError: gasPriceError ?? this.gasPriceError, - utxoCustomFeeError: utxoCustomFeeError ?? this.utxoCustomFeeError, + isCustomFee: isCustomFee ?? this.isCustomFee, isCustomFeeEnabled: isCustomFeeEnabled ?? this.isCustomFeeEnabled, - trezorProgressStatus: trezorProgressStatus, - memo: memo ?? this.memo, - coinsRepository: coinsRepository ?? _coinsRepo, + customFee: customFee != null ? customFee() : this.customFee, + memo: memo != null ? memo() : this.memo, + isIbcTransfer: isIbcTransfer ?? this.isIbcTransfer, + ibcChannel: ibcChannel != null ? ibcChannel() : this.ibcChannel, + preview: preview != null ? preview() : this.preview, + isSending: isSending ?? this.isSending, + result: result != null ? result() : this.result, + recipientAddressError: recipientAddressError != null + ? recipientAddressError() + : this.recipientAddressError, + isMixedCaseAddress: isMixedCaseAddress ?? this.isMixedCaseAddress, + amountError: amountError != null ? amountError() : this.amountError, + customFeeError: + customFeeError != null ? customFeeError() : this.customFeeError, + ibcChannelError: + ibcChannelError != null ? ibcChannelError() : this.ibcChannelError, + previewError: previewError != null ? previewError() : this.previewError, + transactionError: + transactionError != null ? transactionError() : this.transactionError, + networkError: networkError != null ? networkError() : this.networkError, ); } - final Coin coin; - final String address; - final String amount; - final WithdrawFormStep step; - final List senderAddresses; - final String selectedSenderAddress; - final FeeRequest customFee; - final WithdrawDetails withdrawDetails; - final bool isSending; - final bool isCustomFeeEnabled; - final String? trezorProgressStatus; - final BaseError sendError; - final BaseError addressError; - final BaseError amountError; - final BaseError utxoCustomFeeError; - final BaseError gasLimitError; - final BaseError gasPriceError; - final bool _isMaxAmount; - final String? memo; - final CoinsRepo _coinsRepo; + WithdrawParameters toWithdrawParameters() { + return WithdrawParameters( + asset: asset.id.id, + toAddress: recipientAddress, + amount: isMaxAmount ? null : Decimal.parse(amount), + fee: isCustomFeeEnabled ? customFee : null, + from: selectedSourceAddress?.derivationPath != null + ? WithdrawalSource.hdDerivationPath( + selectedSourceAddress!.derivationPath!, + ) + : null, + memo: memo, + ibcTransfer: isIbcTransfer ? true : null, + isMax: isMaxAmount, + ); + } + + //TODO! + double? get usdFeePrice => 0.0; + + //TODO! + double? get usdAmountPrice => 0.0; + + //TODO! + bool get isFeePriceExpensive => false; @override List get props => [ - coin, - address, - amount, + asset, + pubkeys, step, - senderAddresses, - selectedSenderAddress, + recipientAddress, + amount, + selectedSourceAddress, isMaxAmount, + isCustomFee, + isCustomFeeEnabled, customFee, - withdrawDetails, + memo, + isIbcTransfer, + ibcChannel, + preview, isSending, - isCustomFeeEnabled, - trezorProgressStatus, - sendError, - addressError, + result, + recipientAddressError, + isMixedCaseAddress, amountError, - utxoCustomFeeError, - gasLimitError, - gasPriceError, - memo, + customFeeError, + ibcChannelError, + previewError, + transactionError, + networkError, ]; - - bool get isMaxAmount => - _isMaxAmount || amount == doubleToString(senderAddressBalance); - double get amountToSendDouble => double.tryParse(amount) ?? 0; - String get amountToSendString { - if (isMaxAmount && coin.abbr == withdrawDetails.feeCoin) { - return doubleToString( - amountToSendDouble - double.parse(withdrawDetails.feeValue), - ); - } - return amount; - } - - double get senderAddressBalance { - switch (coin.enabledType) { - case WalletType.trezor: - return coin.getHdAddress(selectedSenderAddress)?.balance.spendable ?? - 0.0; - default: - return coin.sendableBalance; - } - } - - bool get hasAddressError => addressError.message.isNotEmpty; - bool get hasAmountError => amountError.message.isNotEmpty; - bool get hasSendError => sendError.message.isNotEmpty; - bool get hasGasLimitError => gasLimitError.message.isNotEmpty; - bool get hasGasPriceError => gasPriceError.message.isNotEmpty; - bool get hasUtxoFeeError => utxoCustomFeeError.message.isNotEmpty; - - double? get usdAmountPrice => _coinsRepo.getUsdPriceByAmount( - amount, - coin.abbr, - ); - - double? get usdFeePrice => _coinsRepo.getUsdPriceByAmount( - withdrawDetails.feeValue, - withdrawDetails.feeCoin, - ); - - bool get isFeePriceExpensive { - final usdFeePrice = this.usdFeePrice; - final usdAmountPrice = this.usdAmountPrice; - - if (usdFeePrice == null || usdAmountPrice == null || usdAmountPrice == 0) { - return false; - } - - return usdFeePrice / usdAmountPrice >= 0.05; - } } diff --git a/lib/bloc/withdraw_form/withdraw_form_step.dart b/lib/bloc/withdraw_form/withdraw_form_step.dart index 24d0f50dca..54aa822ca8 100644 --- a/lib/bloc/withdraw_form/withdraw_form_step.dart +++ b/lib/bloc/withdraw_form/withdraw_form_step.dart @@ -2,10 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; enum WithdrawFormStep { - failed, fill, confirm, - success; + success, + failed; + + static WithdrawFormStep initial() => WithdrawFormStep.fill; String get title { switch (this) { diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 0b8d41db60..36f774cc9c 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -1,6 +1,6 @@ // DO NOT EDIT. This is code generated via package:easy_localization/generate.dart -abstract class LocaleKeys { +abstract class LocaleKeys { static const plsActivateKmd = 'plsActivateKmd'; static const rewardClaiming = 'rewardClaiming'; static const noKmdAddress = 'noKmdAddress'; @@ -102,39 +102,50 @@ abstract class LocaleKeys { static const seedPhrase = 'seedPhrase'; static const assetNumber = 'assetNumber'; static const clipBoard = 'clipBoard'; - static const walletsManagerCreateWalletButton = 'walletsManagerCreateWalletButton'; - static const walletsManagerImportWalletButton = 'walletsManagerImportWalletButton'; - static const walletsManagerStepBuilderCreationWalletError = 'walletsManagerStepBuilderCreationWalletError'; + static const walletsManagerCreateWalletButton = + 'walletsManagerCreateWalletButton'; + static const walletsManagerImportWalletButton = + 'walletsManagerImportWalletButton'; + static const walletsManagerStepBuilderCreationWalletError = + 'walletsManagerStepBuilderCreationWalletError'; static const walletCreationTitle = 'walletCreationTitle'; static const walletImportTitle = 'walletImportTitle'; static const walletImportByFileTitle = 'walletImportByFileTitle'; - static const walletImportCreatePasswordTitle = 'walletImportCreatePasswordTitle'; + static const walletImportCreatePasswordTitle = + 'walletImportCreatePasswordTitle'; static const walletImportByFileDescription = 'walletImportByFileDescription'; static const walletLogInTitle = 'walletLogInTitle'; static const walletCreationNameHint = 'walletCreationNameHint'; static const walletCreationPasswordHint = 'walletCreationPasswordHint'; - static const walletCreationConfirmPasswordHint = 'walletCreationConfirmPasswordHint'; + static const walletCreationConfirmPasswordHint = + 'walletCreationConfirmPasswordHint'; static const walletCreationConfirmPassword = 'walletCreationConfirmPassword'; static const walletCreationUploadFile = 'walletCreationUploadFile'; static const walletCreationEmptySeedError = 'walletCreationEmptySeedError'; static const walletCreationExistNameError = 'walletCreationExistNameError'; static const walletCreationNameLengthError = 'walletCreationNameLengthError'; - static const walletCreationFormatPasswordError = 'walletCreationFormatPasswordError'; - static const walletCreationConfirmPasswordError = 'walletCreationConfirmPasswordError'; + static const walletCreationFormatPasswordError = + 'walletCreationFormatPasswordError'; + static const walletCreationConfirmPasswordError = + 'walletCreationConfirmPasswordError'; static const invalidPasswordError = 'invalidPasswordError'; static const importSeedEnterSeedPhraseHint = 'importSeedEnterSeedPhraseHint'; static const passphraseCheckingTitle = 'passphraseCheckingTitle'; static const passphraseCheckingDescription = 'passphraseCheckingDescription'; static const passphraseCheckingEnterWord = 'passphraseCheckingEnterWord'; - static const passphraseCheckingEnterWordHint = 'passphraseCheckingEnterWordHint'; + static const passphraseCheckingEnterWordHint = + 'passphraseCheckingEnterWordHint'; static const back = 'back'; static const settingsMenuGeneral = 'settingsMenuGeneral'; static const settingsMenuLanguage = 'settingsMenuLanguage'; static const settingsMenuSecurity = 'settingsMenuSecurity'; static const settingsMenuAbout = 'settingsMenuAbout'; - static const seedPhraseSettingControlsViewSeed = 'seedPhraseSettingControlsViewSeed'; - static const seedPhraseSettingControlsDownloadSeed = 'seedPhraseSettingControlsDownloadSeed'; - static const debugSettingsResetActivatedCoins = 'debugSettingsResetActivatedCoins'; + static const seedPhraseSettingControlsViewSeed = + 'seedPhraseSettingControlsViewSeed'; + static const seedPhraseSettingControlsDownloadSeed = + 'seedPhraseSettingControlsDownloadSeed'; + static const debugSettingsResetActivatedCoins = + 'debugSettingsResetActivatedCoins'; static const debugSettingsDownloadButton = 'debugSettingsDownloadButton'; static const or = 'or'; static const passwordTitle = 'passwordTitle'; @@ -144,16 +155,19 @@ abstract class LocaleKeys { static const changePasswordSpan1 = 'changePasswordSpan1'; static const updatePassword = 'updatePassword'; static const passwordHasChanged = 'passwordHasChanged'; - static const confirmationForShowingSeedPhraseTitle = 'confirmationForShowingSeedPhraseTitle'; + static const confirmationForShowingSeedPhraseTitle = + 'confirmationForShowingSeedPhraseTitle'; static const saveAndRemember = 'saveAndRemember'; static const seedPhraseShowingTitle = 'seedPhraseShowingTitle'; static const seedPhraseShowingWarning = 'seedPhraseShowingWarning'; static const seedPhraseShowingShowPhrase = 'seedPhraseShowingShowPhrase'; static const seedPhraseShowingCopySeed = 'seedPhraseShowingCopySeed'; - static const seedPhraseShowingSavedPhraseButton = 'seedPhraseShowingSavedPhraseButton'; + static const seedPhraseShowingSavedPhraseButton = + 'seedPhraseShowingSavedPhraseButton'; static const seedAccessSpan1 = 'seedAccessSpan1'; static const backupSeedNotificationTitle = 'backupSeedNotificationTitle'; - static const backupSeedNotificationDescription = 'backupSeedNotificationDescription'; + static const backupSeedNotificationDescription = + 'backupSeedNotificationDescription'; static const backupSeedNotificationButton = 'backupSeedNotificationButton'; static const swapConfirmationTitle = 'swapConfirmationTitle'; static const swapConfirmationYouReceive = 'swapConfirmationYouReceive'; @@ -161,36 +175,48 @@ abstract class LocaleKeys { static const tradingDetailsTitleFailed = 'tradingDetailsTitleFailed'; static const tradingDetailsTitleCompleted = 'tradingDetailsTitleCompleted'; static const tradingDetailsTitleInProgress = 'tradingDetailsTitleInProgress'; - static const tradingDetailsTitleOrderMatching = 'tradingDetailsTitleOrderMatching'; + static const tradingDetailsTitleOrderMatching = + 'tradingDetailsTitleOrderMatching'; static const tradingDetailsTotalSpentTime = 'tradingDetailsTotalSpentTime'; - static const tradingDetailsTotalSpentTimeWithHours = 'tradingDetailsTotalSpentTimeWithHours'; + static const tradingDetailsTotalSpentTimeWithHours = + 'tradingDetailsTotalSpentTimeWithHours'; static const swapRecoverButtonTitle = 'swapRecoverButtonTitle'; static const swapRecoverButtonText = 'swapRecoverButtonText'; static const swapRecoverButtonErrorMessage = 'swapRecoverButtonErrorMessage'; - static const swapRecoverButtonSuccessMessage = 'swapRecoverButtonSuccessMessage'; + static const swapRecoverButtonSuccessMessage = + 'swapRecoverButtonSuccessMessage'; static const swapProgressStatusFailed = 'swapProgressStatusFailed'; static const swapDetailsStepStatusFailed = 'swapDetailsStepStatusFailed'; static const disclaimerAcceptEulaCheckbox = 'disclaimerAcceptEulaCheckbox'; - static const disclaimerAcceptTermsAndConditionsCheckbox = 'disclaimerAcceptTermsAndConditionsCheckbox'; + static const disclaimerAcceptTermsAndConditionsCheckbox = + 'disclaimerAcceptTermsAndConditionsCheckbox'; static const disclaimerAcceptDescription = 'disclaimerAcceptDescription'; - static const swapDetailsStepStatusInProcess = 'swapDetailsStepStatusInProcess'; - static const swapDetailsStepStatusTimeSpent = 'swapDetailsStepStatusTimeSpent'; + static const swapDetailsStepStatusInProcess = + 'swapDetailsStepStatusInProcess'; + static const swapDetailsStepStatusTimeSpent = + 'swapDetailsStepStatusTimeSpent'; static const milliseconds = 'milliseconds'; static const seconds = 'seconds'; static const minutes = 'minutes'; static const hours = 'hours'; - static const coinAddressDetailsNotificationTitle = 'coinAddressDetailsNotificationTitle'; - static const coinAddressDetailsNotificationDescription = 'coinAddressDetailsNotificationDescription'; + static const coinAddressDetailsNotificationTitle = + 'coinAddressDetailsNotificationTitle'; + static const coinAddressDetailsNotificationDescription = + 'coinAddressDetailsNotificationDescription'; static const swapFeeDetailsPaidFromBalance = 'swapFeeDetailsPaidFromBalance'; static const swapFeeDetailsSendCoinTxFee = 'swapFeeDetailsSendCoinTxFee'; - static const swapFeeDetailsReceiveCoinTxFee = 'swapFeeDetailsReceiveCoinTxFee'; + static const swapFeeDetailsReceiveCoinTxFee = + 'swapFeeDetailsReceiveCoinTxFee'; static const swapFeeDetailsTradingFee = 'swapFeeDetailsTradingFee'; - static const swapFeeDetailsSendTradingFeeTxFee = 'swapFeeDetailsSendTradingFeeTxFee'; + static const swapFeeDetailsSendTradingFeeTxFee = + 'swapFeeDetailsSendTradingFeeTxFee'; static const swapFeeDetailsNone = 'swapFeeDetailsNone'; - static const swapFeeDetailsPaidFromReceivedVolume = 'swapFeeDetailsPaidFromReceivedVolume'; + static const swapFeeDetailsPaidFromReceivedVolume = + 'swapFeeDetailsPaidFromReceivedVolume'; static const logoutPopupTitle = 'logoutPopupTitle'; static const logoutPopupDescription = 'logoutPopupDescription'; - static const logoutPopupDescriptionWalletOnly = 'logoutPopupDescriptionWalletOnly'; + static const logoutPopupDescriptionWalletOnly = + 'logoutPopupDescriptionWalletOnly'; static const transactionDetailsTitle = 'transactionDetailsTitle'; static const customSeedWarningText = 'customSeedWarningText'; static const customSeedIUnderstand = 'customSeedIUnderstand'; @@ -264,7 +290,8 @@ abstract class LocaleKeys { static const sell = 'sell'; static const buy = 'buy'; static const changingWalletPassword = 'changingWalletPassword'; - static const changingWalletPasswordDescription = 'changingWalletPasswordDescription'; + static const changingWalletPasswordDescription = + 'changingWalletPasswordDescription'; static const dark = 'dark'; static const darkMode = 'darkMode'; static const light = 'light'; @@ -287,7 +314,8 @@ abstract class LocaleKeys { static const email = 'email'; static const emailValidatorError = 'emailValidatorError'; static const feedbackValidatorEmptyError = 'feedbackValidatorEmptyError'; - static const feedbackValidatorMaxLengthError = 'feedbackValidatorMaxLengthError'; + static const feedbackValidatorMaxLengthError = + 'feedbackValidatorMaxLengthError'; static const yourFeedback = 'yourFeedback'; static const sendFeedback = 'sendFeedback'; static const sendFeedbackError = 'sendFeedbackError'; @@ -335,7 +363,8 @@ abstract class LocaleKeys { static const noSenderAddress = 'noSenderAddress'; static const confirmOnTrezor = 'confirmOnTrezor'; static const alphaVersionWarningTitle = 'alphaVersionWarningTitle'; - static const alphaVersionWarningDescription = 'alphaVersionWarningDescription'; + static const alphaVersionWarningDescription = + 'alphaVersionWarningDescription'; static const sendToAnalytics = 'sendToAnalytics'; static const backToWallet = 'backToWallet'; static const backToDex = 'backToDex'; @@ -381,13 +410,16 @@ abstract class LocaleKeys { static const bridgeMaxSendAmountError = 'bridgeMaxSendAmountError'; static const bridgeMinOrderAmountError = 'bridgeMinOrderAmountError'; static const bridgeMaxOrderAmountError = 'bridgeMaxOrderAmountError'; - static const bridgeInsufficientBalanceError = 'bridgeInsufficientBalanceError'; + static const bridgeInsufficientBalanceError = + 'bridgeInsufficientBalanceError'; static const lowTradeVolumeError = 'lowTradeVolumeError'; static const bridgeSelectReceiveCoinError = 'bridgeSelectReceiveCoinError'; static const withdrawNoParentCoinError = 'withdrawNoParentCoinError'; static const withdrawTopUpBalanceError = 'withdrawTopUpBalanceError'; - static const withdrawNotEnoughBalanceForGasError = 'withdrawNotEnoughBalanceForGasError'; - static const withdrawNotSufficientBalanceError = 'withdrawNotSufficientBalanceError'; + static const withdrawNotEnoughBalanceForGasError = + 'withdrawNotEnoughBalanceForGasError'; + static const withdrawNotSufficientBalanceError = + 'withdrawNotSufficientBalanceError'; static const withdrawZeroBalanceError = 'withdrawZeroBalanceError'; static const withdrawAmountTooLowError = 'withdrawAmountTooLowError'; static const withdrawNoSuchCoinError = 'withdrawNoSuchCoinError'; @@ -452,8 +484,10 @@ abstract class LocaleKeys { static const availableForSwaps = 'availableForSwaps'; static const swapNow = 'swapNow'; static const passphrase = 'passphrase'; - static const enterPassphraseHiddenWalletTitle = 'enterPassphraseHiddenWalletTitle'; - static const enterPassphraseHiddenWalletDescription = 'enterPassphraseHiddenWalletDescription'; + static const enterPassphraseHiddenWalletTitle = + 'enterPassphraseHiddenWalletTitle'; + static const enterPassphraseHiddenWalletDescription = + 'enterPassphraseHiddenWalletDescription'; static const skip = 'skip'; static const activateToSeeFunds = 'activateToSeeFunds'; static const allowCustomFee = 'allowCustomFee'; @@ -528,8 +562,10 @@ abstract class LocaleKeys { static const noWalletsAvailable = 'noWalletsAvailable'; static const selectWalletToReset = 'selectWalletToReset'; static const qrScannerTitle = 'qrScannerTitle'; - static const qrScannerErrorControllerUninitialized = 'qrScannerErrorControllerUninitialized'; - static const qrScannerErrorPermissionDenied = 'qrScannerErrorPermissionDenied'; + static const qrScannerErrorControllerUninitialized = + 'qrScannerErrorControllerUninitialized'; + static const qrScannerErrorPermissionDenied = + 'qrScannerErrorPermissionDenied'; static const qrScannerErrorGenericError = 'qrScannerErrorGenericError'; static const qrScannerErrorTitle = 'qrScannerErrorTitle'; static const spend = 'spend'; @@ -572,7 +608,8 @@ abstract class LocaleKeys { static const fiatPaymentInProgressMessage = 'fiatPaymentInProgressMessage'; static const pleaseWait = 'pleaseWait'; static const bitrefillPaymentSuccessfull = 'bitrefillPaymentSuccessfull'; - static const bitrefillPaymentSuccessfullInstruction = 'bitrefillPaymentSuccessfullInstruction'; + static const bitrefillPaymentSuccessfullInstruction = + 'bitrefillPaymentSuccessfullInstruction'; static const tradingBot = 'tradingBot'; static const margin = 'margin'; static const updateInterval = 'updateInterval'; @@ -616,5 +653,4 @@ abstract class LocaleKeys { static const allTimeInvestment = 'allTimeInvestment'; static const allTimeProfit = 'allTimeProfit'; static const profitAndLoss = 'profitAndLoss'; - } diff --git a/lib/main.dart b/lib/main.dart index 0f93e70075..360396dd37 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -76,7 +76,7 @@ Future main() async { mm2: mm2, trezorBloc: trezor, ); - final mm2Api = Mm2Api(mm2: mm2, coinsRepo: coinsRepo); + final mm2Api = Mm2Api(mm2: mm2, coinsRepo: coinsRepo, sdk: komodoDefiSdk); final walletsRepository = WalletsRepository( komodoDefiSdk, mm2Api, diff --git a/lib/mm2/mm2.dart b/lib/mm2/mm2.dart index 3d8615084e..b03a35a81d 100644 --- a/lib/mm2/mm2.dart +++ b/lib/mm2/mm2.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/version/version_response.dart'; import 'package:web_dex/shared/utils/utils.dart'; diff --git a/lib/mm2/mm2_api/mm2_api.dart b/lib/mm2/mm2_api/mm2_api.dart index f0be1b7316..041a7faaa3 100644 --- a/lib/mm2/mm2_api/mm2_api.dart +++ b/lib/mm2/mm2_api/mm2_api.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2.dart'; @@ -23,7 +24,6 @@ import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_request.dart import 'package:web_dex/mm2/mm2_api/rpc/max_taker_vol/max_taker_vol_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol.dart'; import 'package:web_dex/mm2/mm2_api/rpc/min_trading_vol/min_trading_vol_response.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/my_balance/my_balance_req.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_orders/my_orders_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_orders/my_orders_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/my_recent_swaps/my_recent_swaps_request.dart'; @@ -60,12 +60,20 @@ class Mm2Api { Mm2Api({ required MM2 mm2, required CoinsRepo coinsRepo, - }) : _mm2 = mm2 { + required KomodoDefiSdk sdk, + }) : _sdk = sdk, + _mm2 = mm2 { trezor = Mm2ApiTrezor(_mm2.call); nft = Mm2ApiNft(_mm2.call, coinsRepo); } final MM2 _mm2; + + // Ideally we will transition cleanly over to the SDK, but for methods + // which are deeply intertwined with the app and are broken by HD wallet + // changes, we will tie into the SDK here. + final KomodoDefiSdk _sdk; + late Mm2ApiTrezor trezor; late Mm2ApiNft nft; VersionResponse? _versionResponse; @@ -84,34 +92,12 @@ class Mm2Api { } } + @Deprecated('Use balance from KomoDefiSdk instead') Future getBalance(String abbr) async { - dynamic response; - try { - response = await _mm2.call(MyBalanceReq(coin: abbr)); - } catch (e, s) { - log( - 'Error getting balance $abbr: ${e.toString()}', - path: 'api => getBalance => _call', - trace: s, - isError: true, - ); - return null; - } - - Map json; - try { - json = jsonDecode(response); - } catch (e, s) { - log( - 'Error parsing of get balance $abbr response: ${e.toString()}', - path: 'api => getBalance => jsonDecode', - trace: s, - isError: true, - ); - return null; - } + final sdkAsset = _sdk.assets.assetsFromTicker(abbr).single; + final addresses = await sdkAsset.getPubkeys(_sdk); - return json['balance']; + return addresses.balance.total.toString(); } Future _fallbackToBalanceTaker(String abbr) async { diff --git a/lib/mm2/mm2_api/mm2_api_nft.dart b/lib/mm2/mm2_api/mm2_api_nft.dart index 43531a314d..460bdec0f1 100644 --- a/lib/mm2/mm2_api/mm2_api_nft.dart +++ b/lib/mm2/mm2_api/mm2_api_nft.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/mm2/mm2_api/rpc/errors.dart'; import 'package:web_dex/mm2/mm2_api/rpc/nft/get_nft_list/get_nft_list_req.dart'; @@ -21,14 +21,15 @@ class Mm2ApiNft { final Future Function(dynamic) call; Future> updateNftList( - List chains) async { + List chains, + ) async { try { final List nftChains = await getActiveNftChains(chains); if (nftChains.isEmpty) { return { 'error': 'Please ensure an NFT chain is activated and patiently await ' - 'while your NFTs are loaded.' + 'while your NFTs are loaded.', }; } final UpdateNftRequest request = UpdateNftRequest(chains: nftChains); @@ -54,18 +55,24 @@ class Mm2ApiNft { } } - Future> refreshNftMetadata( - {required String chain, - required String tokenAddress, - required String tokenId}) async { + Future> refreshNftMetadata({ + required String chain, + required String tokenAddress, + required String tokenId, + }) async { try { final RefreshNftMetadataRequest request = RefreshNftMetadataRequest( - chain: chain, tokenAddress: tokenAddress, tokenId: tokenId); + chain: chain, + tokenAddress: tokenAddress, + tokenId: tokenId, + ); return await call(request); } catch (e) { - log(e.toString(), - path: 'Mm2ApiNft => RefreshNftMetadataRequest', isError: true) - .ignore(); + log( + e.toString(), + path: 'Mm2ApiNft => RefreshNftMetadataRequest', + isError: true, + ).ignore(); throw TransportError(message: e.toString()); } } @@ -77,7 +84,7 @@ class Mm2ApiNft { return { 'error': 'Please ensure the NFT chain is activated and patiently await ' - 'while your NFTs are loaded.' + 'while your NFTs are loaded.', }; } final GetNftListRequest request = GetNftListRequest(chains: nftChains); @@ -111,7 +118,9 @@ class Mm2ApiNft { } Future> getNftTxs( - NftTransactionsRequest request, bool withAdditionalInfo) async { + NftTransactionsRequest request, + bool withAdditionalInfo, + ) async { try { final JsonMap json = await call(request); if (withAdditionalInfo) { @@ -127,7 +136,8 @@ class Mm2ApiNft { } Future> getNftTxDetails( - NftTxDetailsRequest request) async { + NftTxDetailsRequest request, + ) async { try { final additionalTxInfo = await const ProxyApiNft() .getTxDetailsByHash(request.chain, request.txHash); @@ -143,9 +153,11 @@ class Mm2ApiNft { final List apiCoins = await _coinsRepo.getEnabledCoins(); // log(apiCoins.toString(), path: 'Mm2ApiNft => apiCoins', isError: true); final List enabledCoins = apiCoins.map((c) => c.abbr).toList(); - log(enabledCoins.toString(), - path: 'Mm2ApiNft => enabledCoins', isError: true) - .ignore(); + log( + enabledCoins.toString(), + path: 'Mm2ApiNft => enabledCoins', + isError: true, + ).ignore(); final List nftCoins = chains.map((c) => c.coinAbbr()).toList(); log(nftCoins.toString(), path: 'Mm2ApiNft => nftCoins', isError: true) .ignore(); @@ -154,9 +166,11 @@ class Mm2ApiNft { .toList() .where((c) => enabledCoins.contains(c.coinAbbr())) .toList(); - log(activeChains.toString(), - path: 'Mm2ApiNft => activeChains', isError: true) - .ignore(); + log( + activeChains.toString(), + path: 'Mm2ApiNft => activeChains', + isError: true, + ).ignore(); final List nftChains = activeChains.map((c) => c.toApiRequest()).toList(); log(nftChains.toString(), path: 'Mm2ApiNft => nftChains', isError: true) @@ -172,10 +186,12 @@ class ProxyApiNft { final transactions = List.from(json['result']['transfer_history'] as List? ?? []); final listOfAdditionalData = transactions - .map((tx) => { - 'blockchain': convertChainForProxy(tx['chain'] as String), - 'tx_hash': tx['transaction_hash'], - }) + .map( + (tx) => { + 'blockchain': convertChainForProxy(tx['chain'] as String), + 'tx_hash': tx['transaction_hash'], + }, + ) .toList(); final response = await Client().post( diff --git a/lib/mm2/mm2_api/mm2_api_trezor.dart b/lib/mm2/mm2_api/mm2_api_trezor.dart index 3a6673f95b..13986baf4f 100644 --- a/lib/mm2/mm2_api/mm2_api_trezor.dart +++ b/lib/mm2/mm2_api/mm2_api_trezor.dart @@ -1,4 +1,4 @@ -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_request.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_init/trezor_balance_init_response.dart'; import 'package:web_dex/mm2/mm2_api/rpc/trezor/balance/trezor_balance_status/trezor_balance_status_request.dart'; diff --git a/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart b/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart index 3c19a1da4a..6b37851914 100644 --- a/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart +++ b/lib/mm2/mm2_api/rpc/best_orders/best_orders.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_dynamic_calls import 'package:equatable/equatable.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/orderbook/order.dart'; diff --git a/lib/model/coin.dart b/lib/model/coin.dart index af783be988..cd796c5438 100644 --- a/lib/model/coin.dart +++ b/lib/model/coin.dart @@ -1,4 +1,6 @@ import 'package:collection/collection.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/model/cex_price.dart'; import 'package:web_dex/model/coin_type.dart'; @@ -6,6 +8,7 @@ import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/model/hd_account/hd_account.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/formatters.dart'; +import 'package:web_dex/shared/utils/utils.dart'; class Coin { Coin({ @@ -75,6 +78,7 @@ class Coin { double sendableBalance = 0; + @Deprecated('Use the balance manager from the SDK') double get balance { switch (enabledType) { case WalletType.trezor: @@ -186,7 +190,8 @@ class Coin { if (address.isEmpty) return null; return addresses.firstWhereOrNull( - (HdAddress hdAddress) => hdAddress.address == address); + (HdAddress hdAddress) => hdAddress.address == address, + ); } static bool checkSegwitByAbbr(String abbr) => abbr.contains('-segwit'); @@ -291,6 +296,10 @@ class Coin { } } +extension LegacyCoinToSdkAsset on Coin { + Asset toSdkAsset(KomodoDefiSdk sdk) => getSdkAsset(sdk, abbr); +} + CoinType? getCoinType(String? jsonType, String coinAbbr) { // anchor: protocols support for (CoinType value in CoinType.values) { @@ -439,8 +448,9 @@ class ProtocolData { class CoinNode { const CoinNode({required this.url, required this.guiAuth}); static CoinNode fromJson(Map json) => CoinNode( - url: json['url'], - guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false); + url: json['url'], + guiAuth: (json['gui_auth'] ?? json['komodo_proxy']) ?? false, + ); final bool guiAuth; final String url; diff --git a/lib/model/swap.dart b/lib/model/swap.dart index d3576b0f31..d600a5c0f5 100644 --- a/lib/model/swap.dart +++ b/lib/model/swap.dart @@ -141,7 +141,7 @@ class Swap extends Equatable { return 0; case SwapStatus.negotiated: return 0; - } + } } static String getSwapStatusString(SwapStatus status) { diff --git a/lib/model/wallet.dart b/lib/model/wallet.dart index fec465e00e..b96b0de6e9 100644 --- a/lib/model/wallet.dart +++ b/lib/model/wallet.dart @@ -1,5 +1,6 @@ import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:uuid/uuid.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/shared/utils/encryption_tool.dart'; @@ -93,7 +94,8 @@ class WalletConfig { factory WalletConfig.fromJson(Map json) { return WalletConfig( type: WalletType.fromJson( - json['type'] as String? ?? WalletType.iguana.name), + json['type'] as String? ?? WalletType.iguana.name, + ), seedPhrase: json['seed_phrase'] as String? ?? '', pubKey: json['pub_key'] as String?, activatedCoins: diff --git a/lib/router/navigators/app_router_delegate.dart b/lib/router/navigators/app_router_delegate.dart index 73b2c2e483..d59a02c6dd 100644 --- a/lib/router/navigators/app_router_delegate.dart +++ b/lib/router/navigators/app_router_delegate.dart @@ -37,7 +37,7 @@ class AppRouterDelegate extends RouterDelegate materialPageContext = context; return GestureDetector( onTap: () => runDropdownDismiss(context), - child: MainLayout(), + child: const MainLayout(), ); }, ), @@ -53,19 +53,19 @@ class AppRouterDelegate extends RouterDelegate } void runDropdownDismiss(BuildContext context) { - // Taker form - context.read().add(TakerCoinSelectorOpen(false)); - context.read().add(TakerOrderSelectorOpen(false)); + // Taker form + context.read().add(TakerCoinSelectorOpen(false)); + context.read().add(TakerOrderSelectorOpen(false)); - // Maker form - final makerFormBloc = RepositoryProvider.of(context); - makerFormBloc.showSellCoinSelect = false; - makerFormBloc.showBuyCoinSelect = false; + // Maker form + final makerFormBloc = RepositoryProvider.of(context); + makerFormBloc.showSellCoinSelect = false; + makerFormBloc.showBuyCoinSelect = false; - // Bridge form - context.read().add(const BridgeShowTickerDropdown(false)); - context.read().add(const BridgeShowSourceDropdown(false)); - context.read().add(const BridgeShowTargetDropdown(false)); + // Bridge form + context.read().add(const BridgeShowTickerDropdown(false)); + context.read().add(const BridgeShowSourceDropdown(false)); + context.read().add(const BridgeShowTargetDropdown(false)); } @override diff --git a/lib/shared/ui/custom_numeric_text_form_field.dart b/lib/shared/ui/custom_numeric_text_form_field.dart index e921a218a2..468e75cf2b 100644 --- a/lib/shared/ui/custom_numeric_text_form_field.dart +++ b/lib/shared/ui/custom_numeric_text_form_field.dart @@ -29,7 +29,7 @@ class CustomNumericTextFormField extends StatelessWidget { final String filteringRegExp; final int? errorMaxLines; final InputValidationMode validationMode; - final void Function(String)? onChanged; + final void Function(String?)? onChanged; final FocusNode? focusNode; final void Function(FocusNode)? onFocus; diff --git a/lib/shared/utils/extensions/async_extensions.dart b/lib/shared/utils/extensions/async_extensions.dart index 2c97b7ff08..3d8a78ef71 100644 --- a/lib/shared/utils/extensions/async_extensions.dart +++ b/lib/shared/utils/extensions/async_extensions.dart @@ -6,4 +6,3 @@ extension WaitAllFutures on List> { /// See Dart docs on error handling in lists of futures: [Future.wait] Future> awaitAll() => Future.wait(this); } - diff --git a/lib/shared/utils/formatters.dart b/lib/shared/utils/formatters.dart index d1ce7628c8..f995dd540f 100644 --- a/lib/shared/utils/formatters.dart +++ b/lib/shared/utils/formatters.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:rational/rational.dart'; import 'package:web_dex/shared/constants.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; final List currencyInputFormatters = [ DecimalTextInputFormatter(decimalRange: decimalRange), @@ -14,7 +14,6 @@ final List currencyInputFormatters = [ ]; class DurationLocalization { - DurationLocalization({ required this.milliseconds, required this.seconds, @@ -29,7 +28,9 @@ class DurationLocalization { /// unit test: [testDurationFormat] String durationFormat( - Duration duration, DurationLocalization durationLocalization,) { + Duration duration, + DurationLocalization durationLocalization, +) { final int hh = duration.inHours; final int mm = duration.inMinutes.remainder(60); final int ss = duration.inSeconds.remainder(60); @@ -177,7 +178,10 @@ String formatDexAmt(dynamic amount) { return cutTrailingZeros((amount as double).toStringAsFixed(8)); case Rational: return cutTrailingZeros( - (amount as Rational).toDecimal(scaleOnInfinitePrecision: 12).toStringAsFixed(8),); + (amount as Rational) + .toDecimal(scaleOnInfinitePrecision: 12) + .toStringAsFixed(8), + ); case String: return cutTrailingZeros(double.parse(amount).toStringAsFixed(8)); case int: @@ -333,8 +337,11 @@ void formatAmountInput(TextEditingController controller, Rational? value) { /// print(result2); // Output: "12...890" /// ``` /// unit tests: [testTruncateHash] -String truncateMiddleSymbols(String text, - [int? startSymbolsCount, int endCount = 7,]) { +String truncateMiddleSymbols( + String text, [ + int? startSymbolsCount, + int endCount = 7, +]) { final int startCount = startSymbolsCount ?? (text.startsWith('0x') ? 6 : 4); if (text.length <= startCount + endCount + 3) return text; final String firstPart = text.substring(0, startCount); diff --git a/lib/shared/utils/password.dart b/lib/shared/utils/password.dart index 761ce9011a..1c9f7b1df2 100644 --- a/lib/shared/utils/password.dart +++ b/lib/shared/utils/password.dart @@ -3,7 +3,7 @@ import 'dart:math'; String generatePassword() { final List passwords = []; - final rng = Random(); + final rng = Random.secure(); const String lowerCase = 'abcdefghijklmnopqrstuvwxyz'; const String upperCase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; @@ -57,7 +57,8 @@ bool validateRPCPassword(String src) { // 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}$'); + 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, diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index ece4cc2aeb..8e38a40df9 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:rational/rational.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:web_dex/common/screen.dart'; @@ -25,16 +25,19 @@ export 'package:web_dex/shared/utils/prominent_colors.dart'; void copyToClipBoard(BuildContext context, String str) { final themeData = Theme.of(context); try { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - duration: const Duration(seconds: 2), - content: Text( - LocaleKeys.clipBoard.tr(), - style: themeData.textTheme.bodyLarge!.copyWith( + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + LocaleKeys.clipBoard.tr(), + style: themeData.textTheme.bodyLarge!.copyWith( color: themeData.brightness == Brightness.dark ? themeData.hintColor - : themeData.primaryColor), + : themeData.primaryColor, + ), + ), ), - )); + ); } catch (_) {} Clipboard.setData(ClipboardData(text: str)); @@ -171,6 +174,7 @@ String getAddressExplorerUrl(Coin coin, String address) { return '$explorerUrl$explorerAddressUrl$address'; } +@Deprecated('Use the Protocol class\'s explorer URL methods') void viewHashOnExplorer(Coin coin, String address, HashExplorerType type) { late String url; switch (type) { @@ -181,10 +185,34 @@ void viewHashOnExplorer(Coin coin, String address, HashExplorerType type) { url = getTxExplorerUrl(coin, address); break; } - launchURL(url); + launchURLString(url); +} + +extension AssetExplorerUrls on Asset { + Uri? txExplorerUrl(String? txHash) { + return txHash == null ? null : protocol.explorerTxUrl(txHash); + } + + Uri? addressExplorerUrl(String? address) { + return address == null ? null : protocol.explorerAddressUrl(address); + } } -Future launchURL( +Future openUrl(Uri uri, {bool? inSeparateTab}) async { + if (!await canLaunchUrl(uri)) { + throw Exception('Could not launch $uri'); + } + await launchUrl( + uri, + mode: inSeparateTab == null + ? LaunchMode.platformDefault + : inSeparateTab == true + ? LaunchMode.externalApplication + : LaunchMode.inAppWebView, + ); +} + +Future launchURLString( String url, { bool? inSeparateTab, }) async { @@ -651,11 +679,7 @@ enum HashExplorerType { tx, } -Asset getSdkAsset(KomodoDefiSdk? sdk, String abbr) { - if (sdk == null) { - throw Exception('getSdkAsset: SDK is null'); - } - +Asset getSdkAsset(KomodoDefiSdk sdk, String abbr) { // ignore: deprecated_member_use return sdk.assets.assetsFromTicker(abbr).single; } diff --git a/lib/shared/utils/zip.dart b/lib/shared/utils/zip.dart index 467df41ad3..c5d8ba1a76 100644 --- a/lib/shared/utils/zip.dart +++ b/lib/shared/utils/zip.dart @@ -9,7 +9,7 @@ Uint8List createZipOfSingleFile({ final fileNameWithExtension = '$fileName.txt'; final originalBytes = utf8.encode(fileContent); - // use `raw: true` to exclude zlip header and trailer data that causes + // use `raw: true` to exclude zlip header and trailer data that causes // zip decompression to fail final compressedBytes = Uint8List.fromList(ZLibCodec(raw: true).encode(originalBytes)); diff --git a/lib/shared/widgets/html_parser.dart b/lib/shared/widgets/html_parser.dart index f7f3cd4ddd..60751cef17 100644 --- a/lib/shared/widgets/html_parser.dart +++ b/lib/shared/widgets/html_parser.dart @@ -5,6 +5,7 @@ import 'package:web_dex/shared/utils/utils.dart'; class HtmlParser extends StatefulWidget { const HtmlParser( this.html, { + super.key, this.textStyle, this.linkStyle, }); @@ -42,18 +43,22 @@ class _HtmlParserState extends State { // ignore: unnecessary_string_escapes RegExp(']+href=\'(.*?)\'[^>]*>(.*)?<\/a>').firstMatch(chunk); if (linkMatch == null) { - children.add(TextSpan( - text: chunk, - style: textStyle, - )); + children.add( + TextSpan( + text: chunk, + style: textStyle, + ), + ); } else { children.add(_buildClickable(linkMatch)); } } - return SelectableText.rich(TextSpan( - children: children, - )); + return SelectableText.rich( + TextSpan( + children: children, + ), + ); } List _splitLinks(String text) { @@ -70,7 +75,8 @@ class _HtmlParserState extends State { } InlineSpan _buildClickable(RegExpMatch match) { - recognizers.add(TapGestureRecognizer()..onTap = () => launchURL(match[1]!)); + recognizers + .add(TapGestureRecognizer()..onTap = () => launchURLString(match[1]!)); return TextSpan( text: match[2], diff --git a/lib/shared/widgets/launch_native_explorer_button.dart b/lib/shared/widgets/launch_native_explorer_button.dart index e3fb75f9ff..35b1e0b485 100644 --- a/lib/shared/widgets/launch_native_explorer_button.dart +++ b/lib/shared/widgets/launch_native_explorer_button.dart @@ -20,7 +20,7 @@ class LaunchNativeExplorerButton extends StatelessWidget { width: 160, height: 30, onPressed: () { - launchURL(getNativeExplorerUrlByCoin(coin, address)); + launchURLString(getNativeExplorerUrlByCoin(coin, address)); }, text: LocaleKeys.viewOnExplorer.tr(), ); diff --git a/lib/shared/widgets/simple_copyable_link.dart b/lib/shared/widgets/simple_copyable_link.dart index bdea41570f..1b0571e3e1 100644 --- a/lib/shared/widgets/simple_copyable_link.dart +++ b/lib/shared/widgets/simple_copyable_link.dart @@ -19,7 +19,7 @@ class SimpleCopyableLink extends StatelessWidget { return CopyableLink( text: text, valueToCopy: valueToCopy, - onLinkTap: link == null ? null : () => launchURL(link), + onLinkTap: link == null ? null : () => launchURLString(link), ); } } diff --git a/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart b/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart index dbda89c9ed..34ce41cde9 100644 --- a/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart +++ b/lib/views/dex/dex_list_filter/mobile/dex_list_filter_coins_list_mobile.dart @@ -40,9 +40,9 @@ class _DexListFilterCoinsListState extends State { children: [ UiTextFormField( hintText: LocaleKeys.searchAssets.tr(), - onChanged: (String searchPhrase) { + onChanged: (String? searchPhrase) { setState(() { - _searchPhrase = searchPhrase; + _searchPhrase = searchPhrase ?? ''; }); }, ), @@ -68,40 +68,44 @@ class _DexListFilterCoinsListState extends State { final tradingEntitiesBloc = RepositoryProvider.of(context); return StreamBuilder>( - stream: tradingEntitiesBloc.outSwaps, - initialData: tradingEntitiesBloc.swaps, - builder: (context, snapshot) { - final list = snapshot.data ?? []; - final filtered = widget.listType == DexListType.history - ? list.where((s) => s.isCompleted).toList() - : list.where((s) => !s.isCompleted).toList(); - final Map> coinAbbrMap = - getCoinAbbrMapFromSwapList(filtered, widget.isSellCoin); + stream: tradingEntitiesBloc.outSwaps, + initialData: tradingEntitiesBloc.swaps, + builder: (context, snapshot) { + final list = snapshot.data ?? []; + final filtered = widget.listType == DexListType.history + ? list.where((s) => s.isCompleted).toList() + : list.where((s) => !s.isCompleted).toList(); + final Map> coinAbbrMap = + getCoinAbbrMapFromSwapList(filtered, widget.isSellCoin); - return _buildCoinList(coinAbbrMap); - }); + return _buildCoinList(coinAbbrMap); + }, + ); } Widget _buildOrderCoinList() { final tradingEntitiesBloc = RepositoryProvider.of(context); return StreamBuilder>( - stream: tradingEntitiesBloc.outMyOrders, - initialData: tradingEntitiesBloc.myOrders, - builder: (context, snapshot) { - final list = snapshot.data ?? []; - final Map> coinAbbrMap = - getCoinAbbrMapFromOrderList(list, widget.isSellCoin); + stream: tradingEntitiesBloc.outMyOrders, + initialData: tradingEntitiesBloc.myOrders, + builder: (context, snapshot) { + final list = snapshot.data ?? []; + final Map> coinAbbrMap = + getCoinAbbrMapFromOrderList(list, widget.isSellCoin); - return _buildCoinList(coinAbbrMap); - }); + return _buildCoinList(coinAbbrMap); + }, + ); } Widget _buildCoinList(Map> coinAbbrMap) { final List coinAbbrList = (_searchPhrase.isEmpty ? coinAbbrMap.keys.toList() - : coinAbbrMap.keys.where((String coinAbbr) => - coinAbbr.toLowerCase().contains(_searchPhrase))) + : coinAbbrMap.keys.where( + (String coinAbbr) => + coinAbbr.toLowerCase().contains(_searchPhrase), + )) .where((abbr) => abbr != widget.anotherCoin) .toList(); @@ -112,28 +116,29 @@ class _DexListFilterCoinsListState extends State { isMobile: isMobile, scrollController: scrollController, child: ListView.builder( - controller: scrollController, - shrinkWrap: true, - itemCount: coinAbbrList.length, - itemBuilder: (BuildContext context, int i) { - final coinAbbr = coinAbbrList[i]; - final String? anotherCoinAbbr = widget.anotherCoin; - final coinPairsCount = getCoinPairsCountFromCoinAbbrMap( - coinAbbrMap, - coinAbbr, - anotherCoinAbbr, - ); + controller: scrollController, + shrinkWrap: true, + itemCount: coinAbbrList.length, + itemBuilder: (BuildContext context, int i) { + final coinAbbr = coinAbbrList[i]; + final String? anotherCoinAbbr = widget.anotherCoin; + final coinPairsCount = getCoinPairsCountFromCoinAbbrMap( + coinAbbrMap, + coinAbbr, + anotherCoinAbbr, + ); - return Padding( - padding: EdgeInsets.fromLTRB( - 18, - 5.0, - 18, - lastIndex == i ? 20.0 : 0.0, - ), - child: _buildCoinListItem(coinAbbr, coinPairsCount), - ); - }), + return Padding( + padding: EdgeInsets.fromLTRB( + 18, + 5.0, + 18, + lastIndex == i ? 20.0 : 0.0, + ), + child: _buildCoinListItem(coinAbbr, coinPairsCount), + ); + }, + ), ); } @@ -153,7 +158,8 @@ class _DexListFilterCoinsListState extends State { Padding( padding: const EdgeInsets.only(left: 5.0), child: Text( - '${Coin.normalizeAbbr(coinAbbr)} ${isSegwit ? ' (segwit)' : ''}'), + '${Coin.normalizeAbbr(coinAbbr)} ${isSegwit ? ' (segwit)' : ''}', + ), ), const Spacer(), Text('($pairCount)'), diff --git a/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart b/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart index 2e6821021f..00c1f52024 100644 --- a/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart +++ b/lib/views/dex/entities_list/history/swap_history_sort_mixin.dart @@ -16,7 +16,8 @@ mixin SwapHistorySortingMixin { } List sortSwaps( - BuildContext context, List swaps, { + BuildContext context, + List swaps, { required SortData sortData, }) { final direction = sortData.sortDirection; @@ -87,10 +88,12 @@ mixin SwapHistorySortingMixin { } List _sortByPrice( - BuildContext context, List swaps, { + BuildContext context, + List swaps, { required SortDirection sortDirection, }) { - final tradingEntitiesBloc = RepositoryProvider.of(context); + final tradingEntitiesBloc = + RepositoryProvider.of(context); swaps.sort( (first, second) => sortByDouble( tradingEntitiesBloc.getPriceFromAmount( diff --git a/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart b/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart index e8ee9f41d1..824ebb4353 100644 --- a/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart +++ b/lib/views/dex/entity_details/maker_order/maker_order_details_page.dart @@ -141,9 +141,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.status.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.status.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Text(status), ], ); @@ -154,9 +157,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.orderId.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.orderId.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: InkWell( @@ -173,15 +179,20 @@ class _MakerOrderDetailsPageState extends State { TableRow _buildCreatedAt() { final String createdAt = DateFormat('dd MMM yyyy, HH:mm').format( - DateTime.fromMillisecondsSinceEpoch( - widget.makerOrderStatus.order.createdAt * 1000)); + DateTime.fromMillisecondsSinceEpoch( + widget.makerOrderStatus.order.createdAt * 1000, + ), + ); return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.createdAt.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.createdAt.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Text(createdAt), ], ); @@ -195,9 +206,12 @@ class _MakerOrderDetailsPageState extends State { return TableRow( children: [ SizedBox( - height: 30, - child: Text('${LocaleKeys.price.tr()}:', - style: Theme.of(context).textTheme.bodyLarge)), + height: 30, + child: Text( + '${LocaleKeys.price.tr()}:', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), Row( children: [ Text( @@ -207,7 +221,7 @@ class _MakerOrderDetailsPageState extends State { const SizedBox( width: 5, ), - Text(order.rel) + Text(order.rel), ], ), ], @@ -220,7 +234,8 @@ class _MakerOrderDetailsPageState extends State { _inProgress = true; }); - final tradingEntitiesBloc = RepositoryProvider.of(context); + final tradingEntitiesBloc = + RepositoryProvider.of(context); final String? error = await tradingEntitiesBloc .cancelOrder(widget.makerOrderStatus.order.uuid); diff --git a/lib/views/dex/entity_details/swap/swap_details_step.dart b/lib/views/dex/entity_details/swap/swap_details_step.dart index 5cafd31577..96ea3cd7ed 100644 --- a/lib/views/dex/entity_details/swap/swap_details_step.dart +++ b/lib/views/dex/entity_details/swap/swap_details_step.dart @@ -78,10 +78,11 @@ class SwapDetailsStep extends StatelessWidget { padding: const EdgeInsets.all(2), child: DecoratedBox( decoration: BoxDecoration( - shape: BoxShape.circle, - color: isProcessedStep || isFailedStep - ? Colors.transparent - : themeData.colorScheme.surface), + shape: BoxShape.circle, + color: isProcessedStep || isFailedStep + ? Colors.transparent + : themeData.colorScheme.surface, + ), ), ), ), @@ -148,13 +149,13 @@ class SwapDetailsStep extends StatelessWidget { size: 20, ), onTap: () => - launchURL(getTxExplorerUrl(coin, txHash)), + launchURLString(getTxExplorerUrl(coin, txHash)), ), ), ), - ) + ), ], - ) + ), ], ), ), @@ -197,16 +198,18 @@ class SwapDetailsStep extends StatelessWidget { } String _getTimeSpent(BuildContext context) { - return LocaleKeys.swapDetailsStepStatusTimeSpent.tr(args: [ - durationFormat( - Duration(milliseconds: timeSpent), - DurationLocalization( - milliseconds: LocaleKeys.milliseconds.tr(), - seconds: LocaleKeys.seconds.tr(), - minutes: LocaleKeys.minutes.tr(), - hours: LocaleKeys.hours.tr(), + return LocaleKeys.swapDetailsStepStatusTimeSpent.tr( + args: [ + durationFormat( + Duration(milliseconds: timeSpent), + DurationLocalization( + milliseconds: LocaleKeys.milliseconds.tr(), + seconds: LocaleKeys.seconds.tr(), + minutes: LocaleKeys.minutes.tr(), + hours: LocaleKeys.hours.tr(), + ), ), - ), - ]); + ], + ); } } diff --git a/lib/views/dex/entity_details/swap/swap_recover_button.dart b/lib/views/dex/entity_details/swap/swap_recover_button.dart index c2ecb991bc..e7d096bbe8 100644 --- a/lib/views/dex/entity_details/swap/swap_recover_button.dart +++ b/lib/views/dex/entity_details/swap/swap_recover_button.dart @@ -119,9 +119,10 @@ class _SwapRecoverButtonState extends State { padding: const EdgeInsets.only(top: 5.0), child: InkWell( child: Text( - '${LocaleKeys.transactionHash.tr()}: ${response.result.txHash}'), + '${LocaleKeys.transactionHash.tr()}: ${response.result.txHash}', + ), onTap: () { - launchURL(url); + launchURLString(url); }, ), ), 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 f46cd60803..42fd7f6dbb 100644 --- a/lib/views/dex/simple/form/taker/taker_form_content.dart +++ b/lib/views/dex/simple/form/taker/taker_form_content.dart @@ -24,6 +24,8 @@ import 'package:web_dex/views/dex/simple/form/taker/taker_form_exchange_info.dar import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; class TakerFormContent extends StatelessWidget { + const TakerFormContent({super.key}); + @override Widget build(BuildContext context) { return FormPlate( @@ -43,11 +45,16 @@ class TakerFormContent extends StatelessWidget { final coinsRepo = RepositoryProvider.of(context); final knownCoins = coinsRepo.getKnownCoins(); final buyCoin = knownCoins.firstWhereOrNull( - (element) => element.abbr == selectedOrder.coin); + (element) => element.abbr == selectedOrder.coin, + ); if (buyCoin == null) return false; - takerBloc.add(TakerSetSellCoin(buyCoin, - autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr),); + takerBloc.add( + TakerSetSellCoin( + buyCoin, + autoSelectOrderAbbr: takerBloc.state.sellCoin?.abbr, + ), + ); return true; }, topWidget: const TakerFormSellItem(), @@ -95,7 +102,7 @@ class _FormControls extends StatelessWidget { } class ResetSwapFormButton extends StatelessWidget { - const ResetSwapFormButton(); + const ResetSwapFormButton({super.key}); @override Widget build(BuildContext context) { @@ -109,41 +116,43 @@ class ResetSwapFormButton extends StatelessWidget { } class TradeButton extends StatelessWidget { - const TradeButton(); + const TradeButton({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, systemHealthState) { - final bool isSystemClockValid = - systemHealthState is SystemHealthLoadSuccess && - systemHealthState.isValid; + builder: (context, systemHealthState) { + final bool isSystemClockValid = + systemHealthState is SystemHealthLoadSuccess && + systemHealthState.isValid; - return BlocSelector( - selector: (state) => state.inProgress, - builder: (context, inProgress) { - final bool disabled = inProgress || !isSystemClockValid; + return BlocSelector( + selector: (state) => state.inProgress, + builder: (context, inProgress) { + final bool disabled = inProgress || !isSystemClockValid; - return Opacity( - opacity: disabled ? 0.8 : 1, - child: UiPrimaryButton( - key: const Key('take-order-button'), - text: LocaleKeys.swapNow.tr(), - prefix: inProgress ? const TradeButtonSpinner() : null, - onPressed: disabled - ? null - : () => context.read().add(TakerFormSubmitClick()), - height: isMobile ? 52 : 40, - ), - ); - }, - ); - }); + return Opacity( + opacity: disabled ? 0.8 : 1, + child: UiPrimaryButton( + key: const Key('take-order-button'), + text: LocaleKeys.swapNow.tr(), + prefix: inProgress ? const TradeButtonSpinner() : null, + onPressed: disabled + ? null + : () => + context.read().add(TakerFormSubmitClick()), + height: isMobile ? 52 : 40, + ), + ); + }, + ); + }, + ); } } class TradeButtonSpinner extends StatelessWidget { - const TradeButtonSpinner(); + const TradeButtonSpinner({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/views/dex/simple/form/taker/taker_form_layout.dart b/lib/views/dex/simple/form/taker/taker_form_layout.dart index 79572faf3c..89714a04d1 100644 --- a/lib/views/dex/simple/form/taker/taker_form_layout.dart +++ b/lib/views/dex/simple/form/taker/taker_form_layout.dart @@ -56,7 +56,7 @@ class _TakerFormDesktopLayout extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - TakerFormContent(), + const TakerFormContent(), Padding( padding: const EdgeInsets.fromLTRB(16, 52, 16, 0), child: TakerSellCoinsTable(), @@ -95,11 +95,11 @@ class _TakerFormMobileLayout extends StatelessWidget { constraints: BoxConstraints(maxWidth: theme.custom.dexFormWidth), child: Stack( children: [ - Column( + const Column( children: [ TakerFormContent(), - const SizedBox(height: 22), - const TakerOrderbook(), + SizedBox(height: 22), + TakerOrderbook(), ], ), Padding( diff --git a/lib/views/fiat/webview_dialog.dart b/lib/views/fiat/webview_dialog.dart index 84212a5c38..10d12ac004 100644 --- a/lib/views/fiat/webview_dialog.dart +++ b/lib/views/fiat/webview_dialog.dart @@ -18,7 +18,7 @@ class WebViewDialog { // `flutter_inappwebview` does not yet support Linux, so use `url_launcher` // to launch the URL in the default browser. if (!kIsWeb && !kIsWasm && Platform.isLinux) { - return launchURL(url); + return launchURLString(url); } final webviewSettings = settings ?? diff --git a/lib/views/main_layout/main_layout.dart b/lib/views/main_layout/main_layout.dart index 66ce61801d..9f6947a055 100644 --- a/lib/views/main_layout/main_layout.dart +++ b/lib/views/main_layout/main_layout.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -17,6 +16,8 @@ import 'package:web_dex/views/common/header/app_header.dart'; import 'package:web_dex/views/common/main_menu/main_menu_bar_mobile.dart'; class MainLayout extends StatefulWidget { + const MainLayout({super.key}); + @override State createState() => _MainLayoutState(); } @@ -29,10 +30,10 @@ class _MainLayoutState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { await AlphaVersionWarningService().run(); - updateBloc.init(); + await updateBloc.init(); - if (kDebugMode && !await _hasAgreedNoTrading()) { - _showDebugModeDialog().ignore(); + if (!kIsWalletOnly && !await _hasAgreedNoTrading()) { + _showNoTradingWarning().ignore(); } }); @@ -70,17 +71,19 @@ class _MainLayoutState extends State { // Method to show an alert dialog with an option to agree if the app is in // debug mode stating that trading features may not be used for actual trading // and that only test assets/networks may be used. - Future _showDebugModeDialog() async { + Future _showNoTradingWarning() async { await showDialog( context: context, barrierDismissible: false, builder: (context) { return AlertDialog( - title: const Text('Debug mode'), + title: const Text('Warning'), content: const Text( - 'This app is in debug mode. Trading features may not be used for ' - 'actual trading. Only test assets/networks may be used.', - ), + 'Trading features may not be used for actual trading. Only test ' + 'assets/networks may be used and only for development purposes. ' + 'You are solely responsible for any losses/damage that may occur.' + '\n\nKomodoPlatform does not condone the use of this app for ' + 'trading purposes and unequivocally forbids it.'), actions: [ TextButton( onPressed: () { @@ -97,11 +100,11 @@ class _MainLayoutState extends State { Future _saveAgreedState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - prefs.setBool('wallet_only_agreed', true); + prefs.setInt('wallet_only_agreed', DateTime.now().millisecondsSinceEpoch); } Future _hasAgreedNoTrading() async { SharedPreferences prefs = await SharedPreferences.getInstance(); - return prefs.getBool('wallet_only_agreed') ?? false; + return prefs.getInt('wallet_only_agreed') != null; } } diff --git a/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart b/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart index 84408b3a94..8b837dca33 100644 --- a/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart +++ b/lib/views/market_maker_bot/market_maker_form_error_message_extensions.dart @@ -18,7 +18,7 @@ extension TradeMarginValidationErrorText on TradeMarginValidationError { return LocaleKeys.postitiveNumberRequired.tr(); case TradeMarginValidationError.greaterThanMaximum: return LocaleKeys.mustBeLessThan.tr(args: [maxValue.toString()]); - } + } } } @@ -54,7 +54,7 @@ extension AmountValidationErrorText on AmountValidationError { .tr(args: [coin?.balance.toString() ?? '0', coin?.abbr ?? '']); case AmountValidationError.lessThanMinimum: return LocaleKeys.mmBotMinimumTradeVolume.tr(args: ["0.00000001"]); - } + } } } diff --git a/lib/views/market_maker_bot/trade_pair_list_item.dart b/lib/views/market_maker_bot/trade_pair_list_item.dart index 65c4660109..3d14913e87 100644 --- a/lib/views/market_maker_bot/trade_pair_list_item.dart +++ b/lib/views/market_maker_bot/trade_pair_list_item.dart @@ -36,7 +36,8 @@ class TradePairListItem extends StatelessWidget { final buyCoin = config.relCoinId; final buyAmount = order?.relAmountAvailable ?? pair.relCoinAmount; final String date = order != null ? getFormattedDate(order.createdAt) : '-'; - final tradingEntitiesBloc = RepositoryProvider.of(context); + final tradingEntitiesBloc = + RepositoryProvider.of(context); final double fillProgress = order != null ? tradingEntitiesBloc.getProgressFillSwap(pair.order!) : 0; @@ -198,6 +199,7 @@ class _OrderItemDesktop extends StatelessWidget { class TableActionsButtonList extends StatelessWidget { const TableActionsButtonList({ + super.key, required this.actions, }); diff --git a/lib/views/qr_scanner.dart b/lib/views/qr_scanner.dart deleted file mode 100644 index 14db10ac8e..0000000000 --- a/lib/views/qr_scanner.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; - -class QrScanner extends StatefulWidget { - const QrScanner({super.key}); - - @override - State createState() => _QrScannerState(); -} - -class _QrScannerState extends State { - bool qrDetected = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(LocaleKeys.qrScannerTitle.tr()), - foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, - elevation: 0, - ), - body: MobileScanner( - controller: MobileScannerController( - detectionTimeoutMs: 1000, - formats: [BarcodeFormat.qrCode], - ), - errorBuilder: _buildQrScannerError, - onDetect: (capture) { - if (qrDetected) return; - - final List qrCodes = capture.barcodes; - - if (qrCodes.isNotEmpty) { - final r = qrCodes.first.rawValue; - qrDetected = true; - - // MRC: Guarantee that we don't try to close the current screen - // if it was already closed - if (!context.mounted) return; - Navigator.pop(context, r); - } - }, - placeholderBuilder: (context, _) => const Center( - child: CircularProgressIndicator(), - ), - ), - ); - } - - Widget _buildQrScannerError( - BuildContext context, MobileScannerException exception, _) { - late String errorMessage; - - switch (exception.errorCode) { - case MobileScannerErrorCode.controllerUninitialized: - errorMessage = LocaleKeys.qrScannerErrorControllerUninitialized.tr(); - break; - case MobileScannerErrorCode.permissionDenied: - errorMessage = LocaleKeys.qrScannerErrorPermissionDenied.tr(); - break; - case MobileScannerErrorCode.genericError: - default: - errorMessage = LocaleKeys.qrScannerErrorGenericError.tr(); - } - - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon( - Icons.warning, - color: Colors.yellowAccent, - size: 64, - ), - const SizedBox(height: 8), - Text( - LocaleKeys.qrScannerErrorTitle.tr(), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 32), - Text(errorMessage, style: Theme.of(context).textTheme.bodyLarge), - if (exception.errorDetails != null) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '${LocaleKeys.errorCode.tr()}: ${exception.errorDetails!.code}'), - Text( - '${LocaleKeys.errorDetails.tr()}: ${exception.errorDetails!.details}'), - Text( - '${LocaleKeys.errorMessage.tr()}: ${exception.errorDetails!.message}'), - ], - ), - ], - ), - ); - } -} diff --git a/lib/views/settings/widgets/general_settings/app_version_number.dart b/lib/views/settings/widgets/general_settings/app_version_number.dart index fb863036fa..8c862df4ad 100644 --- a/lib/views/settings/widgets/general_settings/app_version_number.dart +++ b/lib/views/settings/widgets/general_settings/app_version_number.dart @@ -59,7 +59,6 @@ class _BundledCoinsCommitConfig extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -104,8 +103,10 @@ class _ApiVersion extends StatelessWidget { final String? commitHash = _tryParseCommitHash(snapshot.data); if (commitHash == null) return const SizedBox.shrink(); - return SelectableText('${LocaleKeys.api.tr()}: $commitHash', - style: _textStyle); + return SelectableText( + '${LocaleKeys.api.tr()}: $commitHash', + style: _textStyle, + ); }, ), ), diff --git a/lib/views/settings/widgets/support_page/support_page.dart b/lib/views/settings/widgets/support_page/support_page.dart index 64fa74f7e7..e8dccd5f92 100644 --- a/lib/views/settings/widgets/support_page/support_page.dart +++ b/lib/views/settings/widgets/support_page/support_page.dart @@ -38,72 +38,82 @@ class SupportPage extends StatelessWidget { color: Theme.of(context).colorScheme.surface, ), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Visibility( - visible: !isMobile, - child: SelectableText(LocaleKeys.support.tr(), - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w700)), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Visibility( + visible: !isMobile, + child: SelectableText( + LocaleKeys.support.tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + ), ), - const SizedBox( - height: 16, + ), + const SizedBox( + height: 16, + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(18.0), ), - Container( - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(18.0)), - child: Stack( - children: [ - const _DiscordIcon(), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 18.0, horizontal: 5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(right: isMobile ? 0 : 160), - child: SelectableText( - LocaleKeys.supportAskSpan.tr(), - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), + child: Stack( + children: [ + const _DiscordIcon(), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 18.0, + horizontal: 5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(right: isMobile ? 0 : 160), + child: SelectableText( + LocaleKeys.supportAskSpan.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, ), ), - const SizedBox( - height: 12, + ), + const SizedBox( + height: 12, + ), + UiBorderButton( + backgroundColor: Theme.of(context).colorScheme.surface, + prefix: Icon( + Icons.discord, + color: Theme.of(context).textTheme.bodyMedium?.color, ), - UiBorderButton( - backgroundColor: - Theme.of(context).colorScheme.surface, - prefix: Icon( - Icons.discord, - color: - Theme.of(context).textTheme.bodyMedium?.color, - ), - text: LocaleKeys.supportDiscordButton.tr(), - fontSize: isMobile ? 13 : 14, - width: 400, - height: 40, - allowMultiline: true, - onPressed: () { - launchURL('https://komodoplatform.com/discord'); - }) - ], - ), - ) - ], - ), - ), - const SizedBox(height: 20), - SelectableText( - LocaleKeys.supportFrequentlyQuestionSpan.tr(), - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + text: LocaleKeys.supportDiscordButton.tr(), + fontSize: isMobile ? 13 : 14, + width: 400, + height: 40, + allowMultiline: true, + onPressed: () { + launchURLString( + 'https://komodoplatform.com/discord', + ); + }, + ), + ], + ), + ), + ], ), - const SizedBox(height: 30), - /* + ), + const SizedBox(height: 20), + SelectableText( + LocaleKeys.supportFrequentlyQuestionSpan.tr(), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 30), + /* if (!isMobile) Flexible( child: DexScrollbar( @@ -121,16 +131,18 @@ class SupportPage extends StatelessWidget { ), if (isMobile) */ - Container( - padding: const EdgeInsets.fromLTRB(0, 0, 12, 0), - child: Column( - children: supportInfo.asMap().entries.map((entry) { + Container( + padding: const EdgeInsets.fromLTRB(0, 0, 12, 0), + child: Column( + children: supportInfo.asMap().entries.map((entry) { return SupportItem( data: entry.value, ); - }).toList()), - ) - ]), + }).toList(), + ), + ), + ], + ), ); } } @@ -161,42 +173,42 @@ class _DiscordIcon extends StatelessWidget { final List> supportInfo = [ { 'title': LocaleKeys.supportInfoTitle1.tr(), - 'content': LocaleKeys.supportInfoContent1.tr() + 'content': LocaleKeys.supportInfoContent1.tr(), }, { 'title': LocaleKeys.supportInfoTitle2.tr(), - 'content': LocaleKeys.supportInfoContent2.tr() + 'content': LocaleKeys.supportInfoContent2.tr(), }, { 'title': LocaleKeys.supportInfoTitle3.tr(), - 'content': LocaleKeys.supportInfoContent3.tr() + 'content': LocaleKeys.supportInfoContent3.tr(), }, { 'title': LocaleKeys.supportInfoTitle4.tr(), - 'content': LocaleKeys.supportInfoContent4.tr() + 'content': LocaleKeys.supportInfoContent4.tr(), }, { 'title': LocaleKeys.supportInfoTitle5.tr(), - 'content': LocaleKeys.supportInfoContent5.tr() + 'content': LocaleKeys.supportInfoContent5.tr(), }, { 'title': LocaleKeys.supportInfoTitle6.tr(), - 'content': LocaleKeys.supportInfoContent6.tr() + 'content': LocaleKeys.supportInfoContent6.tr(), }, { 'title': LocaleKeys.supportInfoTitle7.tr(), - 'content': LocaleKeys.supportInfoContent7.tr() + 'content': LocaleKeys.supportInfoContent7.tr(), }, { 'title': LocaleKeys.supportInfoTitle8.tr(), - 'content': LocaleKeys.supportInfoContent8.tr() + 'content': LocaleKeys.supportInfoContent8.tr(), }, { 'title': LocaleKeys.supportInfoTitle9.tr(), - 'content': LocaleKeys.supportInfoContent9.tr() + 'content': LocaleKeys.supportInfoContent9.tr(), }, { 'title': LocaleKeys.supportInfoTitle10.tr(), - 'content': LocaleKeys.supportInfoContent10.tr() + 'content': LocaleKeys.supportInfoContent10.tr(), } ]; diff --git a/lib/views/wallet/coin_details/coin_details.dart b/lib/views/wallet/coin_details/coin_details.dart index 4b1a751d86..5c38f2a827 100644 --- a/lib/views/wallet/coin_details/coin_details.dart +++ b/lib/views/wallet/coin_details/coin_details.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_event.dart'; @@ -37,16 +38,14 @@ class _CoinDetailsState extends State { void initState() { _txHistoryBloc = context.read(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - context - .read() - .add(TransactionHistorySubscribe(coin: widget.coin)); + _txHistoryBloc.add(TransactionHistorySubscribe(coin: widget.coin)); }); super.initState(); } @override void dispose() { - _txHistoryBloc.add(TransactionHistoryUnsubscribe(coin: widget.coin)); + // _txHistoryBloc.add(TransactionHistoryUnsubscribe(coin: widget.coin)); super.dispose(); } @@ -70,9 +69,9 @@ class _CoinDetailsState extends State { case CoinPageType.send: return WithdrawForm( - coin: widget.coin, + asset: widget.coin.toSdkAsset(context.read()), + onSuccess: _openInfo, onBackButtonPressed: _openInfo, - onSuccess: () => _setPageType(CoinPageType.info), ); case CoinPageType.receive: diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart index 904c51100c..7e956bcb45 100644 --- a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart +++ b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_growth_chart.dart @@ -2,6 +2,7 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.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/cex_market_data/portfolio_growth/portfolio_growth_bloc.dart'; @@ -89,8 +90,14 @@ class _PortfolioGrowthChartState extends State { NumberFormat.currency(symbol: '\$', decimalDigits: 2) .format(totalValue), ), - availableCoins: - widget.initialCoins.map((coin) => coin.abbr).toList(), + availableCoins: widget.initialCoins + .map( + (coin) => getSdkAsset( + context.read(), + coin.abbr, + ).id, + ) + .toList(), selectedCoinId: _singleCoinOrNull?.abbr, onCoinSelected: _isCoinPage ? null : _showSpecificCoin, centreAmount: totalValue, diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart index f7bb543db5..ad5f33f3b8 100644 --- a/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart +++ b/lib/views/wallet/coin_details/coin_details_info/charts/portfolio_profit_loss_chart.dart @@ -2,12 +2,13 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/cex_market_data/profit_loss/profit_loss_bloc.dart'; import 'package:web_dex/blocs/current_wallet_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/utils/prominent_colors.dart'; +import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; class PortfolioProfitLossChart extends StatefulWidget { @@ -103,8 +104,14 @@ class PortfolioProfitLossChartState extends State { ), leadingText: Text(formattedValue), emptySelectAllowed: !_isCoinPage, - availableCoins: - widget.initialCoins.map((coin) => coin.abbr).toList(), + availableCoins: widget.initialCoins + .map( + (coin) => getSdkAsset( + context.read(), + coin.abbr, + ).id, + ) + .toList(), selectedCoinId: _singleCoinOrNull?.abbr, onCoinSelected: _isCoinPage ? null : _showSpecificCoin, centreAmount: totalValue, 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 22ca7af244..00a490ad1e 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 @@ -3,7 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; @@ -30,91 +30,97 @@ class CoinAddresses extends StatelessWidget { @override Widget build(BuildContext context) { final kdfSdk = RepositoryProvider.of(context); - return BlocBuilder(builder: (context, state) { - return BlocProvider( - create: (context) => CoinAddressesBloc( - kdfSdk, - coin.abbr, - )..add(const LoadAddressesEvent()), - child: BlocBuilder( - builder: (context, state) { - return SliverToBoxAdapter( - child: Column( - children: [ - Card( - margin: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - color: theme.custom.dexPageTheme.frontPlate, - child: Padding( - padding: EdgeInsets.all(isMobile ? 16.0 : 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _Header( - status: state.status, - createAddressStatus: state.createAddressStatus, - hideZeroBalance: state.hideZeroBalance, - cantCreateNewAddressReasons: - state.cantCreateNewAddressReasons, - ), - const SizedBox(height: 12), - ...state.addresses.asMap().entries.map( - (entry) { - final index = entry.key; - final address = entry.value; - if (state.hideZeroBalance && - !address.balance.hasBalance) { - return const SizedBox(); - } - - return AddressCard( - address: address, - index: index, - coin: coin, - ); - }, - ).toList(), - if (state.status == FormStatus.submitting) - const Padding( - padding: EdgeInsets.symmetric(vertical: 20.0), - child: Center(child: CircularProgressIndicator()), + return BlocBuilder( + builder: (context, state) { + return BlocProvider( + create: (context) => CoinAddressesBloc( + kdfSdk, + coin.abbr, + )..add(const LoadAddressesEvent()), + child: BlocBuilder( + builder: (context, state) { + return SliverToBoxAdapter( + child: Column( + children: [ + Card( + margin: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + color: theme.custom.dexPageTheme.frontPlate, + child: Padding( + padding: EdgeInsets.all(isMobile ? 16.0 : 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Header( + status: state.status, + createAddressStatus: state.createAddressStatus, + hideZeroBalance: state.hideZeroBalance, + cantCreateNewAddressReasons: + state.cantCreateNewAddressReasons, ), - if (state.status == FormStatus.failure || - state.createAddressStatus == FormStatus.failure) - Padding( - padding: - const EdgeInsets.symmetric(vertical: 20.0), - child: Center( + const SizedBox(height: 12), + ...state.addresses.asMap().entries.map( + (entry) { + final index = entry.key; + final address = entry.value; + if (state.hideZeroBalance && + !address.balance.hasBalance) { + return const SizedBox(); + } + + return AddressCard( + address: address, + index: index, + coin: coin, + ); + }, + ).toList(), + if (state.status == FormStatus.submitting) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: + Center(child: CircularProgressIndicator()), + ), + if (state.status == FormStatus.failure || + state.createAddressStatus == FormStatus.failure) + Padding( + padding: + const EdgeInsets.symmetric(vertical: 20.0), + child: Center( child: Text( - state.errorMessage ?? - LocaleKeys.somethingWrong.tr(), - style: TextStyle( - color: theme.currentGlobal.colorScheme - .error))), - ), - ], + state.errorMessage ?? + LocaleKeys.somethingWrong.tr(), + style: TextStyle( + color: + theme.currentGlobal.colorScheme.error, + ), + ), + ), + ), + ], + ), ), ), - ), - if (isMobile) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: CreateButton( - status: state.status, - createAddressStatus: state.createAddressStatus, - cantCreateNewAddressReasons: - state.cantCreateNewAddressReasons, + if (isMobile) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CreateButton( + status: state.status, + createAddressStatus: state.createAddressStatus, + cantCreateNewAddressReasons: + state.cantCreateNewAddressReasons, + ), ), - ), - ], - ), - ); - }, - ), - ); - }); + ], + ), + ); + }, + ), + ); + }, + ); } } 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 7f3486e3a0..385e53f73c 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 @@ -277,7 +277,8 @@ class CoinDetailsSendButton extends StatelessWidget { textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, - onPressed: coin.isSuspended || coin.balance == 0 + onPressed: coin.isSuspended + //TODO!.sdk || coin.balance == 0 ? null : () { selectWidget(CoinPageType.send); 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 b09ab49eac..04985c82fb 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 @@ -3,7 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:komodo_defi_types/types.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/auth_bloc/auth_bloc.dart'; @@ -348,8 +348,8 @@ class _CoinDetailsInfoHeader extends StatelessWidget { isMobile: true, selectWidget: setPageType, onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() - ? null - : () => _goToSwap(context, coin), + ? () => _goToSwap(context, coin) + : null, coin: coin, ), ), @@ -430,7 +430,7 @@ class _CoinDetailsMarketMetricsTabBar extends StatelessWidget { ]), ], ), - ) + ), ], ); } diff --git a/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart b/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart index ff5161e1f8..fb7feb41bc 100644 --- a/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart +++ b/lib/views/wallet/coin_details/coin_details_info/contract_address_button.dart @@ -23,8 +23,9 @@ class ContractAddressButton extends StatelessWidget { onTap: coin.explorerUrl.isEmpty ? null : () { - launchURL( - '${coin.explorerUrl}address/${coin.protocolData?.contractAddress ?? ''}'); + launchURLString( + '${coin.explorerUrl}address/${coin.protocolData?.contractAddress ?? ''}', + ); }, child: isMobile ? _ContractAddressMobile(coin) @@ -91,7 +92,7 @@ class _ContractAddressDesktop extends StatelessWidget { height: 16, child: _ContractAddressCopyButton(coin), ), - ) + ), ], ), ), @@ -133,12 +134,14 @@ class _ContractAddressValue extends StatelessWidget { ?.copyWith(fontWeight: FontWeight.w500, fontSize: 11), ), Flexible( - child: TruncatedMiddleText(coin.protocolData?.contractAddress ?? '', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: Theme.of(context).textTheme.bodyMedium?.color, - )), + child: TruncatedMiddleText( + coin.protocolData?.contractAddress ?? '', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), ), ], ); diff --git a/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart b/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart index 4e04b16062..3a01846956 100644 --- a/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart +++ b/lib/views/wallet/coin_details/faucet/cubit/faucet_cubit.dart @@ -16,9 +16,9 @@ class FaucetCubit extends Cubit { Future callFaucet() async { emit(const FaucetLoading()); try { - // Temporary band-aid fix to faucet to support HD wallet - currently + // Temporary band-aid fix to faucet to support HD wallet - currently // defaults to calling faucet on all addresses - // TODO: maybe add faucet button per address, or ask user if they want + // TODO: maybe add faucet button per address, or ask user if they want // to faucet all addresses at once (or offer both options) final asset = kdfSdk.assets.assetsFromTicker(coinAbbr).single; final addresses = (await asset.getPubkeys(kdfSdk)).keys; diff --git a/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart b/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart index 710872265e..fa2f246f09 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_reward_info_header.dart @@ -74,7 +74,7 @@ class KmdRewardInfoHeader extends StatelessWidget { style: const TextStyle(color: Colors.blue), recognizer: TapGestureRecognizer() ..onTap = () { - launchURL('https://www.coingecko.com'); + launchURLString('https://www.coingecko.com'); }, ), const TextSpan(text: ', '), @@ -83,7 +83,7 @@ class KmdRewardInfoHeader extends StatelessWidget { style: const TextStyle(color: Colors.blue), recognizer: TapGestureRecognizer() ..onTap = () { - launchURL('https://exchangeratesapi.io'); + launchURLString('https://exchangeratesapi.io'); }, ), const TextSpan(text: ')'), diff --git a/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart b/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart index 7f46a60386..a1475be911 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_rewards_info.dart @@ -129,65 +129,69 @@ class _KmdRewardsInfoState extends State { ), const Spacer(), Container( - width: 350, - height: 177, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: theme.custom.userRewardBoxColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 7), - blurRadius: 10, - color: theme.custom.rewardBoxShadowColor) - ]), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.rewardBoxTitle.tr(), - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 20, - ), - ), - Text( - LocaleKeys.rewardBoxSubTitle.tr(), - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withValues(alpha: 0.4), - ), + width: 350, + height: 177, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: theme.custom.userRewardBoxColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, 7), + blurRadius: 10, + color: theme.custom.rewardBoxShadowColor, + ), + ], + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.rewardBoxTitle.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, ), - const SizedBox( - height: 30.0, + ), + Text( + LocaleKeys.rewardBoxSubTitle.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.4), ), - UiBorderButton( - width: 160, - height: 38, - text: LocaleKeys.rewardBoxReadMore.tr(), - onPressed: () { - launchURL( - 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know'); - }, - ) - ], - ), + ), + const SizedBox( + height: 30.0, + ), + UiBorderButton( + width: 160, + height: 38, + text: LocaleKeys.rewardBoxReadMore.tr(), + onPressed: () { + launchURLString( + 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know', + ); + }, + ), + ], ), - const Positioned( - bottom: 0, - right: 0, - child: RewardBackground(), - ) - ], - )) + ), + const Positioned( + bottom: 0, + right: 0, + child: RewardBackground(), + ), + ], + ), + ), ], ), const SizedBox(height: 20), @@ -223,79 +227,85 @@ class _KmdRewardsInfoState extends State { mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16.0), - color: Theme.of(context).colorScheme.surface), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: double.infinity, - height: 177, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: theme.custom.userRewardBoxColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 7), - blurRadius: 10, - color: theme.custom.rewardBoxShadowColor) - ]), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.rewardBoxTitle.tr(), - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 20, - ), - ), - Text( - LocaleKeys.rewardBoxSubTitle.tr(), - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withValues(alpha: 0.3), - ), - ), - const SizedBox(height: 24.0), - UiBorderButton( - width: 160, - height: 38, - text: LocaleKeys.rewardBoxReadMore.tr(), - onPressed: () { - launchURL( - 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know'); - }, - ) - ], + padding: const EdgeInsets.fromLTRB(20, 20, 20, 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + color: Theme.of(context).colorScheme.surface, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 177, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: theme.custom.userRewardBoxColor, + boxShadow: [ + BoxShadow( + offset: const Offset(0, 7), + blurRadius: 10, + color: theme.custom.rewardBoxShadowColor, + ), + ], + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.rewardBoxTitle.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 20, + ), + ), + Text( + LocaleKeys.rewardBoxSubTitle.tr(), + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.3), + ), + ), + const SizedBox(height: 24.0), + UiBorderButton( + width: 160, + height: 38, + text: LocaleKeys.rewardBoxReadMore.tr(), + onPressed: () { + launchURLString( + 'https://support.komodoplatform.com/support/solutions/articles/29000024428-komodo-5-active-user-reward-all-you-need-to-know', + ); + }, ), - ), - const Positioned( - bottom: 0, - right: 0, - child: RewardBackground(), - ) - ], - )), - const SizedBox(height: 20.0), - _buildTotal(), - _buildMessage(), - const SizedBox(height: 20), - _buildControls(context), - ], - )), - Flexible(child: _buildContent(context)) + ], + ), + ), + const Positioned( + bottom: 0, + right: 0, + child: RewardBackground(), + ), + ], + ), + ), + const SizedBox(height: 20.0), + _buildTotal(), + _buildMessage(), + const SizedBox(height: 20), + _buildControls(context), + ], + ), + ), + Flexible(child: _buildContent(context)), ], ); } @@ -303,27 +313,27 @@ class _KmdRewardsInfoState extends State { Widget _buildRewardList(BuildContext context) { final scrollController = ScrollController(); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: [ - isDesktop ? _buildRewardListHeader(context) : const SizedBox(), - const SizedBox(height: 10), - Flexible( - child: DexScrollbar( - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Column( - children: - (_rewards ?? []).map(_buildRewardLstItem).toList(), - ), + padding: const EdgeInsets.symmetric(horizontal: 0.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.min, + children: [ + isDesktop ? _buildRewardListHeader(context) : const SizedBox(), + const SizedBox(height: 10), + Flexible( + child: DexScrollbar( + scrollController: scrollController, + child: SingleChildScrollView( + controller: scrollController, + child: Column( + children: (_rewards ?? []).map(_buildRewardLstItem).toList(), ), ), ), - ], - )); + ), + ], + ), + ); } Widget _buildRewardListHeader(BuildContext context) { @@ -434,7 +444,9 @@ class _KmdRewardsInfoState extends State { } bool _rewardsEquals( - List previous, List current) { + List previous, + List current, + ) { if (previous.length != current.length) return false; for (int i = 0; i < previous.length; i++) { diff --git a/lib/views/wallet/coin_details/transactions/transaction_details.dart b/lib/views/wallet/coin_details/transactions/transaction_details.dart index de0e753e0d..964f1a6a87 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_details.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_details.dart @@ -2,7 +2,8 @@ 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:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; @@ -28,10 +29,11 @@ class TransactionDetails extends StatelessWidget { @override Widget build(BuildContext context) { final EdgeInsets padding = EdgeInsets.only( - top: isMobile ? 16 : 0, - left: 16, - right: 16, - bottom: isMobile ? 20 : 30); + top: isMobile ? 16 : 0, + left: 16, + right: 16, + bottom: isMobile ? 20 : 30, + ); final scrollController = ScrollController(); return DexScrollbar( @@ -124,8 +126,11 @@ class TransactionDetails extends StatelessWidget { ); } - Widget _buildAddress(BuildContext context, - {required String title, required String address}) { + Widget _buildAddress( + BuildContext context, { + required String title, + required String address, + }) { return Padding( padding: const EdgeInsets.only(bottom: 20), child: Row( @@ -238,7 +243,7 @@ class TransactionDetails extends StatelessWidget { color: theme.custom.defaultGradientButtonTextColor, ), onPressed: () { - launchURL(getTxExplorerUrl(coin, transaction.txHash ?? '')); + launchURLString(getTxExplorerUrl(coin, transaction.txHash ?? '')); }, text: LocaleKeys.viewOnExplorer.tr(), ), @@ -259,7 +264,7 @@ class TransactionDetails extends StatelessWidget { } Widget _buildFee(BuildContext context) { - final String? fee = transaction.fee?.amount.toString(); + final String? fee = transaction.fee?.formatTotal(); final String formattedFee = getNumberWithoutExponent(double.parse(fee ?? '').abs().toString()); final coinsBloc = context.read(); diff --git a/lib/views/wallet/coin_details/transactions/transaction_list.dart b/lib/views/wallet/coin_details/transactions/transaction_list.dart index c7d96e3ee6..f7e53820cc 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_list.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_list.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/views/wallet/coin_details/transactions/transaction_list_item.dart'; class TransactionList extends StatelessWidget { diff --git a/lib/views/wallet/coin_details/transactions/transaction_list_item.dart b/lib/views/wallet/coin_details/transactions/transaction_list_item.dart index 786cd3e9da..f69680a286 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_list_item.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_list_item.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/custom_tooltip.dart'; import 'package:web_dex/shared/utils/formatters.dart'; @@ -61,15 +61,15 @@ class _TransactionListRowState extends State { }, hoverColor: Theme.of(context).primaryColor.withAlpha(20), child: Container( - color: _hasFocus - ? Theme.of(context).colorScheme.tertiary - : Colors.transparent, - margin: EdgeInsets.symmetric(vertical: isMobile ? 5 : 0), - padding: isMobile - ? const EdgeInsets.only(bottom: 12) - : const EdgeInsets.all(6), - child: - isMobile ? _buildMobileRow(context) : _buildNormalRow(context)), + color: _hasFocus + ? Theme.of(context).colorScheme.tertiary + : Colors.transparent, + margin: EdgeInsets.symmetric(vertical: isMobile ? 5 : 0), + padding: isMobile + ? const EdgeInsets.only(bottom: 12) + : const EdgeInsets.all(6), + child: isMobile ? _buildMobileRow(context) : _buildNormalRow(context), + ), onTap: () => widget.setTransaction(widget.transaction), ), ); @@ -102,11 +102,12 @@ class _TransactionListRowState extends State { Text( '${Coin.normalizeAbbr(widget.transaction.assetId.id)} $formatted', style: TextStyle( - color: _isReceived - ? theme.custom.increaseColor - : theme.custom.decreaseColor, - fontSize: 14, - fontWeight: FontWeight.w500), + color: _isReceived + ? theme.custom.increaseColor + : theme.custom.decreaseColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), ], ); @@ -127,7 +128,7 @@ class _TransactionListRowState extends State { style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w400), ), ], - ) + ), ], ); } @@ -193,14 +194,15 @@ class _TransactionListRowState extends State { children: [ Expanded(flex: 4, child: _buildAddress()), Expanded( - flex: 4, - child: Text( - _isReceived ? LocaleKeys.receive.tr() : LocaleKeys.send.tr(), - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), - )), + flex: 4, + child: Text( + _isReceived ? LocaleKeys.receive.tr() : LocaleKeys.send.tr(), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), Expanded(flex: 4, child: _buildBalanceChanges()), Expanded(flex: 4, child: _buildUsdChanges()), Expanded(flex: 3, child: _buildMemoAndDate()), @@ -213,32 +215,33 @@ class _TransactionListRowState extends State { if (memo == null || memo.isEmpty) return const SizedBox(); return CustomTooltip( - maxWidth: 200, - tooltip: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${LocaleKeys.memo.tr()}:', - style: theme.currentGlobal.textTheme.bodyLarge, - ), - const SizedBox(height: 6), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 120), - child: SingleChildScrollView( - controller: ScrollController(), - child: Text( - memo, - style: const TextStyle(fontSize: 14), - ), + maxWidth: 200, + tooltip: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${LocaleKeys.memo.tr()}:', + style: theme.currentGlobal.textTheme.bodyLarge, + ), + const SizedBox(height: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 120), + child: SingleChildScrollView( + controller: ScrollController(), + child: Text( + memo, + style: const TextStyle(fontSize: 14), ), ), - ], - ), - child: Icon( - Icons.note, - size: 14, - color: theme.currentGlobal.colorScheme.onSurface, - )); + ), + ], + ), + child: Icon( + Icons.note, + size: 14, + color: theme.currentGlobal.colorScheme.onSurface, + ), + ); } Widget _buildUsdChanges() { @@ -250,11 +253,12 @@ class _TransactionListRowState extends State { return Text( '$_sign \$${formatAmt((usdChanges ?? 0).abs())}', style: TextStyle( - color: _isReceived - ? theme.custom.increaseColor - : theme.custom.decreaseColor, - fontSize: 14, - fontWeight: FontWeight.w500), + color: _isReceived + ? theme.custom.increaseColor + : theme.custom.decreaseColor, + fontSize: 14, + fontWeight: FontWeight.w500, + ), ); } diff --git a/lib/views/wallet/coin_details/transactions/transaction_table.dart b/lib/views/wallet/coin_details/transactions/transaction_table.dart index 2b541c7131..5ca6badd44 100644 --- a/lib/views/wallet/coin_details/transactions/transaction_table.dart +++ b/lib/views/wallet/coin_details/transactions/transaction_table.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_bloc.dart'; import 'package:web_dex/bloc/transaction_history/transaction_history_state.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:komodo_defi_types/types.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/widgets/launch_native_explorer_button.dart'; @@ -68,21 +68,19 @@ class TransactionTable extends StatelessWidget { Widget _buildTransactionList(BuildContext context) { return BlocBuilder( builder: (BuildContext ctx, TransactionHistoryState state) { - if (state.transactions.isEmpty) { - if (coin.isActivating || state.loading) { - return const SliverToBoxAdapter( - child: UiSpinnerList(), - ); - } - - if (state.error != null) { - return SliverToBoxAdapter( - child: _ErrorMessage( - text: state.error!.message, - textColor: theme.currentGlobal.colorScheme.error, - ), - ); - } + if (state.transactions.isEmpty && state.loading) { + return const SliverToBoxAdapter( + child: UiSpinnerList(), + ); + } + + if (state.error != null) { + return SliverToBoxAdapter( + child: _ErrorMessage( + text: state.error!.message, + textColor: theme.currentGlobal.colorScheme.error, + ), + ); } return _TransactionsListWrapper( diff --git a/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart b/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart index 91fd802e92..d2fbdf7ad3 100644 --- a/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart +++ b/lib/views/wallet/coin_details/withdraw_form/pages/failed_page.dart @@ -6,12 +6,13 @@ import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/coin_details/constants.dart'; class FailedPage extends StatelessWidget { - const FailedPage(); + const FailedPage({super.key}); @override Widget build(BuildContext context) { @@ -60,8 +61,10 @@ class _SendErrorHeader extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - Text(LocaleKeys.errorDescription.tr(), - style: Theme.of(context).textTheme.bodyMedium), + Text( + LocaleKeys.errorDescription.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), ], ); } @@ -72,9 +75,10 @@ class _SendErrorBody extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.sendError.message, - builder: (BuildContext context, String errorText) { + return BlocSelector( + // TODO: Confirm this is the correct error + selector: (state) => state.transactionError, + builder: (BuildContext context, error) { final iconColor = Theme.of(context) .textTheme .bodyMedium @@ -85,7 +89,9 @@ class _SendErrorBody extends StatelessWidget { color: theme.custom.buttonColorDefault, borderRadius: BorderRadius.circular(18), child: InkWell( - onTap: () => copyToClipBoard(context, errorText), + onTap: error == null + ? null + : () => copyToClipBoard(context, error.error), borderRadius: BorderRadius.circular(18), child: Padding( padding: const EdgeInsets.all(20.0), @@ -95,7 +101,7 @@ class _SendErrorBody extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _MultilineText(errorText)), + Expanded(child: _MultilineText(error?.error ?? '')), const SizedBox(width: 16), Icon( Icons.copy_rounded, diff --git a/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart b/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart deleted file mode 100644 index b979076a5b..0000000000 --- a/lib/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/common/screen.dart'; -import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/views/wallet/coin_details/constants.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_title.dart'; - -class FillFormPage extends StatelessWidget { - const FillFormPage(); - - @override - Widget build(BuildContext context) { - final double maxWidth = isMobile ? double.infinity : withdrawWidth; - final state = context.watch().state; - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - isMobile ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FillFormTitle(state.coin.abbr), - const SizedBox(height: 28), - if (state.coin.enabledType == WalletType.trezor) - Padding( - padding: const EdgeInsets.only(bottom: 20.0), - child: FillFormTrezorSenderAddress( - coin: state.coin, - addresses: state.senderAddresses, - selectedAddress: state.selectedSenderAddress, - ), - ), - FillFormRecipientAddress(), - const SizedBox(height: 20), - FillFormAmount(), - if (state.coin.isTxMemoSupported) - const Padding( - padding: EdgeInsets.only(top: 20), - child: FillFormMemo(), - ), - if (state.coin.isCustomFeeSupported) - Padding( - padding: const EdgeInsets.only(top: 9.0), - child: FillFormCustomFee(), - ), - const SizedBox(height: 10), - const FillFormError(), - ], - ), - const SizedBox(height: 10), - FillFormFooter(), - ], - ), - ); - } -} diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart index 8025a5cf3b..6faa12e407 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart @@ -7,7 +7,7 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/shared/ui/ui_primary_button.dart'; class ConvertAddressButton extends StatelessWidget { - const ConvertAddressButton(); + const ConvertAddressButton({super.key}); @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class ConvertAddressButton extends StatelessWidget { ), onPressed: () => context .read() - .add(const WithdrawFormConvertAddress()), + .add(const WithdrawFormConvertAddressRequested()), ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart index a724bae060..0b9ebf28a8 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart @@ -5,7 +5,7 @@ import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; class SellMaxButton extends StatefulWidget { - const SellMaxButton(); + const SellMaxButton({super.key}); @override State createState() => _SellMaxButtonState(); @@ -27,7 +27,7 @@ class _SellMaxButtonState extends State { }), onTap: () => context .read() - .add(WithdrawFormMaxTapped(isEnabled: !state.isMaxAmount)), + .add(WithdrawFormMaxAmountEnabled(!state.isMaxAmount)), borderRadius: BorderRadius.circular(7), child: Container( width: 46, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart index 88e8cfd506..12708dd114 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart @@ -1,6 +1,8 @@ +import 'package:decimal/decimal.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -8,6 +10,8 @@ import 'package:web_dex/shared/constants.dart'; import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; class CustomFeeFieldEVM extends StatefulWidget { + const CustomFeeFieldEVM({super.key}); + @override State createState() => _CustomFeeFieldEVMState(); } @@ -35,32 +39,59 @@ class _CustomFeeFieldEVMState extends State { } Widget _buildGasLimitField() { - return BlocSelector( - selector: (state) { - return state.gasLimitError.message; + return CustomNumericTextFormField( + controller: _gasLimitController, + validationMode: InputValidationMode.aggressive, + validator: (_) { + const error = null; //TODO!.SDK + if (error.isEmpty) return null; + return error; + }, + onChanged: (_) { + _change(); + }, + filteringRegExp: r'^(|[1-9]\d*)$', + style: _style, + hintText: LocaleKeys.gasLimit.tr(), + hintTextStyle: _hintTextStyle, + ); + } + + Widget _buildGasPriceField() { + return BlocConsumer( + listenWhen: (previous, current) => + // TODO!.SDK: Add custom fee error property error to state and add here + previous.customFee != current.customFee, + listener: (context, state) { + // }, builder: (context, error) { - return BlocSelector( + return BlocSelector( selector: (state) { - return state.customFee.gas?.toString() ?? ''; + if (state.customFee is! FeeInfoEthGas) return null; + return (state.customFee as FeeInfoEthGas); }, - builder: (context, gasLimit) { - _gasLimitController - ..text = gasLimit - ..selection = _gasLimitSelection; + builder: (context, fee) { + // final price = fee?.gasPrice.toString() ?? ''; + + // _gasPriceController + // ..text = price + // ..selection = _gasPriceSelection; return CustomNumericTextFormField( - controller: _gasLimitController, + controller: _gasPriceController, validationMode: InputValidationMode.aggressive, validator: (_) { + const error = null; //TODO!.SDK if (error.isEmpty) return null; return error; }, onChanged: (_) { _change(); }, - filteringRegExp: r'^(|[1-9]\d*)$', + filteringRegExp: numberRegExp.pattern, style: _style, - hintText: LocaleKeys.gasLimit.tr(), + hintText: LocaleKeys.gasPriceGwei.tr(), hintTextStyle: _hintTextStyle, ); }, @@ -69,50 +100,21 @@ class _CustomFeeFieldEVMState extends State { ); } - Widget _buildGasPriceField() { - return BlocSelector( - selector: (state) { - return state.gasLimitError.message; - }, builder: (context, error) { - return BlocSelector( - selector: (state) { - return state.customFee.gasPrice ?? ''; - }, - builder: (context, gasPrice) { - final String price = gasPrice; - - _gasPriceController - ..text = price - ..selection = _gasPriceSelection; - return CustomNumericTextFormField( - controller: _gasPriceController, - validationMode: InputValidationMode.aggressive, - validator: (_) { - if (error.isEmpty) return null; - return error; - }, - onChanged: (_) { - _change(); - }, - filteringRegExp: numberRegExp.pattern, - style: _style, - hintText: LocaleKeys.gasPriceGwei.tr(), - hintTextStyle: _hintTextStyle, - ); - }, - ); - }); - } - void _change() { setState(() { _gasLimitSelection = _gasLimitController.selection; _gasPriceSelection = _gasPriceController.selection; }); + final asset = context.read().state.asset; + context.read().add( - WithdrawFormCustomEvmFeeChanged( - gas: double.tryParse(_gasLimitController.text)?.toInt(), - gasPrice: _gasPriceController.text, + WithdrawFormCustomFeeChanged( + FeeInfo.ethGas( + coin: asset.id.id, + gas: double.tryParse(_gasLimitController.text)?.toInt() ?? 0, + gasPrice: + Decimal.tryParse(_gasPriceController.text) ?? Decimal.zero, + ), ), ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart index b53c5b11e1..a539657ab3 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart @@ -1,15 +1,20 @@ +import 'package:decimal/decimal.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.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/constants.dart'; import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; class CustomFeeFieldUtxo extends StatefulWidget { + const CustomFeeFieldUtxo({super.key}); + @override State createState() => _CustomFeeFieldUtxoState(); } @@ -21,18 +26,19 @@ class _CustomFeeFieldUtxoState extends State { @override Widget build(BuildContext context) { final style = TextStyle( - fontSize: 12, - fontWeight: FontWeight.w400, - color: Theme.of(context).textTheme.bodyMedium?.color); + fontSize: 12, + fontWeight: FontWeight.w400, + color: Theme.of(context).textTheme.bodyMedium?.color, + ); - return BlocSelector( + return BlocSelector( selector: (state) { - return state.utxoCustomFeeError; + return state.customFeeError; }, builder: (context, customFeeError) { return BlocSelector( selector: (state) { - return state.customFee.amount; + return state.customFee?.formatTotal(); }, builder: (context, feeAmount) { final amount = feeAmount ?? ''; @@ -44,24 +50,31 @@ class _CustomFeeFieldUtxoState extends State { controller: _feeController, validationMode: InputValidationMode.aggressive, validator: (_) { - if (customFeeError.message.isEmpty) return null; - return customFeeError.message; + if (customFeeError?.message.isEmpty ?? true) return null; + return customFeeError!.message; }, onChanged: (String? value) { setState(() { _previousTextSelection = _feeController.selection; }); + final asset = context.read().state.asset; + final feeInfo = FeeInfo.utxoFixed( + coin: asset.id.id, + amount: Decimal.tryParse(value ?? '0') ?? Decimal.zero, + ); context .read() - .add(WithdrawFormCustomFeeChanged(amount: value ?? '')); + .add(WithdrawFormCustomFeeChanged(feeInfo)); }, filteringRegExp: numberRegExp.pattern, style: style, - hintText: LocaleKeys.customFeeCoin.tr(args: [ - Coin.normalizeAbbr( - context.read().state.coin.abbr, - ) - ]), + hintText: LocaleKeys.customFeeCoin.tr( + args: [ + Coin.normalizeAbbr( + context.read().state.asset.id.id, + ), + ], + ), hintTextStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart index 07c7c35476..db6cef969e 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/fill_form_custom_fee.dart @@ -2,14 +2,16 @@ 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:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/fee_type.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_evm.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/custom_fee/custom_fee_field_utxo.dart'; class FillFormCustomFee extends StatefulWidget { + const FillFormCustomFee({super.key}); + @override State createState() => _FillFormCustomFeeState(); } @@ -29,20 +31,21 @@ class _FillFormCustomFeeState extends State { radius: 18, onTap: () { final bool newOpenState = !_isOpen; - context.read().add(newOpenState - ? const WithdrawFormCustomFeeEnabled() - : const WithdrawFormCustomFeeDisabled()); + context + .read() + .add(WithdrawFormCustomFeeEnabled(_isOpen)); setState(() { _isOpen = newOpenState; }); }, child: Container( - width: double.infinity, - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(18)), - color: Colors.transparent, - ), - child: _isOpen ? _Expanded() : _Collapsed()), + width: double.infinity, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(18)), + color: Colors.transparent, + ), + child: _isOpen ? _Expanded() : _Collapsed(), + ), ); } } @@ -56,9 +59,9 @@ class _Collapsed extends StatelessWidget { width: double.infinity, height: 25, decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: - Border.all(color: theme.custom.specificButtonBorderColor)), + borderRadius: const BorderRadius.all(Radius.circular(18)), + border: Border.all(color: theme.custom.specificButtonBorderColor), + ), child: const Padding( padding: EdgeInsets.only(left: 13, right: 13), child: _Header( @@ -79,9 +82,9 @@ class _Expanded extends StatelessWidget { Container( width: double.infinity, decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: - Border.all(color: theme.custom.specificButtonBorderColor)), + borderRadius: const BorderRadius.all(Radius.circular(18)), + border: Border.all(color: theme.custom.specificButtonBorderColor), + ), child: Padding( padding: const EdgeInsets.only(left: 13, right: 13), child: Column( @@ -167,10 +170,12 @@ class _FeeAmount extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder( - builder: (ctx, state) { - final isUtxo = state.customFee.type == feeType.utxoFixed; + builder: (ctx, state) { + // TODO! Handle both fixed and perkb + final isUtxo = state.customFee is FeeInfoUtxoFixed; - return isUtxo ? CustomFeeFieldUtxo() : CustomFeeFieldEVM(); - }); + return isUtxo ? const CustomFeeFieldUtxo() : const CustomFeeFieldEVM(); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart new file mode 100644 index 0000000000..4e6c976951 --- /dev/null +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart @@ -0,0 +1,632 @@ +// TODO! Separate out into individual files and remove unused fields +// form_fields.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; + +class ToAddressField extends StatelessWidget { + const ToAddressField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-recipient-address-input'), + autocorrect: false, + textInputAction: TextInputAction.next, + enableInteractiveSelection: true, + onChanged: (value) { + context + .read() + .add(WithdrawFormRecipientChanged(value ?? '')); + }, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter recipient address'; + } + return null; + }, + labelText: 'Recipient Address', + hintText: 'Enter recipient address', + suffixIcon: IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () async { + // TODO: Implement QR scanner + }, + ), + ); + }, + ); + } +} + +class AmountField extends StatelessWidget { + const AmountField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + UiTextFormField( + key: const Key('withdraw-amount-input'), + enabled: !state.isMaxAmount, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: currencyInputFormatters, + textInputAction: TextInputAction.next, + onChanged: (value) { + context + .read() + .add(WithdrawFormAmountChanged(value ?? '')); + }, + validator: (value) { + if (state.isMaxAmount) return null; + if (value?.isEmpty ?? true) return 'Please enter an amount'; + + final amount = Decimal.tryParse(value!); + if (amount == null) return 'Please enter a valid number'; + if (amount <= Decimal.zero) { + return 'Amount must be greater than 0'; + } + return null; + }, + labelText: 'Amount', + hintText: 'Enter amount to send', + suffix: Text(state.asset.id.id), + ), + CheckboxListTile( + value: state.isMaxAmount, + onChanged: (value) { + context + .read() + .add(WithdrawFormMaxAmountEnabled(value ?? false)); + }, + title: const Text('Send maximum amount'), + ), + ], + ); + }, + ); + } +} + +/// Fee configuration section +class FeeSection extends StatelessWidget { + const FeeSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Network Fee', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + const CustomFeeToggle(), + if (state.isCustomFee) ...[ + const SizedBox(height: 8), + _buildFeeFields(context, state), + const SizedBox(height: 8), + // Fee summary display + // if (state.customFee != null) ...[ + // const Divider(), + // _buildFeeSummary(context, state.customFee!, state.asset), + // ], + ], + ], + ); + }, + ); + } + + Widget _buildFeeSummary(BuildContext context, FeeInfo fee, Asset asset) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Fee Summary', + style: theme.textTheme.titleSmall, + ), + const SizedBox(height: 4), + if (fee is FeeInfoEthGas) ...[ + Text( + 'Gas: ${fee.gas} units @ ${fee.gasPrice} Gwei', + style: theme.textTheme.bodySmall, + ), + ], + Text( + 'Total Fee: ${fee.totalFee} ${asset.id.id}', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + Widget _buildFeeFields(BuildContext context, WithdrawFormState state) { + final protocol = state.asset.protocol; + + if (protocol is Erc20Protocol) { + return const EvmFeeFields(); + } else if (protocol is UtxoProtocol) { + return const UtxoFeeFields(); + } + + return const SizedBox.shrink(); + } +} + +/// Toggle for enabling custom fee configuration +class CustomFeeToggle extends StatelessWidget { + const CustomFeeToggle({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: const Text('Custom fee'), + value: state.isCustomFee, + onChanged: (value) { + context.read().add( + WithdrawFormCustomFeeEnabled(value), + ); + }, + contentPadding: EdgeInsets.zero, + ); + }, + ); + } +} + +/// EVM-specific fee configuration fields (gas price & limit) +class EvmFeeFields extends StatelessWidget { + const EvmFeeFields({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final evmFee = state.customFee as FeeInfoEthGas?; + + return Column( + children: [ + UiTextFormField( + labelText: 'Gas Price (Gwei)', + keyboardType: TextInputType.number, + initialValue: evmFee?.gasPrice.toString(), + onChanged: (value) { + final gasPrice = Decimal.tryParse(value ?? ''); + if (gasPrice != null) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoEthGas( + coin: state.asset.id.id, + gasPrice: gasPrice, + gas: evmFee?.gas ?? 21000, + ), + ), + ); + } + }, + helperText: 'Higher gas price = faster confirmation', + ), + const SizedBox(height: 8), + UiTextFormField( + labelText: 'Gas Limit', + keyboardType: TextInputType.number, + initialValue: evmFee?.gas.toString() ?? '21000', + onChanged: (value) { + final gas = int.tryParse(value ?? ''); + if (gas != null) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoEthGas( + coin: state.asset.id.id, + gasPrice: evmFee?.gasPrice ?? Decimal.one, + gas: gas, + ), + ), + ); + } + }, + helperText: 'Estimated: 21000', + ), + ], + ); + }, + ); + } +} + +/// UTXO-specific fee configuration with predefined tiers +class UtxoFeeFields extends StatelessWidget { + const UtxoFeeFields({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final protocol = state.asset.protocol as UtxoProtocol; + final defaultFee = protocol.txFee ?? 10000; + final currentFee = state.customFee as FeeInfoUtxoFixed?; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SegmentedButton( + segments: [ + ButtonSegment( + value: defaultFee, + label: Text('Standard ($defaultFee)'), + ), + ButtonSegment( + value: defaultFee * 2, + label: Text('Fast (${defaultFee * 2})'), + ), + ButtonSegment( + value: defaultFee * 5, + label: Text('Urgent (${defaultFee * 5})'), + ), + ], + selected: { + currentFee?.amount.toBigInt().toInt() ?? defaultFee, + }, + onSelectionChanged: (values) { + if (values.isNotEmpty) { + context.read().add( + WithdrawFormCustomFeeChanged( + FeeInfoUtxoFixed( + coin: state.asset.id.id, + amount: Decimal.fromInt(values.first), + ), + ), + ); + } + }, + ), + const SizedBox(height: 8), + Text( + 'Higher fee = faster confirmation', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + }, + ); + } +} + +/// Field for entering transaction memo +class MemoField extends StatelessWidget { + const MemoField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-memo-input'), + labelText: 'Memo (Optional)', + maxLines: 2, + onChanged: (value) { + context.read().add( + WithdrawFormMemoChanged(value ?? ''), + ); + }, + helperText: 'Required for some exchanges', + ); + }, + ); + } +} + +/// Preview button to initiate withdrawal confirmation +class PreviewButton extends StatelessWidget { + const PreviewButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SizedBox( + // Wrap with SizedBox + width: double.infinity, // Take full width + height: 48.0, // Fixed height + child: FilledButton.icon( + onPressed: state.isSending + ? null + : () => context.read().add( + const WithdrawFormPreviewSubmitted(), + ), + icon: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.send), + label: Text( + state.isSending ? 'Loading...' : 'Preview Withdrawal', + ), + ), + ); + }, + ); + } +} + +/// Page for confirming withdrawal details +class ConfirmationPage extends StatelessWidget { + const ConfirmationPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.preview == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + children: [ + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _ConfirmationItem( + label: 'From', + value: state.selectedSourceAddress?.address ?? + 'Default Wallet', + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'To', + value: state.recipientAddress, + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Amount', + value: + '${state.preview!.balanceChanges.netChange.abs()} ${state.asset.id.id}', + ), + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Network Fee', + value: state.preview!.fee.formatTotal(), + ), + if (state.memo != null) ...[ + const SizedBox(height: 12), + _ConfirmationItem( + label: 'Memo', + value: state.memo!, + ), + ], + ], + ), + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context.read().add( + const WithdrawFormCancelled(), + ), + child: const Text('Back'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + //TODO! onPressed: state.submissionInProgress + onPressed: state.isSending + ? null + : () => context.read().add( + const WithdrawFormSubmitted(), + ), + //TODO! child: state.submissionInProgress + child: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Confirm'), + ), + ), + ], + ), + ], + ); + }, + ); + } +} + +/// Helper widget for displaying confirmation details +class _ConfirmationItem extends StatelessWidget { + final String label; + final String value; + + const _ConfirmationItem({ + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], + ); + } +} + +/// Page showing successful withdrawal +class SuccessPage extends StatelessWidget { + const SuccessPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + 'Withdrawal Successful', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Transaction Hash:', + style: Theme.of(context).textTheme.bodySmall, + ), + SelectableText( + state.result!.txHash, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Done'), + ), + ], + ); + }, + ); + } +} + +/// Page showing withdrawal failure +class FailurePage extends StatelessWidget { + const FailurePage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.error_outline, + size: 64, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(height: 24), + Text( + 'Withdrawal Failed', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + const SizedBox(height: 16), + if (state.transactionError != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + state.transactionError!.error, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + const SizedBox(height: 24), + OutlinedButton( + onPressed: () => context.read().add( + const WithdrawFormCancelled(), + ), + child: const Text('Try Again'), + ), + ], + ); + }, + ); + } +} + +class IbcTransferField extends StatelessWidget { + const IbcTransferField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return SwitchListTile( + title: const Text('IBC Transfer'), + subtitle: const Text('Send to another Cosmos chain'), + value: state.isIbcTransfer, + onChanged: (value) { + context + .read() + .add(WithdrawFormIbcTransferEnabled(value)); + }, + ); + }, + ); + } +} + +class IbcChannelField extends StatelessWidget { + const IbcChannelField({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return UiTextFormField( + key: const Key('withdraw-ibc-channel-input'), + labelText: 'IBC Channel', + hintText: 'Enter IBC channel ID', + onChanged: (value) { + context + .read() + .add(WithdrawFormIbcChannelChanged(value ?? '')); + }, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Please enter IBC channel'; + } + return null; + }, + ); + }, + ); + } +} diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart index f1f6d8f5ab..a903af25c7 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_amount.dart @@ -9,6 +9,8 @@ import 'package:web_dex/shared/ui/custom_numeric_text_form_field.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/sell_max_button.dart'; class FillFormAmount extends StatefulWidget { + const FillFormAmount({super.key}); + @override State createState() => _FillFormAmountState(); } @@ -48,14 +50,10 @@ class _FillFormAmountState extends State { }); context .read() - .add(WithdrawFormAmountChanged(amount: amount ?? '')); + .add(WithdrawFormAmountChanged(amount ?? '')); }, validationMode: InputValidationMode.aggressive, - validator: (_) { - final String amountError = state.amountError.message; - if (amountError.isEmpty) return null; - return amountError; - }, + validator: (_) => state.amountError?.message, ); }, ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart index c21e4e6a95..778841d971 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart @@ -1,26 +1,29 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; -class FillFormMemo extends StatelessWidget { - const FillFormMemo({Key? key}) : super(key: key); +class WithdrawMemoField extends StatelessWidget { + final String? memo; + final ValueChanged? onChanged; + + const WithdrawMemoField({ + required this.memo, + required this.onChanged, + super.key, + }); @override Widget build(BuildContext context) { return UiTextFormField( key: const Key('withdraw-form-memo-field'), + initialValue: memo, + maxLines: 2, + onChanged: onChanged == null ? null : (v) => onChanged!(v ?? ''), autocorrect: false, textInputAction: TextInputAction.next, enableInteractiveSelection: true, - onChanged: (String? memo) { - context - .read() - .add(WithdrawFormMemoUpdated(text: memo)); - }, inputFormatters: [LengthLimitingTextInputFormatter(256)], maxLength: 256, counterText: '', diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart index bbd3bcec6f..8388d1b889 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_recipient_address.dart @@ -6,14 +6,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/views/qr_scanner.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/buttons/convert_address_button.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class FillFormRecipientAddress extends StatefulWidget { + const FillFormRecipientAddress({super.key}); + @override State createState() => _FillFormRecipientAddressState(); @@ -26,15 +28,14 @@ class _FillFormRecipientAddressState extends State { @override Widget build(BuildContext context) { - return BlocSelector( + return BlocSelector( selector: (state) { - return state.addressError; + //TODO! return state.addressError; + return state.amountError; }, builder: (context, addressError) { return BlocSelector( - selector: (state) { - return state.address; - }, + selector: (state) => state.recipientAddress, builder: (context, address) { _addressController ..text = address @@ -51,40 +52,45 @@ class _FillFormRecipientAddressState extends State { setState(() { _previousTextSelection = _addressController.selection; }); - context.read().add( - WithdrawFormAddressChanged(address: address ?? '')); + context + .read() + .add(WithdrawFormRecipientChanged(address ?? '')); }, validator: (String? value) { - if (addressError.message.isEmpty) return null; + if (addressError?.message.isEmpty ?? true) return null; if (addressError is MixedCaseAddressError) { return null; } - return addressError.message; + return addressError!.message; }, validationMode: InputValidationMode.aggressive, inputFormatters: [LengthLimitingTextInputFormatter(256)], hintText: LocaleKeys.recipientAddress.tr(), hintTextStyle: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w500), - suffixIcon: - (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) - ? IconButton( - icon: const Icon(Icons.qr_code_scanner), - onPressed: () async { - final address = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const QrScanner()), - ); + fontSize: 14, + fontWeight: FontWeight.w500, + ), + suffixIcon: (!kIsWeb && + (Platform.isAndroid || Platform.isIOS)) + ? IconButton( + icon: const Icon(Icons.qr_code_scanner), + onPressed: () async { + final address = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const QrCodeReaderOverlay(), + ), + ); - if (context.mounted) { - context.read().add( - WithdrawFormAddressChanged( - address: address ?? '')); - } - }, - ) - : null, + if (context.mounted) { + context.read().add( + WithdrawFormRecipientChanged(address ?? ''), + ); + } + }, + ) + : null, ), if (addressError is MixedCaseAddressError) _ErrorAddressRow( @@ -121,7 +127,7 @@ class _ErrorAddressRow extends StatelessWidget { const Padding( padding: EdgeInsets.only(left: 6.0), child: ConvertAddressButton(), - ) + ), ], ), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart index fd980bdea6..5dbb5b5c92 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_trezor_sender_address.dart @@ -8,6 +8,7 @@ import 'package:web_dex/views/wallet/coin_details/constants.dart'; class FillFormTrezorSenderAddress extends StatelessWidget { const FillFormTrezorSenderAddress({ + super.key, required this.coin, required this.addresses, required this.selectedAddress, @@ -26,7 +27,7 @@ class FillFormTrezorSenderAddress extends StatelessWidget { onChanged: (String address) { context .read() - .add(WithdrawFormSenderAddressChanged(address: address)); + .add(WithdrawFormRecipientChanged(address)); }, maxWidth: withdrawWidth, maxHeight: 300, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart index 66a932cc24..5c805de4e5 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_error.dart @@ -8,41 +8,43 @@ import 'package:web_dex/shared/widgets/copied_text.dart'; import 'package:web_dex/shared/widgets/details_dropdown.dart'; class FillFormError extends StatelessWidget { - const FillFormError(); + const FillFormError({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (ctx, state) { - if (!state.hasSendError) { - return const SizedBox(); - } - final BaseError sendError = state.sendError; - return Column( - children: [ - SizedBox( - width: double.infinity, - child: SelectableText( - sendError.message, - textAlign: TextAlign.left, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + builder: (ctx, state) { + if (!state.hasTransactionError) { + return const SizedBox(); + } + final BaseError sendError = state.transactionError!; + return Column( + children: [ + SizedBox( + width: double.infinity, + child: SelectableText( + sendError.message, + textAlign: TextAlign.left, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), ), - ), - if (sendError is ErrorWithDetails) - Padding( - padding: const EdgeInsets.only(top: 10.0), - child: DetailsDropdown( - summary: LocaleKeys.showMore.tr(), - content: SingleChildScrollView( - controller: ScrollController(), - child: CopiedText( - copiedValue: (sendError as ErrorWithDetails).details), + if (sendError is ErrorWithDetails) + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: DetailsDropdown( + summary: LocaleKeys.showMore.tr(), + content: SingleChildScrollView( + controller: ScrollController(), + child: CopiedText( + copiedValue: (sendError as ErrorWithDetails).details, + ), + ), ), ), - ) - ], - ); - }); + ], + ); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart index 367aeee033..ee2d590778 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fill_form_footer.dart @@ -9,6 +9,8 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_for import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class FillFormFooter extends StatelessWidget { + const FillFormFooter({super.key}); + @override Widget build(BuildContext context) { return BlocBuilder( @@ -16,7 +18,10 @@ class FillFormFooter extends StatelessWidget { return ConstrainedBox( constraints: const BoxConstraints(maxWidth: withdrawWidth), child: state.isSending - ? FillFormPreloader(state.trezorProgressStatus) + ? + //TODO(@takenagain): Trezor SDK support + // FillFormPreloader(state.trezorProgressStatus) + const FillFormPreloader('Sending') : UiBorderButton( key: const Key('send-enter-button'), backgroundColor: Theme.of(context).colorScheme.surface, diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart index 563de41da5..4a75d966a9 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form.dart @@ -2,6 +2,7 @@ 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:komodo_ui/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -13,7 +14,7 @@ import 'package:web_dex/views/wallet/coin_details/constants.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart'; class SendCompleteForm extends StatelessWidget { - const SendCompleteForm(); + const SendCompleteForm({super.key}); @override Widget build(BuildContext context) { @@ -24,6 +25,10 @@ class SendCompleteForm extends StatelessWidget { return BlocBuilder( builder: (context, WithdrawFormState state) { + final feeValue = state.result?.fee; + + if (state.result == null) return const SizedBox.shrink(); + return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -36,7 +41,7 @@ class SendCompleteForm extends StatelessWidget { children: [ SendConfirmItem( title: LocaleKeys.recipientAddress.tr(), - value: state.withdrawDetails.toAddress, + value: state.result!.toAddress, centerAlign: true, ), const SizedBox(height: 7), @@ -45,34 +50,37 @@ class SendCompleteForm extends StatelessWidget { children: [ const SizedBox(height: 10), SelectableText( - '-${state.amount} ${Coin.normalizeAbbr(state.coin.abbr)}', + '-${state.amount} ${Coin.normalizeAbbr(state.asset.id.id)}', style: TextStyle( - fontSize: 25, - fontWeight: FontWeight.w700, - color: theme.custom.headerFloatBoxColor), + fontSize: 25, + fontWeight: FontWeight.w700, + color: theme.custom.headerFloatBoxColor, + ), ), const SizedBox(height: 5), SelectableText( '\$${state.usdAmountPrice ?? 0}', style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: theme.custom.headerFloatBoxColor), + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.custom.headerFloatBoxColor, + ), ), ], ), - if (state.hasSendError) - _SendCompleteError(error: state.sendError), + if (state.hasTransactionError) + _SendCompleteError(error: state.transactionError!), ], ), ), - _TransactionHash( - feeValue: state.withdrawDetails.feeValue, - feeCoin: state.withdrawDetails.feeCoin, - txHash: state.withdrawDetails.txHash, - usdFeePrice: state.usdFeePrice, - isFeePriceExpensive: state.isFeePriceExpensive, - ), + if (state.result?.txHash != null) + _TransactionHash( + feeValue: feeValue!.formatTotal(), + feeCoin: feeValue.coin, + txHash: state.result!.txHash, + usdFeePrice: state.usdFeePrice, + isFeePriceExpensive: state.isFeePriceExpensive, + ), ], ); }, @@ -149,18 +157,20 @@ class _BuildMemo extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( - selector: (state) { - return state.memo; - }, builder: (context, memo) { - if (memo == null || memo.isEmpty) return const SizedBox.shrink(); + selector: (state) { + return state.memo; + }, + builder: (context, memo) { + if (memo == null || memo.isEmpty) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(bottom: 21), - child: SendConfirmItem( - title: '${LocaleKeys.memo.tr()}:', - value: memo, - ), - ); - }); + return Padding( + padding: const EdgeInsets.only(bottom: 21), + child: SendConfirmItem( + title: '${LocaleKeys.memo.tr()}:', + value: memo, + ), + ); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart index 40cfa81f11..420817c359 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_complete_form/send_complete_form_buttons.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; @@ -30,32 +31,36 @@ class _MobileButtons extends StatelessWidget { final WithdrawFormBloc withdrawFormBloc = context.read(); final WithdrawFormState state = withdrawFormBloc.state; - return Row(children: [ - Expanded( - child: AppDefaultButton( - key: const Key('send-complete-view-on-explorer'), - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: () => viewHashOnExplorer( - state.coin, - state.withdrawDetails.txHash, - HashExplorerType.tx, + final txHash = state.result?.txHash; + + final explorerUrl = + txHash == null ? null : state.asset.protocol.explorerTxUrl(txHash); + + return Row( + children: [ + if (explorerUrl != null) + Expanded( + child: AppDefaultButton( + key: const Key('send-complete-view-on-explorer'), + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: () => launchUrl(explorerUrl), + text: LocaleKeys.viewOnExplorer.tr(), + ), ), - text: LocaleKeys.viewOnExplorer.tr(), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: UiPrimaryButton( - key: const Key('send-complete-done'), - height: height, - onPressed: () => withdrawFormBloc.add(const WithdrawFormReset()), - text: LocaleKeys.done.tr(), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: UiPrimaryButton( + key: const Key('send-complete-done'), + height: height, + onPressed: () => withdrawFormBloc.add(const WithdrawFormReset()), + text: LocaleKeys.done.tr(), + ), ), ), - ), - ]); + ], + ); } } @@ -73,15 +78,16 @@ class _DesktopButtons extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - AppDefaultButton( - key: const Key('send-complete-view-on-explorer'), - width: width, - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: () => viewHashOnExplorer( - state.coin, state.withdrawDetails.txHash, HashExplorerType.tx), - text: LocaleKeys.viewOnExplorer.tr(), - ), + if (state.result?.txHash != null) + AppDefaultButton( + key: const Key('send-complete-view-on-explorer'), + width: width, + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: () => + openUrl(state.asset.txExplorerUrl(state.result?.txHash)!), + text: LocaleKeys.viewOnExplorer.tr(), + ), Padding( padding: const EdgeInsets.only(left: space), child: UiPrimaryButton( @@ -91,7 +97,7 @@ class _DesktopButtons extends StatelessWidget { onPressed: () => _sendCompleteDone(context), text: LocaleKeys.done.tr(), ), - ) + ), ], ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart index e219e1e065..27395b4d38 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_buttons.dart @@ -9,8 +9,11 @@ import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/views/wallet/coin_details/constants.dart'; class SendConfirmButtons extends StatelessWidget { - const SendConfirmButtons( - {required this.hasSendError, required this.onBackTap}); + const SendConfirmButtons({ + super.key, + required this.hasSendError, + required this.onBackTap, + }); final bool hasSendError; final VoidCallback onBackTap; @override @@ -33,31 +36,33 @@ class _MobileButtons extends StatelessWidget { Widget build(BuildContext context) { const height = 52.0; - return Row(children: [ - Expanded( - child: AppDefaultButton( - key: const Key('confirm-back-button'), - height: height + 6, - padding: const EdgeInsets.symmetric(vertical: 0), - onPressed: onBackTap, - text: LocaleKeys.back.tr(), - ), - ), - if (!hasError) + return Row( + children: [ Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: UiPrimaryButton( - key: const Key('confirm-agree-button'), - height: height, - onPressed: () => context - .read() - .add(const WithdrawFormSendRawTx()), - text: LocaleKeys.confirm.tr(), - ), + child: AppDefaultButton( + key: const Key('confirm-back-button'), + height: height + 6, + padding: const EdgeInsets.symmetric(vertical: 0), + onPressed: onBackTap, + text: LocaleKeys.back.tr(), ), ), - ]); + if (!hasError) + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: UiPrimaryButton( + key: const Key('confirm-agree-button'), + height: height, + onPressed: () => context + .read() + .add(const WithdrawFormSubmitted()), + text: LocaleKeys.confirm.tr(), + ), + ), + ), + ], + ); } } @@ -92,10 +97,10 @@ class _DesktopButtons extends StatelessWidget { height: height, onPressed: () => context .read() - .add(const WithdrawFormSendRawTx()), + .add(const WithdrawFormSubmitted()), text: LocaleKeys.confirm.tr(), ), - ) + ), ], ); } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart index 4a459d4541..2203cf6368 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_footer.dart @@ -7,7 +7,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_con import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class SendConfirmFooter extends StatelessWidget { - const SendConfirmFooter(); + const SendConfirmFooter({super.key}); @override Widget build(BuildContext context) { @@ -21,10 +21,9 @@ class SendConfirmFooter extends StatelessWidget { child: Center(child: UiSpinner()), ) : SendConfirmButtons( - hasSendError: state.hasSendError, + hasSendError: state.hasTransactionError, onBackTap: () => context.read().add( - const WithdrawFormStepReverted( - step: WithdrawFormStep.confirm), + const WithdrawFormStepReverted(), ), ), ); diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart index ae6b8304b2..f4a70a082c 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form.dart @@ -2,6 +2,7 @@ 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:komodo_ui/utils.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; @@ -13,7 +14,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_con import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_item.dart'; class SendConfirmForm extends StatelessWidget { - const SendConfirmForm(); + const SendConfirmForm({super.key}); @override Widget build(BuildContext context) { @@ -25,9 +26,15 @@ class SendConfirmForm extends StatelessWidget { return BlocBuilder( builder: (context, WithdrawFormState state) { final amountString = - '${truncateDecimal(state.amountToSendString, decimalRange)} ${Coin.normalizeAbbr(state.withdrawDetails.coin)}'; - final feeString = - '${truncateDecimal(state.withdrawDetails.feeValue, decimalRange)} ${Coin.normalizeAbbr(state.withdrawDetails.feeCoin)}'; + '${truncateDecimal(state.amount, decimalRange)} ${Coin.normalizeAbbr(state.result!.coin)}'; + + // Use the new formatting extension for fees + final feeString = state.preview?.fee.formatTotal( + precision: decimalRange, + ); + + // Use the isExpensive helper for warnings + final isFeePriceExpensive = state.preview?.fee.isHighFee ?? false; return Container( width: isMobile ? double.infinity : withdrawWidth, @@ -38,7 +45,7 @@ class SendConfirmForm extends StatelessWidget { children: [ SendConfirmItem( title: '${LocaleKeys.recipientAddress.tr()}:', - value: state.withdrawDetails.toAddress, + value: state.result!.toAddress, centerAlign: false, ), const SizedBox(height: 26), @@ -50,9 +57,9 @@ class SendConfirmForm extends StatelessWidget { const SizedBox(height: 26), SendConfirmItem( title: '${LocaleKeys.fee.tr()}:', - value: feeString, + value: feeString ?? '', usdPrice: state.usdFeePrice ?? 0, - isWarningShown: state.isFeePriceExpensive, + isWarningShown: isFeePriceExpensive, ), if (state.memo != null) Padding( diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart index dfd57bf00a..1105d2a541 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/send_confirm_form/send_confirm_form_error.dart @@ -1,28 +1,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; class SendConfirmFormError extends StatelessWidget { - const SendConfirmFormError(); + const SendConfirmFormError({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (BuildContext context, WithdrawFormState state) { - final BaseError sendError = state.sendError; + builder: (BuildContext context, WithdrawFormState state) { + final sendError = state.transactionError; - return Container( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - width: double.infinity, - child: Text( - sendError.message, - textAlign: TextAlign.left, - style: TextStyle( - color: Theme.of(context).colorScheme.error, + return Container( + padding: const EdgeInsets.fromLTRB(0, 10, 0, 10), + width: double.infinity, + child: Text( + sendError?.message ?? 'Unknown error', + textAlign: TextAlign.left, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), ), - ), - ); - }); + ); + }, + ); } } diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart index 4e15593f92..245d8d603b 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart @@ -1,34 +1,38 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/segwit_icon.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; class WithdrawFormHeader extends StatelessWidget { const WithdrawFormHeader({ - this.isIndicatorShown = true, - required this.coin, + required this.asset, + this.onBackButtonPressed, + super.key, }); - final bool isIndicatorShown; - final Coin coin; + + final Asset asset; + final VoidCallback? onBackButtonPressed; + + bool get _isSegwit => asset.id.id.toLowerCase().contains('segwit'); @override Widget build(BuildContext context) { return BlocBuilder( - builder: (context, WithdrawFormState state) { + builder: (context, state) { return PageHeader( title: state.step.title, - widgetTitle: coin.mode == CoinMode.segwit + widgetTitle: _isSegwit ? const Padding( padding: EdgeInsets.only(left: 6.0), child: SegwitIcon(height: 22), ) : null, backText: LocaleKeys.backToWallet.tr(), - onBackButtonPressed: context.read().goBack, + onBackButtonPressed: onBackButtonPressed, ); }, ); diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index a7639f5d64..eb2130353e 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -1,81 +1,652 @@ -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/bitrefill/bloc/bitrefill_bloc.dart'; -import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/views/bitrefill/bitrefill_transaction_completed_dialog.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/model/text_error.dart'; +import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; +import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; + +class WithdrawForm extends StatefulWidget { + final Asset asset; + final VoidCallback onSuccess; + final VoidCallback? onBackButtonPressed; // Add this -class WithdrawForm extends StatelessWidget { const WithdrawForm({ - super.key, - required this.coin, - required this.onBackButtonPressed, + required this.asset, required this.onSuccess, + this.onBackButtonPressed, + super.key, }); - final Coin coin; - final VoidCallback onBackButtonPressed; - final VoidCallback onSuccess; + + @override + State createState() => _WithdrawFormState(); +} + +class _WithdrawFormState extends State { + late final WithdrawFormBloc _formBloc; + late final _sdk = context.read(); + + @override + void initState() { + super.initState(); + _formBloc = WithdrawFormBloc( + asset: widget.asset, + sdk: _sdk, + ); + } + + @override + void dispose() { + _formBloc.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => WithdrawFormBloc( - coin: coin, - coinsRepository: RepositoryProvider.of(context), - api: RepositoryProvider.of(context), - goBack: onBackButtonPressed, + return BlocProvider.value( + value: _formBloc, + child: BlocListener( + listenWhen: (prev, curr) => + prev.step != curr.step && curr.step == WithdrawFormStep.success, + listener: (_, __) => widget.onSuccess(), + child: WithdrawFormContent( + onBackButtonPressed: widget.onBackButtonPressed, + ), ), - child: isBitrefillIntegrationEnabled - ? BlocConsumer( - listener: (BuildContext context, BitrefillState state) { - if (state is BitrefillPaymentSuccess) { - onSuccess(); - _showBitrefillPaymentSuccessDialog(context, state); - } - }, - builder: (BuildContext context, BitrefillState state) { - final BitrefillPaymentInProgress? paymentState = - state is BitrefillPaymentInProgress ? state : null; - - final String? paymentAddress = - paymentState?.paymentIntent.paymentAddress; - final String? paymentAmount = - paymentState?.paymentIntent.paymentAmount.toString(); - - return WithdrawFormIndex( - coin: coin, - address: paymentAddress, - amount: paymentAmount, - ); - }, + ); + } +} + +class WithdrawFormContent extends StatelessWidget { + final VoidCallback? onBackButtonPressed; + + const WithdrawFormContent({ + this.onBackButtonPressed, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (prev, curr) => prev.step != curr.step, + builder: (context, state) { + return Column( + children: [ + WithdrawFormHeader( + asset: state.asset, + onBackButtonPressed: onBackButtonPressed, + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(18), + ), + child: _buildStep(state.step), + ), + ), + ), + ], + ); + }, + ); + } + + Widget _buildStep(WithdrawFormStep step) { + switch (step) { + case WithdrawFormStep.fill: + return const WithdrawFormFillSection(); + case WithdrawFormStep.confirm: + return const WithdrawFormConfirmSection(); + case WithdrawFormStep.success: + return const WithdrawFormSuccessSection(); + case WithdrawFormStep.failed: + return const WithdrawFormFailedSection(); + } + } +} + +class NetworkErrorDisplay extends StatelessWidget { + final TextError error; + final VoidCallback? onRetry; + + const NetworkErrorDisplay({ + required this.error, + this.onRetry, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ErrorDisplay( + message: error.message, + icon: Icons.cloud_off, + child: onRetry != null + ? TextButton( + onPressed: onRetry, + child: const Text('Retry'), ) - : WithdrawFormIndex( - coin: coin, + : null, + ); + } +} + +class TransactionErrorDisplay extends StatelessWidget { + final TextError error; + final VoidCallback? onDismiss; + + const TransactionErrorDisplay({ + required this.error, + this.onDismiss, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ErrorDisplay( + message: error.message, + icon: Icons.warning_amber_rounded, + child: onDismiss != null + ? IconButton( + icon: const Icon(Icons.close), + onPressed: onDismiss, + ) + : null, + ); + } +} + +class PreviewWithdrawButton extends StatelessWidget { + final VoidCallback? onPressed; + final bool isSending; + + const PreviewWithdrawButton({ + required this.onPressed, + required this.isSending, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 48, + child: FilledButton( + onPressed: onPressed, + child: isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Preview Withdrawal'), + ), + ); + } +} + +class WithdrawPreviewDetails extends StatelessWidget { + final WithdrawalPreview preview; + + const WithdrawPreviewDetails({ + required this.preview, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildRow('Amount', preview.balanceChanges.netChange.toString()), + const SizedBox(height: 8), + _buildRow('Fee', preview.fee.formatTotal()), + // Add more preview details as needed + ], + ), + ), + ); + } + + Widget _buildRow(String label, String value) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label), + Text(value), + ], + ); + } +} + +class WithdrawResultDetails extends StatelessWidget { + final WithdrawalResult result; + + const WithdrawResultDetails({ + required this.result, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + 'Transaction Hash:', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 4), + SelectableText(result.txHash), + // Add more result details as needed + ], + ), + ), + ); + } +} + +class WithdrawFormFillSection extends StatelessWidget { + const WithdrawFormFillSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (state.asset.supportsMultipleAddresses) ...[ + SourceAddressField( + asset: state.asset, + pubkeys: state.pubkeys, + selectedAddress: state.selectedSourceAddress, + onChanged: (address) => context + .read() + .add(WithdrawFormSourceChanged(address)), + ), + const SizedBox(height: 16), + ], + RecipientAddressField( + address: state.recipientAddress, + onChanged: (value) => context + .read() + .add(WithdrawFormRecipientChanged(value)), + onQrScanned: (value) => context + .read() + .add(WithdrawFormRecipientChanged(value)), + addressError: state.recipientAddressError?.message, + ), + const SizedBox(height: 16), + WithdrawAmountField( + asset: state.asset, + amount: state.amount, + isMaxAmount: state.isMaxAmount, + onChanged: (value) => context + .read() + .add(WithdrawFormAmountChanged(value)), + onMaxToggled: (value) => context + .read() + .add(WithdrawFormMaxAmountEnabled(value)), + amountError: state.amountError?.message, + ), + if (state.isCustomFeeSupported) ...[ + const SizedBox(height: 16), + Row( + children: [ + Checkbox( + value: state.isCustomFee, + onChanged: (enabled) => context + .read() + .add(WithdrawFormCustomFeeEnabled(enabled ?? false)), + ), + const Text('Custom network fee'), + ], + ), + if (state.isCustomFee && state.customFee != null) ...[ + const SizedBox(height: 8), + + FeeInfoInput( + asset: state.asset, + selectedFee: state.customFee!, + isCustomFee: true, // indicates user can edit it + onFeeSelected: (newFee) { + context + .read() + .add(WithdrawFormCustomFeeChanged(newFee!)); + }, + ), + + // If the bloc has an error for custom fees: + if (state.customFeeError != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + state.customFeeError!.message, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ], + ], + const SizedBox(height: 16), + WithdrawMemoField( + memo: state.memo, + onChanged: (value) => context + .read() + .add(WithdrawFormMemoChanged(value)), + ), + const SizedBox(height: 24), + // TODO! Refactor to use Formz and replace with the appropriate + // error state value. + if (state.hasPreviewError) + ErrorDisplay(message: state.previewError!.message), + const SizedBox(height: 16), + PreviewWithdrawButton( + onPressed: state.isSending || state.hasValidationErrors + ? null + : () { + context + .read() + .add(const WithdrawFormPreviewSubmitted()); + }, + isSending: state.isSending, + ), + ], + ); + }, + ); + } +} + +class WithdrawFormConfirmSection extends StatelessWidget { + const WithdrawFormConfirmSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.preview == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + WithdrawPreviewDetails(preview: state.preview!), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => context + .read() + .add(const WithdrawFormCancelled()), + child: const Text('Back'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: FilledButton( + onPressed: state.isSending + ? null + : () { + context + .read() + .add(const WithdrawFormSubmitted()); + }, + child: state.isSending + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Confirm'), + ), + ), + ], ), + ], + ); + }, + ); + } +} + +class WithdrawFormSuccessSection extends StatelessWidget { + const WithdrawFormSuccessSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.check_circle_outline, + size: 64, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + 'Transaction Successful', + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + WithdrawResultDetails(result: state.result!), + const SizedBox(height: 24), + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Done'), + ), + ], + ); + }, + ); + } +} + +class WithdrawResultCard extends StatelessWidget { + final WithdrawalResult result; + final Asset asset; + + const WithdrawResultCard({ + required this.result, + required this.asset, + super.key, + }); + + @override + Widget build(BuildContext context) { + final maybeTxEplorer = asset.protocol.explorerTxUrl(result.txHash); + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHashSection(context), + const Divider(height: 32), + _buildNetworkSection(context), + if (maybeTxEplorer != null) ...[ + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: () => openUrl(maybeTxEplorer), + icon: const Icon(Icons.open_in_new), + label: const Text('View on Explorer'), + ), + ], + ], + ), + ), ); } - void _showBitrefillPaymentSuccessDialog( - BuildContext context, - BitrefillPaymentSuccess state, - ) { - showDialog( - context: context, - builder: (BuildContext context) { - return BitrefillTransactionCompletedDialog( - title: LocaleKeys.bitrefillPaymentSuccessfull.tr(), - message: LocaleKeys.bitrefillPaymentSuccessfullInstruction.tr( - args: [state.invoiceId], + Widget _buildHashSection(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Transaction Hash', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + SelectableText( + result.txHash, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'Mono', ), - onViewInvoicePressed: () {}, + ), + ], + ); + } + + Widget _buildNetworkSection(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Network', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + Row( + children: [ + AssetIcon(asset.id), + const SizedBox(width: 8), + Text( + asset.id.name, + style: theme.textTheme.bodyLarge, + ), + ], + ), + ], + ); + } +} + +class WithdrawFormFailedSection extends StatelessWidget { + const WithdrawFormFailedSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + Icon( + Icons.error_outline, + size: 64, + color: theme.colorScheme.error, + ), + const SizedBox(height: 24), + Text( + 'Transaction Failed', + style: theme.textTheme.headlineMedium?.copyWith( + color: theme.colorScheme.error, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + if (state.transactionError != null) + WithdrawErrorCard( + error: state.transactionError!, + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlinedButton( + onPressed: () => context + .read() + .add(const WithdrawFormStepReverted()), + child: const Text('Back'), + ), + const SizedBox(width: 16), + FilledButton( + onPressed: () => context + .read() + .add(const WithdrawFormReset()), + child: const Text('Try Again'), + ), + ], + ), + ], ); }, ); } } + +class WithdrawErrorCard extends StatelessWidget { + final BaseError error; + + const WithdrawErrorCard({ + required this.error, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error Details', + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + SelectableText( + error.message, + style: theme.textTheme.bodyMedium, + ), + if (error is TextError) ...[ + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + ExpansionTile( + title: const Text('Technical Details'), + children: [ + SelectableText( + (error as TextError).error, + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'Mono', + ), + ), + ], + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart deleted file mode 100644 index 296f5d3b54..0000000000 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form_index.dart +++ /dev/null @@ -1,92 +0,0 @@ -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/withdraw_form/withdraw_form_bloc.dart'; -import 'package:web_dex/common/screen.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/views/common/pages/page_layout.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/complete_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/confirm_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/failed_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/pages/fill_form_page.dart'; -import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; - -class WithdrawFormIndex extends StatefulWidget { - const WithdrawFormIndex({ - required this.coin, - this.address, - this.amount, - }); - - final Coin coin; - final String? address; - final String? amount; - - @override - State createState() => _WithdrawFormIndexState(); -} - -class _WithdrawFormIndexState extends State { - @override - void initState() { - super.initState(); - - if (widget.address != null) { - context.read().add( - WithdrawFormAddressChanged( - address: widget.address!, - ), - ); - } - - if (widget.amount != null) { - context.read().add( - WithdrawFormAmountChanged( - amount: widget.amount!, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - final scrollController = ScrollController(); - return BlocSelector( - selector: (state) => state.step, - builder: (context, step) => PageLayout( - header: WithdrawFormHeader(coin: widget.coin), - content: Flexible( - child: DexScrollbar( - isMobile: isMobile, - scrollController: scrollController, - child: SingleChildScrollView( - controller: scrollController, - child: Container( - padding: - const EdgeInsets.symmetric(vertical: 20, horizontal: 15), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(18.0), - ), - child: Builder( - builder: (context) { - switch (step) { - case WithdrawFormStep.fill: - return const FillFormPage(); - case WithdrawFormStep.confirm: - return const ConfirmPage(); - case WithdrawFormStep.success: - return const CompletePage(); - case WithdrawFormStep.failed: - return const FailedPage(); - } - }, - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/views/wallet/coins_manager/coins_manager_controls.dart b/lib/views/wallet/coins_manager/coins_manager_controls.dart index 11f324a4b6..257c01e2be 100644 --- a/lib/views/wallet/coins_manager/coins_manager_controls.dart +++ b/lib/views/wallet/coins_manager/coins_manager_controls.dart @@ -73,9 +73,9 @@ class CoinsManagerFilters extends StatelessWidget { fontSize: 12, fontWeight: FontWeight.w500, ), - onChanged: (String text) => context + onChanged: (String? text) => context .read() - .add(CoinsManagerSearchUpdate(text: text)), + .add(CoinsManagerSearchUpdate(text: text ?? '')), ); } } diff --git a/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart b/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart index 27d9153859..1f3a103e8b 100644 --- a/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart +++ b/lib/views/wallet/wallet_page/charts/coin_prices_chart.dart @@ -1,7 +1,10 @@ import 'package:dragon_charts_flutter/dragon_charts_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart' hide TextDirection; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/bloc/cex_market_data/price_chart/models/price_chart_data.dart'; import 'package:web_dex/bloc/cex_market_data/price_chart/models/time_period.dart'; @@ -46,12 +49,17 @@ class PriceChartPage extends StatelessWidget { state.data.firstOrNull?.data.lastOrNull?.usdValue ?? 0, ), ), - availableCoins: state.availableCoins.keys.toList(), + availableCoins: state.availableCoins.keys + .map( + (e) => getSdkAsset(context.read(), e).id, + ) + .toList(), selectedCoinId: state.data.firstOrNull?.info.ticker, onCoinSelected: (coinId) { context.read().add( PriceChartCoinsSelected( - coinId == null ? [] : [coinId]), + coinId == null ? [] : [coinId], + ), ); }, centreAmount: @@ -67,13 +75,14 @@ class PriceChartPage extends StatelessWidget { }, customCoinItemBuilder: (coinId) { final coin = state.availableCoins[coinId]; - return CoinSelectItem( - coinId: coinId, + return SelectItem( + id: coinId.id, + value: coinId, trailing: TrendPercentageText( investmentReturnPercentage: coin?.selectedPeriodIncreasePercentage ?? 0, ), - name: coin?.name ?? coinId, + title: coin?.name ?? coinId.name, ); }, ), 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 505f7396a9..214f2af7cb 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_overview.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; 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/coins_bloc/coins_bloc.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index e2b064fd0d..978c886d15 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import file_picker import firebase_analytics import firebase_core import flutter_inappwebview_macos @@ -20,6 +21,7 @@ import video_player_avfoundation import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/packages/komodo_ui_kit/lib/komodo_ui_kit.dart b/packages/komodo_ui_kit/lib/komodo_ui_kit.dart index 090a753745..e134fd30b0 100644 --- a/packages/komodo_ui_kit/lib/komodo_ui_kit.dart +++ b/packages/komodo_ui_kit/lib/komodo_ui_kit.dart @@ -6,7 +6,6 @@ library komodo_ui_kit; // Buttons // This category includes various button widgets used throughout the UI, // providing different styles and functionalities. -export 'src/buttons/divided_button.dart'; // New button export 'src/buttons/hyperlink.dart'; export 'src/buttons/language_switcher/language_switcher.dart'; export 'src/buttons/multiselect_dropdown/filter_container.dart'; @@ -36,7 +35,6 @@ export 'src/custom_icons/custom_icons.dart'; // Display // Widgets primarily focused on displaying data and information. export 'src/display/statistic_card.dart'; -export 'src/display/trend_percentage_text.dart'; // Dividers // Widgets for dividing content or adding scrollbars. export 'src/dividers/ui_divider.dart'; diff --git a/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart b/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart deleted file mode 100644 index 9d4bab2063..0000000000 --- a/packages/komodo_ui_kit/lib/src/buttons/divided_button.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -class DividedButton extends StatelessWidget { - final List children; - final EdgeInsetsGeometry? childPadding; - final VoidCallback? onPressed; - - const DividedButton({ - required this.children, - this.childPadding = const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - super.key, - this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return FilledButton( - style: - (Theme.of(context).segmentedButtonTheme.style ?? const ButtonStyle()) - .copyWith( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - textStyle: WidgetStatePropertyAll( - Theme.of(context).textTheme.labelMedium, - ), - padding: const WidgetStatePropertyAll(EdgeInsets.zero), - backgroundColor: WidgetStatePropertyAll( - Theme.of(context) - .segmentedButtonTheme - .style - ?.backgroundColor - ?.resolve({WidgetState.focused}) ?? - Theme.of(context).colorScheme.surface, - ), - ), - onPressed: onPressed, - child: Row( - children: [ - for (int i = 0; i < children.length; i++) ...[ - Padding( - padding: childPadding!, - child: children[i], - ), - if (i < children.length - 1) - const SizedBox( - height: 32, - child: VerticalDivider( - width: 1, - thickness: 1, - indent: 2, - endIndent: 2, - ), - ), - ], - ], - ), - ); - } -} diff --git a/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart b/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart index 9dc0f266f6..0424ee4a9d 100644 --- a/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart +++ b/packages/komodo_ui_kit/lib/src/controls/market_chart_header_controls.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/src/controls/selected_coin_graph_control.dart'; -import 'package:komodo_ui_kit/src/inputs/coin_search_dropdown.dart'; import 'package:komodo_ui_kit/src/inputs/time_period_selector.dart'; import 'package:komodo_ui_kit/src/utils/gap.dart'; @@ -8,7 +9,7 @@ class MarketChartHeaderControls extends StatelessWidget { final Widget title; final Widget? leadingIcon; final Widget leadingText; - final List availableCoins; + final List availableCoins; final String? selectedCoinId; final void Function(String?)? onCoinSelected; final double centreAmount; @@ -16,7 +17,7 @@ class MarketChartHeaderControls extends StatelessWidget { final List timePeriods; final Duration selectedPeriod; final void Function(Duration?) onPeriodChanged; - final CoinSelectItem Function(String coinId)? customCoinItemBuilder; + final SelectItem Function(AssetId coinId)? customCoinItemBuilder; final bool emptySelectAllowed; const MarketChartHeaderControls({ diff --git a/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart b/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart index 227a9a6c7c..ed16b95cd9 100644 --- a/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart +++ b/packages/komodo_ui_kit/lib/src/controls/selected_coin_graph_control.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:komodo_ui_kit/src/buttons/divided_button.dart'; -import 'package:komodo_ui_kit/src/display/trend_percentage_text.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/utils.dart'; import 'package:komodo_ui_kit/src/images/coin_icon.dart'; -import 'package:komodo_ui_kit/src/inputs/coin_search_dropdown.dart'; class SelectedCoinGraphControl extends StatelessWidget { const SelectedCoinGraphControl({ @@ -26,9 +25,9 @@ class SelectedCoinGraphControl extends StatelessWidget { /// A list of coin IDs that are available for selection. /// /// Must be non-null and not empty if [onCoinSelected] is non-null. - final List? availableCoins; + final List? availableCoins; - final CoinSelectItem Function(String)? customCoinItemBuilder; + final SelectItem Function(AssetId)? customCoinItemBuilder; @override Widget build(BuildContext context) { @@ -50,7 +49,7 @@ class SelectedCoinGraphControl extends StatelessWidget { customCoinItemBuilder: customCoinItemBuilder, ); if (selectedCoin != null) { - onCoinSelected?.call(selectedCoin.coinId); + onCoinSelected?.call(selectedCoin.id); } }, children: [ diff --git a/packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart b/packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart deleted file mode 100644 index 966be6440d..0000000000 --- a/packages/komodo_ui_kit/lib/src/display/trend_percentage_text.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flutter/material.dart'; - -class TrendPercentageText extends StatelessWidget { - const TrendPercentageText({ - super.key, - required this.investmentReturnPercentage, - }); - - final double investmentReturnPercentage; - - @override - Widget build(BuildContext context) { - final iconTextColor = investmentReturnPercentage > 0 - ? Colors.green - : investmentReturnPercentage == 0 - ? Theme.of(context).disabledColor - : Theme.of(context).colorScheme.error; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - investmentReturnPercentage > 0 - ? Icons.trending_up - : (investmentReturnPercentage == 0) - ? Icons.trending_flat - : Icons.trending_down, - color: iconTextColor, - ), - const SizedBox(width: 2), - Text( - '${(investmentReturnPercentage).toStringAsFixed(2)}%', - style: (Theme.of(context).textTheme.bodyLarge ?? - const TextStyle( - fontSize: 12, - )) - .copyWith(color: iconTextColor), - ), - ], - ); - } -} diff --git a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart index 4d729332ad..58cef221b5 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -2,7 +2,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -const coinImagesFolder = 'packages/komodo_defi_framework/assets/coin_icons/png/'; +const coinImagesFolder = + 'packages/komodo_defi_framework/assets/coin_icons/png/'; // NB: ENSURE IT STAYS IN SYNC WITH MAIN PROJECT in `lib/src/utils/utils.dart`. const mediaCdnUrl = 'https://komodoplatform.github.io/coins/icons/'; @@ -67,7 +68,12 @@ Future checkIfAssetExists(String abbr) async { } } +const _deprecatedCoinIconMessage = + 'CoinIcon is deprecated. Use AssetIcon from the SDK\'s `komodo_ui` package instead.'; + +@Deprecated(_deprecatedCoinIconMessage) class CoinIcon extends StatelessWidget { + @Deprecated(_deprecatedCoinIconMessage) const CoinIcon( this.coinAbbr, { this.size = 20, @@ -77,6 +83,7 @@ class CoinIcon extends StatelessWidget { /// Convenience constructor for creating a coin icon from a symbol aka /// abbreviation. This avoids having to call [abbr2Ticker] manually. + @Deprecated(_deprecatedCoinIconMessage) CoinIcon.ofSymbol( String symbol, { this.size = 20, diff --git a/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart b/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart index 06c058219f..8b5578f333 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/coin_search_dropdown.dart @@ -1,389 +1,66 @@ import 'package:flutter/material.dart'; -import 'dart:async'; - +import 'package:komodo_ui/komodo_ui.dart'; import 'package:komodo_ui_kit/src/images/coin_icon.dart'; -class CoinSelectItem { - CoinSelectItem({ - required this.name, - required this.coinId, - this.leading, - this.trailing, +/// A specialized version of [SearchableSelect] for cryptocurrency selection. +class CoinSelect extends StatelessWidget { + const CoinSelect({ + super.key, + required this.coins, + required this.onCoinSelected, + this.customCoinItemBuilder, + this.initialCoin, + this.controller, }); - final String name; - final String coinId; - final Widget? trailing; - final Widget? leading; -} + /// List of coin IDs to show in the selector + final List coins; -class CryptoSearchDelegate extends SearchDelegate { - CryptoSearchDelegate(this.items); + /// Callback when a coin is selected + final Function(String coinId) onCoinSelected; - final Iterable items; + /// Optional custom builder for coin items + final SelectItem Function(String coinId)? customCoinItemBuilder; - @override - List buildActions(BuildContext context) { - return [ - IconButton(icon: const Icon(Icons.clear), onPressed: () => query = ''), - ]; - } + /// Optional initial selected coin + final String? initialCoin; - @override - Widget buildLeading(BuildContext context) { - return IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => close(context, null), - ); - } - - @override - Widget buildResults(BuildContext context) { - final results = items - .where((item) => item.name.toLowerCase().contains(query.toLowerCase())) - .toList(); + /// Optional controller for external state management + final SearchableSelectController? controller; - return ListView.builder( - itemCount: results.length, - itemBuilder: (context, index) { - final item = results[index]; - return CoinListTile( - item: item, - onTap: () => close(context, item), - ); - }, + SelectItem _defaultCoinItemBuilder(String coin) { + return SelectItem( + id: coin, + title: coin, + value: coin, + leading: CoinIcon.ofSymbol(coin), ); } - @override - Widget buildSuggestions(BuildContext context) { - final suggestions = items - .where((item) => item.name.toLowerCase().contains(query.toLowerCase())) - .toList(); - - return ListView.builder( - itemCount: suggestions.length, - itemBuilder: (context, index) { - final item = suggestions[index]; - return CoinListTile( - item: item, - onTap: () => query = item.name, - ); - }, - ); - } -} - -class CoinListTile extends StatelessWidget { - const CoinListTile({ - Key? key, - required this.item, - this.onTap, - }) : super(key: key); - - final CoinSelectItem item; - final VoidCallback? onTap; - @override Widget build(BuildContext context) { - return ListTile( - leading: item.leading ?? CoinIcon.ofSymbol(item.coinId), - title: Text(item.name), - trailing: item.trailing, - onTap: onTap, - ); - } -} - -Future showCoinSearch( - BuildContext context, { - required List coins, - CoinSelectItem Function(String coinId)? customCoinItemBuilder, -}) async { - final isMobile = MediaQuery.of(context).size.width < 600; - - final items = coins.map( - (coin) => - customCoinItemBuilder?.call(coin) ?? _defaultCoinItemBuilder(coin), - ); - - if (isMobile) { - return await showSearch( - context: context, - delegate: CryptoSearchDelegate(items), - ); - } else { - return await showDropdownSearch(context, items); - } -} - -CoinSelectItem _defaultCoinItemBuilder(String coin) { - return CoinSelectItem( - name: coin, - coinId: coin, - leading: CoinIcon.ofSymbol(coin), - ); -} - -OverlayEntry? _overlayEntry; -Completer? _completer; - -Future showDropdownSearch( - BuildContext context, - Iterable items, -) async { - final renderBox = context.findRenderObject() as RenderBox; - final offset = renderBox.localToGlobal(Offset.zero); - - void clearOverlay() { - _overlayEntry?.remove(); - _overlayEntry = null; - _completer = null; - } - - void onItemSelected(CoinSelectItem? item) { - _completer?.complete(item); - clearOverlay(); - } - - clearOverlay(); - - _completer = Completer(); - _overlayEntry = OverlayEntry( - builder: (context) { - return GestureDetector( - onTap: () => onItemSelected(null), - behavior: HitTestBehavior.translucent, - child: Stack( - children: [ - Positioned( - left: offset.dx, - top: offset.dy + renderBox.size.height, - width: 300, - child: _DropdownSearch( - items: items, - onSelected: onItemSelected, - ), - ), - ], - ), - ); - }, - ); - - WidgetsBinding.instance.addPostFrameCallback((_) { - Overlay.of(context).insert(_overlayEntry!); - }); - - return _completer!.future; -} - -class _DropdownSearch extends StatefulWidget { - final Iterable items; - final ValueChanged onSelected; - - const _DropdownSearch({required this.items, required this.onSelected}); - - @override - State<_DropdownSearch> createState() => __DropdownSearchState(); -} - -class __DropdownSearchState extends State<_DropdownSearch> { - late Iterable filteredItems; - String query = ''; - final FocusNode _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - filteredItems = widget.items; - - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _focusNode.requestFocus(); - } - }); - } - - void updateSearchQuery(String newQuery) { - setState(() { - query = newQuery; - filteredItems = widget.items.where( - (item) => item.name.toLowerCase().contains(query.toLowerCase()), - ); - }); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - color: Theme.of(context).colorScheme.surfaceContainerLow, - child: Container( - constraints: const BoxConstraints( - maxHeight: 300, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: TextField( - focusNode: _focusNode, - autofocus: true, - decoration: InputDecoration( - hintText: 'Search', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - prefixIcon: const Icon(Icons.search), - ), - onChanged: updateSearchQuery, - ), - ), - Flexible( - child: ListView.builder( - itemCount: filteredItems.length, - itemBuilder: (context, index) { - final item = filteredItems.elementAt(index); - return CoinListTile( - item: item, - onTap: () => widget.onSelected(item), - ); - }, - ), - ), - ], - ), - ), - ); - } -} - -class CoinDropdown extends StatefulWidget { - final List items; - final Function(CoinSelectItem) onItemSelected; - - const CoinDropdown({ - super.key, - required this.items, - required this.onItemSelected, - }); - - @override - State createState() => _CoinDropdownState(); -} - -class _CoinDropdownState extends State { - CoinSelectItem? selectedItem; - - void _showSearch(BuildContext context) async { - final selected = await showCoinSearch( - context, - coins: widget.items.map((e) => e.coinId).toList(), - customCoinItemBuilder: (coinId) { - return widget.items.firstWhere((e) => e.coinId == coinId); - }, - ); - if (selected != null) { - setState(() { - selectedItem = selected; - }); - widget.onItemSelected(selected); - } - } + final items = coins + .map( + (coin) => + customCoinItemBuilder?.call(coin) ?? + _defaultCoinItemBuilder(coin), + ) + .toList(); - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () => _showSearch(context), - child: InputDecorator( - isEmpty: selectedItem == null, - decoration: const InputDecoration( - hintText: 'Select a Coin', - border: OutlineInputBorder(), - ), - child: selectedItem == null - ? null - : Row( - children: [ - Text(selectedItem!.name), - const Spacer(), - selectedItem?.trailing ?? const SizedBox(), - ], - ), - ), + // Find initial value if provided + final initialValue = initialCoin != null + ? items.firstWhere( + (item) => item.value == initialCoin, + orElse: () => items.first, + ) + : null; + + return SearchableSelect( + items: items, + onItemSelected: (item) => onCoinSelected(item.value), + hint: 'Select a coin', + initialValue: initialValue?.value, + controller: controller, ); } } - -// Example usage - -// void main() { -// runApp(const MyApp()); -// } - -// class MyApp extends StatelessWidget { -// const MyApp({super.key}); - -// @override -// Widget build(BuildContext context) { -// final items = [ -// CoinSelectItem( -// name: "KMD", -// coinId: "KMD", -// trailing: const Text('+2.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "SecondLive", -// coinId: "SL", -// trailing: const Text('+322.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "KiloEx", -// coinId: "KE", -// trailing: const Text('-2.09%', style: TextStyle(color: Colors.red)), -// ), -// CoinSelectItem( -// name: "Native", -// coinId: "NT", -// trailing: const Text('+225.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "XY Finance", -// coinId: "XY", -// trailing: const Text('+62.9%', style: TextStyle(color: Colors.green)), -// ), -// CoinSelectItem( -// name: "KMD", -// coinId: "KMD", -// trailing: const Text('+2.9%', style: TextStyle(color: Colors.green)), -// ), -// ]; - -// return MaterialApp( -// home: Scaffold( -// appBar: AppBar(title: const Text('Crypto Selector')), -// body: Padding( -// padding: const EdgeInsets.all(16.0), -// child: CoinDropdown( -// items: items, -// onItemSelected: (item) { -// // Handle item selection -// print('Selected item: ${item.name}'); -// }, -// ), -// ), -// ), -// ); -// } -// } diff --git a/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart b/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart index 8787b4101d..ffbff35f09 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/percentage_input.dart @@ -28,7 +28,7 @@ class PercentageInput extends StatefulWidget { class _PercentageInputState extends State { late TextEditingController _controller; - String _lastEmittedValue = ''; + String? _lastEmittedValue; bool _shouldUpdateText = true; @override @@ -60,11 +60,11 @@ class _PercentageInputState extends State { } } - void _handlePercentageChanged(String value) { + void _handlePercentageChanged(String? value) { if (value != _lastEmittedValue) { _lastEmittedValue = value; _shouldUpdateText = false; - widget.onChanged?.call(value); + widget.onChanged?.call(value ?? ''); _shouldUpdateText = true; } } @@ -97,7 +97,7 @@ class _PercentageInputState extends State { widget.maxFractionDigits.toString() + r'})?$', ), - replacementString: _lastEmittedValue, + replacementString: _lastEmittedValue ?? '', ), _DecimalInputFormatter(), ], diff --git a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart index 4b840a9b10..ea35716a21 100644 --- a/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart +++ b/packages/komodo_ui_kit/lib/src/inputs/ui_text_form_field.dart @@ -15,16 +15,19 @@ import 'package:komodo_ui_kit/komodo_ui_kit.dart'; /// /// The `UiTextFormField` can be customized using various parameters such as /// `hintText`, `controller`, `inputFormatters`, `textInputAction`, and more. + class UiTextFormField extends StatefulWidget { const UiTextFormField({ super.key, this.initialValue, this.hintText, + this.labelText, this.controller, this.inputFormatters, this.textInputAction, this.style, this.hintTextStyle, + this.labelStyle, this.inputContentPadding, this.keyboardType, this.validator, @@ -48,7 +51,7 @@ class UiTextFormField extends StatefulWidget { this.maxLength, this.maxLengthEnforcement, this.counterText, - this.labelStyle, + this.helperText, this.enabledBorder, this.focusedBorder, this.errorStyle, @@ -57,6 +60,8 @@ class UiTextFormField extends StatefulWidget { final String? initialValue; final String? hintText; + final String? labelText; + final String? helperText; final TextEditingController? controller; final List? inputFormatters; final TextInputAction? textInputAction; @@ -79,7 +84,7 @@ class UiTextFormField extends StatefulWidget { final FocusNode? focusNode; final void Function(FocusNode)? onFocus; final Color? fillColor; - final void Function(String)? onChanged; + final void Function(String?)? onChanged; final void Function(String)? onFieldSubmitted; final String? Function(String?)? validator; final Widget? suffix; @@ -96,17 +101,18 @@ class UiTextFormField extends StatefulWidget { } class _UiTextFormFieldState extends State { - String? _hintText; String? _errorText; String? _displayedErrorText; - FocusNode _focusNode = FocusNode(); + late FocusNode _focusNode; bool _hasFocusExitedOnce = false; bool _shouldValidate = false; + TextEditingController? _controller; @override void initState() { super.initState(); - _hintText = widget.hintText; + _controller = + widget.controller ?? TextEditingController(text: widget.initialValue); _errorText = widget.errorText; _displayedErrorText = widget.errorText; @@ -115,10 +121,8 @@ class _UiTextFormFieldState extends State { _hasFocusExitedOnce = true; _shouldValidate = true; } - if (widget.focusNode != null) { - _focusNode = widget.focusNode!; - } + _focusNode = widget.focusNode ?? FocusNode(); _focusNode.addListener(_handleFocusChange); } @@ -127,41 +131,45 @@ class _UiTextFormFieldState extends State { super.didUpdateWidget(oldWidget); if (widget.errorText != oldWidget.errorText) { - setState(() { - _errorText = widget.errorText; - _displayedErrorText = widget.errorText; - if (_errorText?.isNotEmpty == true) { - _hasFocusExitedOnce = true; - _shouldValidate = true; - } - }); + _errorText = widget.errorText; + _displayedErrorText = widget.errorText; + if (_errorText?.isNotEmpty == true) { + _hasFocusExitedOnce = true; + _shouldValidate = true; + } } - } - @override - void dispose() { - _focusNode.removeListener(_handleFocusChange); - _focusNode.dispose(); - super.dispose(); + if (widget.initialValue != oldWidget.initialValue && + widget.controller == null) { + _controller?.text = widget.initialValue ?? ''; + } } - /// Handles the focus change events. void _handleFocusChange() { + if (!mounted) return; + + final shouldUpdate = !_focusNode.hasFocus && + (widget.validationMode == InputValidationMode.eager || + widget.validationMode == InputValidationMode.passive); + + if (shouldUpdate) { + _hasFocusExitedOnce = true; + _shouldValidate = true; + // Schedule validation for the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _validateAndUpdateError(_controller?.text); + }); + } + }); + } + setState(() { - _hintText = _focusNode.hasFocus ? null : widget.hintText; if (widget.onFocus != null) { widget.onFocus!(_focusNode); } - if (!_focusNode.hasFocus) { - if (!_hasFocusExitedOnce) { - _hasFocusExitedOnce = true; - } - if (widget.validationMode == InputValidationMode.eager || - widget.validationMode == InputValidationMode.lazy) { - _shouldValidate = true; - _performValidation(); - } - } + if (_focusNode.hasFocus && widget.validationMode == InputValidationMode.aggressive) { _shouldValidate = true; @@ -169,46 +177,73 @@ class _UiTextFormFieldState extends State { }); } + // Separate validation logic from state updates + String? _validateAndUpdateError(String? value) { + final error = widget.validator?.call(value) ?? widget.errorText; + _errorText = error; + _displayedErrorText = + _hasFocusExitedOnce || _focusNode.hasFocus ? _errorText : null; + return error; + } + @override Widget build(BuildContext context) { - final widgetStyle = widget.style; - var style = TextStyle( + final theme = Theme.of(context); + + final defaultStyle = TextStyle( fontSize: 14, fontWeight: FontWeight.w400, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: theme.textTheme.bodyMedium?.color, ); - if (widgetStyle != null) { - style = style.merge(widgetStyle); - } + final style = widget.style?.merge(defaultStyle) ?? defaultStyle; - final TextStyle? hintTextStyle = Theme.of(context) - .inputDecorationTheme - .hintStyle - ?.merge(widget.hintTextStyle); + final defaultLabelStyle = theme.inputDecorationTheme.labelStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.8), + ); + final labelStyle = + widget.labelStyle?.merge(defaultLabelStyle) ?? defaultLabelStyle; - final TextStyle? labelStyle = Theme.of(context) - .inputDecorationTheme - .labelStyle - ?.merge(widget.labelStyle); + final defaultHintStyle = theme.inputDecorationTheme.hintStyle ?? + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5), + ); + final hintStyle = + widget.hintTextStyle?.merge(defaultHintStyle) ?? defaultHintStyle; - final TextStyle? errorStyle = Theme.of(context) - .inputDecorationTheme - .errorStyle - ?.merge(widget.errorStyle); + final defaultErrorStyle = theme.inputDecorationTheme.errorStyle ?? + TextStyle( + fontSize: 12, + color: theme.colorScheme.error, + ); + final errorStyle = + widget.errorStyle?.merge(defaultErrorStyle) ?? defaultErrorStyle; return TextFormField( + controller: _controller, maxLength: widget.maxLength, maxLengthEnforcement: widget.maxLengthEnforcement, - initialValue: widget.initialValue, - controller: widget.controller, inputFormatters: widget.inputFormatters, - validator: (value) => _performValidation(value), + validator: (value) { + // Don't update state during build, just return the validation result + final error = widget.validator?.call(value) ?? widget.errorText; + return _shouldValidate ? error : null; + }, onChanged: (value) { - if (widget.onChanged != null) { - widget.onChanged!(value); - } + widget.onChanged?.call(value); if (_shouldValidate) { - _performValidation(value); + // Schedule state update for the next frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _validateAndUpdateError(value); + }); + } + }); } }, onFieldSubmitted: widget.onFieldSubmitted, @@ -228,13 +263,15 @@ class _UiTextFormFieldState extends State { enabled: widget.enabled, decoration: InputDecoration( fillColor: widget.fillColor, - hintText: _hintText, - hintStyle: hintTextStyle, - contentPadding: widget.inputContentPadding, + filled: widget.fillColor != null, + hintText: widget.hintText, + hintStyle: hintStyle, + contentPadding: widget.inputContentPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), counterText: widget.counterText, - labelText: widget.hintText, - labelStyle: - _hintText != null && !_hasValue ? hintTextStyle : labelStyle, + labelText: widget.labelText ?? widget.hintText, + labelStyle: labelStyle, + helperText: widget.helperText, errorText: _displayedErrorText, errorStyle: errorStyle, prefixIcon: widget.prefixIcon, @@ -246,25 +283,4 @@ class _UiTextFormFieldState extends State { ), ); } - - /// Checks if the field has a value. - bool get _hasValue => - (widget.controller?.text.isNotEmpty ?? false) || - (widget.initialValue?.isNotEmpty ?? false); - - /// Performs validation based on the validator function and updates error state. - String? _performValidation([String? value]) { - final error = widget.validator?.call(value ?? widget.controller?.text) ?? - widget.errorText; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _errorText = error; - _displayedErrorText = - _hasFocusExitedOnce || _focusNode.hasFocus ? _errorText : null; - }); - } - }); - return error; - } } diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index 1ac29b9508..cee3945cd7 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -20,10 +20,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: @@ -32,6 +32,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + decimal: + dependency: transitive + description: + name: decimal + sha256: "4140a688f9e443e2f4de3a1162387bf25e1ac6d51e24c9da263f245210f41440" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" flutter: dependency: "direct main" description: flutter @@ -45,6 +61,19 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" intl: dependency: "direct main" description: @@ -53,6 +82,41 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + komodo_defi_rpc_methods: + dependency: transitive + description: + path: "packages/komodo_defi_rpc_methods" + ref: dev + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_defi_types: + dependency: "direct main" + description: + path: "packages/komodo_defi_types" + ref: dev + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" + komodo_ui: + dependency: "direct main" + description: + path: "packages/komodo_ui" + ref: dev + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" lints: dependency: transitive description: @@ -77,14 +141,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mobile_scanner: + dependency: transitive + description: + name: mobile_scanner + sha256: "8b4d42bbdd5cbba78462e7074d99d413fd6ca998bceb1fa77f54423ead7014f0" + url: "https://pub.dev" + source: hosted + version: "6.0.5" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -93,6 +165,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" sky_engine: dependency: transitive description: flutter @@ -106,6 +186,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=2.5.0" + dart: ">=3.4.4 <4.0.0" + flutter: ">=3.22.0" diff --git a/packages/komodo_ui_kit/pubspec.yaml b/packages/komodo_ui_kit/pubspec.yaml index b20c8ab055..c6f72e5628 100644 --- a/packages/komodo_ui_kit/pubspec.yaml +++ b/packages/komodo_ui_kit/pubspec.yaml @@ -12,6 +12,20 @@ dependencies: app_theme: path: ../../app_theme/ + komodo_defi_types: + # path: sdk/packages/komodo_defi_types # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_defi_types + ref: dev + + komodo_ui: + # path: ../../sdk/packages/komodo_ui # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_ui + ref: dev + dev_dependencies: flutter_lints: ^2.0.0 # flutter.dev @@ -22,6 +36,6 @@ flutter: - lib/src/custom_icons/Custom.ttf fonts: - - family: Custom - fonts: - - asset: lib/src/custom_icons/Custom.ttf \ No newline at end of file + - family: Custom + fonts: + - asset: lib/src/custom_icons/Custom.ttf diff --git a/pubspec.lock b/pubspec.lock index a3b0a8f9d9..183d93a51e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "67.0.0" + version: "76.0.0" _flutterfire_internals: dependency: transitive description: @@ -17,14 +17,19 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.33" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.11.0" app_theme: dependency: "direct main" description: @@ -44,10 +49,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.3" + version: "1.5.8" async: dependency: transitive description: @@ -118,10 +123,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" clock: dependency: transitive description: @@ -142,18 +147,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.11.1" cross_file: dependency: "direct main" description: @@ -247,10 +252,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -263,10 +268,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.3.1" file_system_access_api: dependency: transitive description: @@ -311,18 +316,18 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.4.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 + sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b url: "https://pub.dev" source: hosted - version: "2.18.1" + version: "2.19.0" fixnum: dependency: transitive description: @@ -361,58 +366,58 @@ packages: dependency: transitive description: name: flutter_inappwebview_android - sha256: f48203a11c5eb0c23dd5a3cb3638ae678056b6ceae22819373e36c6cb4f1d46a + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.3" flutter_inappwebview_internal_annotations: dependency: transitive description: name: flutter_inappwebview_internal_annotations - sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" flutter_inappwebview_ios: dependency: transitive description: name: flutter_inappwebview_ios - sha256: f6f88d464b38f2fc1c5f2ae74024498115eb1470715bd8b40f902dd4ac99ccc8 + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_inappwebview_macos: dependency: transitive description: name: flutter_inappwebview_macos - sha256: "68e0c3785d8d789710cda7d7efe6effa337c91bf300dd28af7efc2d358fa1a98" + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_inappwebview_platform_interface: dependency: transitive description: name: flutter_inappwebview_platform_interface - sha256: "97b4ab116d949ede20c90c7e3d15d24afaf1b706cc0af96b060770293cd6c49d" + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0+1" flutter_inappwebview_web: dependency: transitive description: name: flutter_inappwebview_web - sha256: f7f97b6faa39416e4e86da1184edd4de6c27b271d036f0838ea3ff9a250a1de2 + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_inappwebview_windows: dependency: transitive description: name: flutter_inappwebview_windows - sha256: "86702d2109384311f8ea634855e90ee143b9bfabddd3858696d905a2c28808aa" + sha256: "1cb428210f7486e4decb52262b243505023d0c57609c6ff5cee772e63b51f443" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" flutter_lints: dependency: "direct dev" description: @@ -438,34 +443,34 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.24" flutter_secure_storage: dependency: transitive description: name: flutter_secure_storage - sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" url: "https://pub.dev" source: hosted - version: "9.2.2" + version: "9.2.4" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -525,6 +530,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -542,10 +555,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" hex: dependency: transitive description: @@ -592,18 +605,18 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" integration_test: dependency: "direct dev" description: flutter @@ -621,10 +634,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: "direct main" description: @@ -633,6 +646,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" komodo_cex_market_data: dependency: "direct main" description: @@ -645,55 +666,55 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.0.1" + version: "0.2.0+0" komodo_defi_framework: dependency: transitive description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.1.0" + version: "0.2.0" komodo_defi_local_auth: dependency: transitive description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.1.0+1" + version: "0.2.0+0" komodo_defi_rpc_methods: dependency: transitive description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.1.0+1" + version: "0.2.0+0" komodo_defi_sdk: dependency: "direct main" description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.1.0+1" + version: "0.2.0+0" komodo_defi_types: dependency: "direct main" description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.0.1" + version: "0.2.0+0" komodo_persistence_layer: dependency: "direct main" description: @@ -701,6 +722,15 @@ packages: relative: true source: path version: "0.0.1" + komodo_ui: + dependency: "direct main" + description: + path: "packages/komodo_ui" + ref: dev + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" + url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" + source: git + version: "0.2.0+0" komodo_ui_kit: dependency: "direct main" description: @@ -713,10 +743,10 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "87ed8da39a14590e89ca9c47e84ecb7cb67186e6" + resolved-ref: "9c75bb7b2d3a556cec4119ad29fdb7fa93fe599e" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git - version: "0.0.1" + version: "0.2.0+0" leak_tracker: dependency: transitive description: @@ -769,10 +799,10 @@ packages: dependency: transitive description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.3" local_auth_platform_interface: dependency: transitive description: @@ -793,18 +823,26 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" markdown: dependency: transitive description: name: markdown - sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" matcher: dependency: transitive description: @@ -833,18 +871,18 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mobile_scanner: - dependency: "direct main" + dependency: transitive description: name: mobile_scanner - sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + sha256: "8b4d42bbdd5cbba78462e7074d99d413fd6ca998bceb1fa77f54423ead7014f0" url: "https://pub.dev" source: hosted - version: "5.2.3" + version: "6.0.5" mutex: dependency: transitive description: @@ -873,10 +911,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: @@ -906,10 +944,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -922,18 +960,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -946,18 +984,18 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -1018,10 +1056,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" qr: dependency: transitive description: @@ -1075,18 +1113,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1123,10 +1161,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_packages_handler: dependency: transitive description: @@ -1139,18 +1177,18 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -1160,18 +1198,18 @@ packages: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -1208,10 +1246,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -1264,10 +1302,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_html: dependency: "direct main" description: @@ -1297,34 +1335,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1337,18 +1375,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1361,26 +1399,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: a1870d398158844fe5db12441611ed9a2222ff4340258b539eaf3590c1b4bd7e url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.17" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1401,34 +1439,34 @@ packages: dependency: transitive description: name: video_player_android - sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" + sha256: "7018dbcb395e2bca0b9a898e73989e67c0c4a5db269528e1b036ca38bcca0d0b" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.7.17" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c + sha256: "8a4e73a3faf2b13512978a43cf1cdda66feeeb900a0527f1fbfd7b19cf3458d3" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" + sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" vm_service: dependency: transitive description: @@ -1441,10 +1479,10 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: @@ -1453,14 +1491,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.2" webdriver: dependency: transitive description: @@ -1481,10 +1527,10 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.1" window_size: dependency: "direct main" description: @@ -1498,10 +1544,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1514,10 +1560,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.3 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index de28c6ea8d..14e9f4a2d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -161,11 +161,6 @@ dependencies: # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/3 flutter_inappwebview: 6.1.3 # Android, iOS, macOS, Web (currently broke, open issue) - # Newly added, not yet reviewed - # TODO: review required - # MRC: At least 3.3.0 is needed for AGP 8.0+ compatibility on Android - mobile_scanner: ^5.1.0 - # Newly added, not yet reviewed formz: 0.7.0 @@ -182,16 +177,25 @@ dependencies: uuid: 4.4.2 # sdk depends on this version flutter_bloc: 8.1.6 # sdk depends on this version, and hosted instead of git reference komodo_defi_sdk: # TODO: change to pub.dev version? + # path: sdk/packages/komodo_defi_sdk # Requires symlink to the SDK in the root of the project git: url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git path: packages/komodo_defi_sdk ref: dev komodo_defi_types: + # path: sdk/packages/komodo_defi_types # Requires symlink to the SDK in the root of the project git: url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git path: packages/komodo_defi_types ref: dev + + komodo_ui: + # path: sdk/packages/komodo_ui # Requires symlink to the SDK in the root of the project + git: + url: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git + path: packages/komodo_ui + ref: dev dev_dependencies: integration_test: # SDK diff --git a/test_units/tests/cex_market_data/profit_loss_repository_test.dart b/test_units/tests/cex_market_data/profit_loss_repository_test.dart index 43dc0b5a13..0a4097bea8 100644 --- a/test_units/tests/cex_market_data/profit_loss_repository_test.dart +++ b/test_units/tests/cex_market_data/profit_loss_repository_test.dart @@ -250,4 +250,4 @@ void testRealisedProfitLossRepository() { expect(result[1].profitLoss, 0.0); }); }); -} \ No newline at end of file +}