From 5ba6ba1595e3951b5dd63f72f25ae027802dbe15 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Thu, 27 Mar 2025 17:47:22 +0100 Subject: [PATCH 01/13] feat: Rework unauthenticated assets list --- firebase.json | 4 +- lib/bloc/coins_bloc/coins_bloc.dart | 53 ++-- lib/bloc/coins_bloc/coins_state.dart | 34 ++- lib/main.dart | 4 +- lib/model/cex_price.dart | 29 +- .../extensions/collection_extensions.dart | 37 +++ lib/shared/utils/utils.dart | 6 +- lib/shared/widgets/asset_item/asset_item.dart | 46 +++ .../widgets/asset_item/asset_item_body.dart | 42 +++ .../widgets/asset_item/asset_item_size.dart | 45 +++ .../asset_item/asset_item_subtitle.dart | 36 +++ .../widgets/asset_item/asset_item_title.dart | 30 ++ .../charts/coin_sparkline.dart | 4 +- .../wallet_page/common/asset_list_item.dart | 42 +++ .../common/asset_list_item_desktop.dart | 80 ++++++ .../common/asset_list_item_mobile.dart | 45 +++ .../wallet_page/common/assets_list.dart | 114 ++++++++ .../common/coin_list_item_desktop.dart | 36 +-- .../common/grouped_asset_ticker_item.dart | 269 ++++++++++++++++++ .../common/grouped_assets_list.dart | 81 ++++++ .../wallet_page/common/wallet_coins_list.dart | 55 ++-- .../wallet_main/all_coins_list.dart | 18 +- .../wallet_page/wallet_main/wallet_main.dart | 31 +- macos/Podfile.lock | 2 +- .../komodo_persistence_layer/pubspec.yaml | 6 +- packages/komodo_ui_kit/pubspec.lock | 6 +- pubspec.lock | 60 ++-- pubspec.yaml | 19 +- 28 files changed, 1056 insertions(+), 178 deletions(-) create mode 100644 lib/shared/utils/extensions/collection_extensions.dart create mode 100644 lib/shared/widgets/asset_item/asset_item.dart create mode 100644 lib/shared/widgets/asset_item/asset_item_body.dart create mode 100644 lib/shared/widgets/asset_item/asset_item_size.dart create mode 100644 lib/shared/widgets/asset_item/asset_item_subtitle.dart create mode 100644 lib/shared/widgets/asset_item/asset_item_title.dart create mode 100644 lib/views/wallet/wallet_page/common/asset_list_item.dart create mode 100644 lib/views/wallet/wallet_page/common/asset_list_item_desktop.dart create mode 100644 lib/views/wallet/wallet_page/common/asset_list_item_mobile.dart create mode 100644 lib/views/wallet/wallet_page/common/assets_list.dart create mode 100644 lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart create mode 100644 lib/views/wallet/wallet_page/common/grouped_assets_list.dart diff --git a/firebase.json b/firebase.json index c2d711b738..bef402c83b 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,6 @@ { "hosting": { - "site": "walletrc", + "site": "wallet-rc-device-preview", "public": "build/web", "ignore": [ "firebase.json", @@ -14,4 +14,4 @@ } ] } -} +} \ No newline at end of file diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart index 3117433181..9625579fa1 100644 --- a/lib/bloc/coins_bloc/coins_bloc.dart +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; @@ -13,6 +14,7 @@ import 'package:web_dex/model/cex_price.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/kdf_auth_metadata_extension.dart'; import 'package:web_dex/model/wallet.dart'; +import 'package:web_dex/shared/utils/utils.dart'; part 'coins_event.dart'; part 'coins_state.dart'; @@ -277,43 +279,36 @@ class CoinsBloc extends Bloc { CoinsPricesUpdated event, Emitter emit, ) async { - bool changed = false; final prices = await _coinsRepo.fetchCurrentPrices(); - if (prices == null) { _log.severe('Coin prices list empty/null'); return; } - - final coins = Map.of(state.coins); - for (final entry in state.coins.entries) { - final coin = entry.value; - final CexPrice? usdPrice = - prices[coin.id.symbol.configSymbol.toUpperCase()]; - - if (usdPrice != coin.usdPrice) { - changed = true; - // Create new coin instance with updated price - coins[entry.key] = coin.copyWith(usdPrice: usdPrice); - } + final didPricesChange = !mapEquals(state.prices, prices); + if (!didPricesChange) { + _log.info('Coin prices list unchanged'); + return; } - if (changed) { - final newWalletCoins = state.walletCoins.map( - (String coinId, Coin coin) => MapEntry( - coinId, - coin.copyWith(usdPrice: coins[coinId]!.usdPrice), - ), - ); - emit( - state.copyWith( - coins: coins, - walletCoins: {...state.walletCoins, ...newWalletCoins}, - ), - ); + Map updateCoinsWithPrices(Map coins) { + final map = coins.map((key, coin) { + final price = prices[coin.id.id]; + if (price != null) { + return MapEntry(key, coin.copyWith(usdPrice: price)); + } + return MapEntry(key, coin); + }); + + return Map.of(map).unmodifiable(); } - _log.info('Coin CEX prices updated'); + emit( + state.copyWith( + prices: prices.unmodifiable(), + coins: updateCoinsWithPrices(state.coins), + walletCoins: updateCoinsWithPrices(state.walletCoins), + ), + ); } Future _onLogin( @@ -560,3 +555,5 @@ class CoinsBloc extends Bloc { } } } + +// diff --git a/lib/bloc/coins_bloc/coins_state.dart b/lib/bloc/coins_bloc/coins_state.dart index ae1e9d196e..95a2a34140 100644 --- a/lib/bloc/coins_bloc/coins_state.dart +++ b/lib/bloc/coins_bloc/coins_state.dart @@ -6,6 +6,7 @@ class CoinsState extends Equatable { required this.walletCoins, required this.loginActivationFinished, required this.pubkeys, + required this.prices, }); factory CoinsState.initial() => const CoinsState( @@ -13,22 +14,25 @@ class CoinsState extends Equatable { walletCoins: {}, loginActivationFinished: false, pubkeys: {}, + prices: {}, ); final Map coins; final Map walletCoins; final bool loginActivationFinished; final Map pubkeys; + final Map prices; @override List get props => - [coins, walletCoins, loginActivationFinished, pubkeys]; + [coins, walletCoins, loginActivationFinished, pubkeys, prices]; CoinsState copyWith({ Map? coins, Map? walletCoins, bool? loginActivationFinished, Map? pubkeys, + Map? prices, }) { return CoinsState( coins: coins ?? this.coins, @@ -36,14 +40,38 @@ class CoinsState extends Equatable { loginActivationFinished: loginActivationFinished ?? this.loginActivationFinished, pubkeys: pubkeys ?? this.pubkeys, + prices: prices ?? this.prices, ); } - // TODO! Migrate to SDK + /// Gets the price for a given asset ID + CexPrice? getPriceForAsset(AssetId assetId) { + return prices[assetId.symbol.configSymbol.toUpperCase()]; + } + + /// Gets the 24h price change percentage for a given asset ID + double? get24hChangeForAsset(AssetId assetId) { + return getPriceForAsset(assetId)?.change24h; + } + + /// Calculates the USD price for a given amount of a coin + /// + /// [amount] The amount of the coin as a string + /// [coinAbbr] The abbreviation/symbol of the coin + /// + /// Returns null if: + /// - The coin is not found in the state + /// - The amount cannot be parsed to a double + /// - The coin does not have a USD price + /// + /// Note: This will be migrated to use the SDK's price functionality in the future. + /// See the MarketDataManager in the SDK for the new implementation. + @Deprecated('Use sdk.prices.fiatPrice(assetId) * amount instead') double? getUsdPriceByAmount(String amount, String coinAbbr) { final Coin? coin = coins[coinAbbr]; final double? parsedAmount = double.tryParse(amount); - final double? usdPrice = coin?.usdPrice?.price; + final CexPrice? cexPrice = prices[coinAbbr.toUpperCase()]; + final double? usdPrice = cexPrice?.price; if (coin == null || usdPrice == null || parsedAmount == null) { return null; diff --git a/lib/main.dart b/lib/main.dart index eec617899f..0ac2f26c12 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,6 @@ import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; -import 'package:universal_html/html.dart' as html; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/app_config/package_information.dart'; import 'package:web_dex/bloc/app_bloc_observer.dart'; @@ -121,8 +120,7 @@ PerformanceMode? _getPerformanceModeFromUrl() { : null; if (kIsWeb) { - final url = html.window.location.href; - final uri = Uri.parse(url); + final uri = Uri.base; maybeEnvPerformanceMode = uri.queryParameters['demo_mode_performance'] ?? maybeEnvPerformanceMode; } diff --git a/lib/model/cex_price.dart b/lib/model/cex_price.dart index 84d4b893b2..ee2e6f9540 100644 --- a/lib/model/cex_price.dart +++ b/lib/model/cex_price.dart @@ -1,6 +1,19 @@ -import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +enum CexDataProvider { + binance, + coingecko, + coinpaprika, + nomics, + unknown, +} + +CexDataProvider cexDataProvider(String string) { + return CexDataProvider.values.firstWhere( + (e) => e.toString().split('.').last == string, + orElse: () => CexDataProvider.unknown); +} + class CexPrice extends Equatable { const CexPrice({ required this.ticker, @@ -67,17 +80,3 @@ class CexPrice extends Equatable { changeProvider, ]; } - -enum CexDataProvider { - binance, - coingecko, - coinpaprika, - nomics, - unknown, -} - -CexDataProvider cexDataProvider(String string) { - return CexDataProvider.values - .firstWhereOrNull((e) => e.toString().split('.').last == string) ?? - CexDataProvider.unknown; -} diff --git a/lib/shared/utils/extensions/collection_extensions.dart b/lib/shared/utils/extensions/collection_extensions.dart new file mode 100644 index 0000000000..c7c0af8b58 --- /dev/null +++ b/lib/shared/utils/extensions/collection_extensions.dart @@ -0,0 +1,37 @@ +import 'dart:collection'; + +/// Extension to make Lists unmodifiable. +extension UnmodifiableListExtension on Iterable { + /// Returns an unmodifiable view of this iterable. + /// + /// NB! This references the original iterable, so if the original iterable is + /// modified, the unmodifiable view will reflect those changes. + /// + /// This is useful for preventing modifications to the list while still + /// allowing read access. + /// + /// This won't protect against modifications to the elements of the iterable + /// if they are mutable. + Iterable unmodifiable() => UnmodifiableListView(this); +} + +/// Extension to make Maps unmodifiable. +/// +/// NB! This references the original map, so if the original map is +/// modified, the unmodifiable view will reflect those changes. +/// +/// This is useful for preventing modifications to the map while still +/// allowing read access. +/// +/// This won't protect against modifications to the elements of the map +/// if they are mutable. +extension UnmodifiableMapExtension on Map { + /// Returns an unmodifiable view of this map. + /// + /// This is useful for preventing modifications to the map while still + /// allowing read access. + /// + /// This won't protect against modifications to the elements of the map + /// if they are mutable. + Map unmodifiable() => UnmodifiableMapView(this); +} diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index b23371277d..f8e1b4f367 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -17,10 +17,12 @@ import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/performance_analytics/performance_analytics.dart'; import 'package:web_dex/services/logger/get_logger.dart'; import 'package:web_dex/shared/constants.dart'; + export 'package:web_dex/shared/utils/extensions/async_extensions.dart'; -export 'package:web_dex/shared/utils/prominent_colors.dart'; -export 'package:web_dex/shared/utils/extensions/sdk_extensions.dart'; +export 'package:web_dex/shared/utils/extensions/collection_extensions.dart'; export 'package:web_dex/shared/utils/extensions/legacy_coin_migration_extensions.dart'; +export 'package:web_dex/shared/utils/extensions/sdk_extensions.dart'; +export 'package:web_dex/shared/utils/prominent_colors.dart'; void copyToClipBoard(BuildContext context, String str) { final themeData = Theme.of(context); diff --git a/lib/shared/widgets/asset_item/asset_item.dart b/lib/shared/widgets/asset_item/asset_item.dart new file mode 100644 index 0000000000..85b60a5560 --- /dev/null +++ b/lib/shared/widgets/asset_item/asset_item.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_body.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; + +/// A widget that displays an asset with its logo and information. +/// +/// This replaces the previous CoinItem and works with AssetId instead of Coin. +class AssetItem extends StatelessWidget { + const AssetItem({ + super.key, + required this.assetId, + this.amount, + this.size = AssetItemSize.medium, + this.subtitleText, + }); + + final AssetId? assetId; + final double? amount; + final AssetItemSize size; + final String? subtitleText; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AssetIcon( + assetId, + size: size.assetLogo, + ), + SizedBox(width: 8), + Flexible( + child: AssetItemBody( + assetId: assetId, + amount: amount, + size: size, + subtitleText: subtitleText, + ), + ), + ], + ); + } +} diff --git a/lib/shared/widgets/asset_item/asset_item_body.dart b/lib/shared/widgets/asset_item/asset_item_body.dart new file mode 100644 index 0000000000..9666502d00 --- /dev/null +++ b/lib/shared/widgets/asset_item/asset_item_body.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_subtitle.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_title.dart'; + +/// A widget that displays an asset's title and subtitle information. +/// +/// This replaces the previous CoinItemBody component and works with AssetId instead of Coin. +class AssetItemBody extends StatelessWidget { + const AssetItemBody({ + super.key, + required this.assetId, + this.amount, + this.size = AssetItemSize.medium, + this.subtitleText, + }); + + final AssetId? assetId; + final double? amount; + final AssetItemSize size; + final String? subtitleText; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: size.verticalSpacing), + AssetItemTitle(assetId: assetId, size: size, amount: amount), + SizedBox(height: size.verticalSpacing), + AssetItemSubtitle( + assetId: assetId, + size: size, + amount: amount, + text: subtitleText, + ), + ], + ); + } +} diff --git a/lib/shared/widgets/asset_item/asset_item_size.dart b/lib/shared/widgets/asset_item/asset_item_size.dart new file mode 100644 index 0000000000..3064ff0dbe --- /dev/null +++ b/lib/shared/widgets/asset_item/asset_item_size.dart @@ -0,0 +1,45 @@ +/// Defines size configurations for an AssetItem component. +class AssetItemSize { + const AssetItemSize({ + required this.assetLogo, + required this.title, + required this.subtitle, + required this.verticalSpacing, + }); + + /// Size preset for a large asset item + static const AssetItemSize large = AssetItemSize( + assetLogo: 34.0, + title: 14.0, + subtitle: 12.0, + verticalSpacing: 6, + ); + + /// Size preset for a medium asset item + static const AssetItemSize medium = AssetItemSize( + assetLogo: 30.0, + title: 13.0, + subtitle: 11.0, + verticalSpacing: 3.0, + ); + + /// Size preset for a small asset item + static const AssetItemSize small = AssetItemSize( + assetLogo: 26.0, + title: 11.0, + subtitle: 10.0, + verticalSpacing: 3.0, + ); + + /// Size of the asset logo + final double assetLogo; + + /// Text size for the title + final double title; + + /// Text size for the subtitle + final double subtitle; + + /// Standard spacing between elements + final double verticalSpacing; +} diff --git a/lib/shared/widgets/asset_item/asset_item_subtitle.dart b/lib/shared/widgets/asset_item/asset_item_subtitle.dart new file mode 100644 index 0000000000..5acb92ab30 --- /dev/null +++ b/lib/shared/widgets/asset_item/asset_item_subtitle.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; + +/// A widget that displays an asset's subtitle, typically showing the ticker symbol. +/// +/// This replaces the previous CoinItemSubtitle component and works with AssetId instead of Coin. +class AssetItemSubtitle extends StatelessWidget { + const AssetItemSubtitle({ + super.key, + required this.assetId, + required this.size, + this.amount, + this.text, + }); + + final AssetId? assetId; + final AssetItemSize size; + final double? amount; + final String? text; + + @override + Widget build(BuildContext context) { + final String subtitleText = + text ?? (assetId != null ? assetId!.symbol.configSymbol : 'Unknown'); + + return Text( + subtitleText, + style: TextStyle( + fontSize: size.subtitle, + height: 1, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } +} diff --git a/lib/shared/widgets/asset_item/asset_item_title.dart b/lib/shared/widgets/asset_item/asset_item_title.dart new file mode 100644 index 0000000000..b4417f154b --- /dev/null +++ b/lib/shared/widgets/asset_item/asset_item_title.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; + +/// A widget that displays an asset's title, typically the asset name. +/// +/// This replaces the previous CoinItemTitle component and works with AssetId instead of Coin. +class AssetItemTitle extends StatelessWidget { + const AssetItemTitle({ + super.key, + required this.assetId, + required this.size, + this.amount, + }); + + final AssetId? assetId; + final AssetItemSize size; + final double? amount; + + @override + Widget build(BuildContext context) { + return DefaultTextStyle.merge( + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontSize: size.title, + height: 1, + ), + child: Text(assetId?.name ?? 'Unknown Asset'), + ); + } +} diff --git a/lib/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart b/lib/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart index 68d987a24b..500e5b55e7 100644 --- a/lib/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart +++ b/lib/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart @@ -23,9 +23,9 @@ class CoinSparkline extends StatelessWidget { return const SizedBox.shrink(); } else { return LimitedBox( - maxWidth: 120, + maxWidth: 130, child: SizedBox( - height: 24, + height: 35, child: SparklineChart( data: snapshot.data!, positiveLineColor: Colors.green, diff --git a/lib/views/wallet/wallet_page/common/asset_list_item.dart b/lib/views/wallet/wallet_page/common/asset_list_item.dart new file mode 100644 index 0000000000..1a5a4c9d7d --- /dev/null +++ b/lib/views/wallet/wallet_page/common/asset_list_item.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/asset_list_item_desktop.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/asset_list_item_mobile.dart'; + +/// A widget that displays an asset in a list item format with different layouts for mobile and desktop. +/// +/// This replaces the previous CoinListItem component and works with AssetId instead of Coin. +class AssetListItem extends StatelessWidget { + const AssetListItem({ + super.key, + required this.assetId, + required this.backgroundColor, + required this.onTap, + this.isActivating = false, + }); + + final AssetId assetId; + final Color backgroundColor; + final void Function(AssetId) onTap; + final bool isActivating; + + @override + Widget build(BuildContext context) { + return Opacity(opacity: isActivating ? 0.3 : 1, child: _buildItem()); + } + + Widget _buildItem() { + return isMobile + ? AssetListItemMobile( + assetId: assetId, + backgroundColor: backgroundColor, + onTap: onTap, + ) + : AssetListItemDesktop( + assetId: assetId, + backgroundColor: backgroundColor, + onTap: onTap, + ); + } +} diff --git a/lib/views/wallet/wallet_page/common/asset_list_item_desktop.dart b/lib/views/wallet/wallet_page/common/asset_list_item_desktop.dart new file mode 100644 index 0000000000..d08b1a5fd0 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/asset_list_item_desktop.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart'; + +/// A widget that displays an asset in a list item format optimized for desktop devices. +/// +/// This replaces the previous CoinListItemDesktop component and works with AssetId instead of Coin. +class AssetListItemDesktop extends StatelessWidget { + const AssetListItemDesktop({ + super.key, + required this.assetId, + required this.backgroundColor, + required this.onTap, + this.priceChangePercentage24h, + }); + + final AssetId assetId; + final Color backgroundColor; + final void Function(AssetId) onTap; + + /// The 24-hour price change percentage for the asset + final double? priceChangePercentage24h; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + clipBehavior: Clip.antiAlias, + child: Material( + color: backgroundColor, + child: InkWell( + onTap: () => onTap(assetId), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: Container( + constraints: const BoxConstraints( + maxWidth: 200, + ), + alignment: Alignment.centerLeft, + child: AssetItem( + assetId: assetId, + size: AssetItemSize.large, + ), + ), + ), + // Spacer(), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: TrendPercentageText( + percentage: priceChangePercentage24h ?? 0, + showIcon: true, + iconSize: 16, + precision: 2, + ), + ), + ), + Expanded( + flex: 2, + child: CoinSparkline(coinId: assetId.id), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/wallet/wallet_page/common/asset_list_item_mobile.dart b/lib/views/wallet/wallet_page/common/asset_list_item_mobile.dart new file mode 100644 index 0000000000..2981e12cb2 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/asset_list_item_mobile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; + +/// A widget that displays an asset in a list item format optimized for mobile devices. +/// +/// This replaces the previous CoinListItemMobile component and works with AssetId instead of Coin. +class AssetListItemMobile extends StatelessWidget { + const AssetListItemMobile({ + super.key, + required this.assetId, + required this.backgroundColor, + required this.onTap, + }); + + final AssetId assetId; + final Color backgroundColor; + final void Function(AssetId) onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onTap(assetId), + child: Container( + color: backgroundColor, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Row( + children: [ + Expanded( + child: AssetItem( + assetId: assetId, + size: AssetItemSize.medium, + ), + ), + const Icon(Icons.chevron_right), + ], + ), + ), + ); + } +} diff --git a/lib/views/wallet/wallet_page/common/assets_list.dart b/lib/views/wallet/wallet_page/common/assets_list.dart new file mode 100644 index 0000000000..6290cc4414 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/assets_list.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/asset_list_item.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart'; + +/// A widget that displays a list of assets. +/// +/// This replaces the previous AllCoinsList component and works with AssetId instead of Coin. +class AssetsList extends StatelessWidget { + const AssetsList({ + super.key, + required this.assets, + required this.onAssetItemTap, + this.withBalance = false, + this.searchPhrase = '', + this.useGroupedView = false, + this.priceChangePercentages = const {}, + }); + + final List assets; + final Function(AssetId) onAssetItemTap; + final bool withBalance; + final String searchPhrase; + final bool useGroupedView; + final Map priceChangePercentages; + + @override + Widget build(BuildContext context) { + if (useGroupedView) { + return _buildGroupedView(context); + } + return _buildFlatView(context); + } + + Widget _buildFlatView(BuildContext context) { + final filteredAssets = _filterAssets(); + + return SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final asset = filteredAssets[index]; + final Color backgroundColor = index.isEven + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.onSurface; + + return AssetListItem( + assetId: asset, + backgroundColor: backgroundColor, + onTap: onAssetItemTap, + ); + }, + childCount: filteredAssets.length, + ), + ); + } + + Widget _buildGroupedView(BuildContext context) { + final groupedAssets = _groupAssetsByTicker(); + final groups = groupedAssets.entries.toList(); + + return SliverList.separated( + itemBuilder: (BuildContext context, int index) { + final group = groups[index]; + final Color backgroundColor = index.isEven + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.onSurface; + + return GroupedAssetTickerItem( + assets: group.value, + backgroundColor: backgroundColor, + onTap: onAssetItemTap, + // priceChangePercentage24h: priceChangePercentages[primaryAsset.id], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 4); + }, + itemCount: groups.length, + ); + } + + /// Groups assets by their ticker symbol + Map> _groupAssetsByTicker() { + final filteredAssets = _filterAssets(); + final groupedAssets = >{}; + + for (final asset in filteredAssets) { + final symbol = asset.symbol.configSymbol; + groupedAssets.putIfAbsent(symbol, () => []).add(asset); + } + + return Map.fromEntries( + groupedAssets.entries.toList()..sort((a, b) => a.key.compareTo(b.key)), + ); + } + + /// Filters assets based on search phrase + List _filterAssets() { + if (searchPhrase.isEmpty) { + return assets; + } + + return assets.where((asset) { + final name = asset.name.toLowerCase(); + final symbol = asset.symbol.configSymbol.toLowerCase(); + final id = asset.id.toLowerCase(); + final searchLower = searchPhrase.toLowerCase(); + + return name.contains(searchLower) || + symbol.contains(searchLower) || + id.contains(searchLower); + }).toList(); + } +} diff --git a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart index 393679caeb..738ce7aff3 100644 --- a/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart +++ b/lib/views/wallet/wallet_page/common/coin_list_item_desktop.dart @@ -7,8 +7,6 @@ import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/ui/ui_simple_border_button.dart'; -import 'package:web_dex/shared/widgets/coin_balance.dart'; -import 'package:web_dex/shared/widgets/coin_fiat_change.dart'; import 'package:web_dex/shared/widgets/coin_fiat_price.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; @@ -17,11 +15,11 @@ import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/coin_ class CoinListItemDesktop extends StatelessWidget { const CoinListItemDesktop({ - Key? key, + super.key, required this.coin, required this.backgroundColor, required this.onTap, - }) : super(key: key); + }); final Coin coin; final Color backgroundColor; @@ -36,7 +34,7 @@ class CoinListItemDesktop extends StatelessWidget { color: backgroundColor, ), child: Material( - type: MaterialType.transparency, + // type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(10), hoverColor: theme.custom.zebraHoverColor, @@ -44,7 +42,7 @@ class CoinListItemDesktop extends StatelessWidget { child: Container( padding: const EdgeInsets.fromLTRB(0, 10, 16, 10), child: Row( - key: Key('active-coin-item-${(coin.abbr).toLowerCase()}'), + key: Key('coin-item-${(coin.abbr).toLowerCase()}'), children: [ Expanded( flex: 5, @@ -73,28 +71,6 @@ class CoinListItemDesktop extends StatelessWidget { ], ), ), - Expanded( - flex: 5, - child: coin.isSuspended - ? _SuspendedMessage( - key: Key('suspended-asset-message-${coin.abbr}'), - coin: coin, - isReEnabling: coin.isActivating, - ) - : CoinBalance( - key: Key('balance-asset-${coin.abbr}'), - coin: coin, - ), - ), - Expanded( - flex: 2, - child: coin.isSuspended - ? const SizedBox.shrink() - : CoinFiatChange( - coin, - style: const TextStyle(fontSize: _fontSize), - ), - ), Expanded( flex: 2, child: coin.isSuspended @@ -106,8 +82,7 @@ class CoinListItemDesktop extends StatelessWidget { ), Expanded( flex: 2, - child: - CoinSparkline(coinId: coin.abbr), // Using CoinSparkline + child: CoinSparkline(coinId: coin.abbr), ), ], ), @@ -120,7 +95,6 @@ class CoinListItemDesktop extends StatelessWidget { class _SuspendedMessage extends StatelessWidget { const _SuspendedMessage({ - super.key, required this.coin, required this.isReEnabling, }); diff --git a/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart b/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart new file mode 100644 index 0000000000..d968363c45 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart @@ -0,0 +1,269 @@ +import 'dart:math' show min; + +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/komodo_ui.dart'; +import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; +import 'package:web_dex/common/screen.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item.dart'; +import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; +import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart'; + +class _ExpandedView extends StatelessWidget { + const _ExpandedView({ + required this.assets, + required this.theme, + }); + + final List assets; + final ThemeData theme; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), + child: Text( + 'Available on Networks:', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.secondary, + ), + ), + ), + _AssetIconsRow(assets: assets), + ], + ), + ); + } +} + +class _AssetIconsRow extends StatelessWidget { + const _AssetIconsRow({ + required this.assets, + }); + + final List assets; + + @override + Widget build(BuildContext context) { + final relatedAssets = assets.length > 1 ? assets.toList() : assets; + + return Wrap( + spacing: 12.0, + runSpacing: 12.0, + children: relatedAssets.map((asset) { + return _AssetIconItem(asset: asset); + }).toList(), + ); + } +} + +class _AssetIconItem extends StatelessWidget { + const _AssetIconItem({ + required this.asset, + }); + + final AssetId asset; + + @override + Widget build(BuildContext context) { + final size = isMobile ? 50.0 : 70.0; + final theme = Theme.of(context); + + return Tooltip( + message: asset.id, + child: InkWell( + onTap: null, + borderRadius: BorderRadius.circular(8.0), + child: Container( + height: size, + constraints: BoxConstraints( + minWidth: size, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surface.withOpacity(0.3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AssetIcon.ofTicker( + asset.subClass.iconTicker, + size: min(36, size * 0.6), + ), + const SizedBox(height: 2), + Text( + asset.subClass.formatted, + style: theme.textTheme.labelSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + ); + } +} + +/// A widget that displays a group of assets sharing the same ticker symbol. +/// +/// Shows the same layout as AssetListItemDesktop but adds an expansion button +/// to show related assets that share the same ticker symbol. +class GroupedAssetTickerItem extends StatefulWidget { + const GroupedAssetTickerItem({ + super.key, + required this.assets, + required this.backgroundColor, + required this.onTap, + this.initiallyExpanded = false, + this.isActivating = false, + }); + + final List assets; + final Color backgroundColor; + final void Function(AssetId)? onTap; + final bool initiallyExpanded; + final bool isActivating; + + @override + State createState() => _GroupedAssetTickerItemState(); +} + +class _GroupedAssetTickerItemState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + void _toggleExpanded() { + setState(() { + _isExpanded = !_isExpanded; + }); + } + + AssetId get _primaryAsset => widget.assets.first; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Opacity( + opacity: widget.isActivating ? 0.3 : 1, + child: Material( + color: widget.backgroundColor, + borderRadius: BorderRadius.circular(10), + clipBehavior: Clip.hardEdge, + type: MaterialType.card, + child: InkWell( + onTap: + widget.onTap == null ? null : () => widget.onTap!(_primaryAsset), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 16, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 3, + child: AssetItem( + assetId: _primaryAsset, + size: AssetItemSize.large, + ), + ), + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + final price = state.getPriceForAsset(_primaryAsset); + final formattedPrice = price?.price != null + ? NumberFormat.currency( + symbol: '\$', + decimalDigits: 2, + ).format(price!.price) + : ''; + return Text( + formattedPrice, + style: theme.textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ); + }, + ), + ), + Expanded( + flex: 2, + child: BlocBuilder( + builder: (context, state) { + return TrendPercentageText( + textStyle: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + percentage: + state.get24hChangeForAsset(_primaryAsset) ?? 0, + showIcon: true, + iconSize: 16, + precision: 2, + ); + }, + ), + ), + Expanded( + flex: 2, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 130, + maxHeight: 35, + ), + child: CoinSparkline(coinId: _primaryAsset.id), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 48), + child: !(widget.assets.length > 1) + ? null + : IconButton( + iconSize: 32, + icon: Icon( + _isExpanded + ? Icons.expand_less + : Icons.expand_more, + color: theme.colorScheme.secondary, + ), + onPressed: _toggleExpanded, + tooltip: _isExpanded + ? 'Hide related assets' + : 'Show related assets', + ), + ), + ], + ), + ), + if (_isExpanded) + _ExpandedView(assets: widget.assets, theme: theme), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/wallet/wallet_page/common/grouped_assets_list.dart b/lib/views/wallet/wallet_page/common/grouped_assets_list.dart new file mode 100644 index 0000000000..6732d99996 --- /dev/null +++ b/lib/views/wallet/wallet_page/common/grouped_assets_list.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart'; + +/// A widget that displays a list of assets grouped by their ticker symbols. +/// +/// This is an alternative to the [AssetsList] component that groups assets +/// with the same ticker symbol together and allows expanding to see all +/// related assets. +class GroupedAssetsList extends StatelessWidget { + const GroupedAssetsList({ + super.key, + required this.assets, + required this.onAssetItemTap, + this.searchPhrase = '', + }); + + /// The complete list of assets to display + final List assets; + + /// Callback function when an asset is tapped + final Function(AssetId) onAssetItemTap; + + /// Optional search phrase to filter assets + final String searchPhrase; + + @override + Widget build(BuildContext context) { + final groupedAssets = _groupAssetsByTicker(); + + return SliverList.separated( + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 8); + }, + itemBuilder: (BuildContext context, int index) { + final ticker = groupedAssets.keys.elementAt(index); + final assetGroup = groupedAssets[ticker]!; + + final Color backgroundColor = index.isEven + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.onSurface; + + return GroupedAssetTickerItem( + assets: assetGroup, + backgroundColor: backgroundColor, + onTap: onAssetItemTap, + initiallyExpanded: false, + ); + }, + itemCount: groupedAssets.length, + ); + } + + /// Groups assets by their ticker symbol + Map> _groupAssetsByTicker() { + final filteredAssets = _filterAssets(); + final groupedAssets = >{}; + + for (final asset in filteredAssets) { + final ticker = asset.symbol.configSymbol; + groupedAssets.putIfAbsent(ticker, () => []).add(asset); + } + + return groupedAssets; + } + + /// Filters assets based on search phrase + List _filterAssets() { + if (searchPhrase.isEmpty) { + return assets; + } + + return assets.where((asset) { + final name = asset.name.toLowerCase(); + final symbol = asset.symbol.configSymbol.toLowerCase(); + final searchLower = searchPhrase.toLowerCase(); + + return name.contains(searchLower) || symbol.contains(searchLower); + }).toList(); + } +} diff --git a/lib/views/wallet/wallet_page/common/wallet_coins_list.dart b/lib/views/wallet/wallet_page/common/wallet_coins_list.dart index 51fe67c738..b01780b633 100644 --- a/lib/views/wallet/wallet_page/common/wallet_coins_list.dart +++ b/lib/views/wallet/wallet_page/common/wallet_coins_list.dart @@ -1,36 +1,35 @@ import 'package:flutter/material.dart'; -import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/views/wallet/wallet_page/common/coin_list_item.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/asset_list_item.dart'; -class WalletCoinsList extends StatelessWidget { - const WalletCoinsList({ - Key? key, - required this.coins, - required this.onCoinItemTap, - }) : super(key: key); +class KnownAssetsList extends StatelessWidget { + const KnownAssetsList({ + super.key, + required this.assets, + required this.onAssetItemTap, + }); - final List coins; - final Function(Coin) onCoinItemTap; + final List assets; + final void Function(AssetId) onAssetItemTap; @override Widget build(BuildContext context) { - return SliverList( - key: const Key('wallet-page-coins-list'), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final Coin coin = coins[index]; - final bool isEven = (index + 1) % 2 == 0; - final Color backgroundColor = isEven - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.onSurface; - return CoinListItem( - key: Key('wallet-coin-list-item-${coin.abbr.toLowerCase()}'), - coin: coin, - backgroundColor: backgroundColor, - onTap: onCoinItemTap, - ); - }, - childCount: coins.length, - )); + return SliverList.separated( + key: const Key('wallet-page-coins-list'), + itemBuilder: (BuildContext context, int index) { + final asset = assets[index]; + final Color backgroundColor = index.isEven + ? Theme.of(context).colorScheme.surface + : Theme.of(context).colorScheme.onSurface; + return AssetListItem( + assetId: asset, + backgroundColor: backgroundColor, + onTap: (assetId) => onAssetItemTap(assetId)); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 8); + }, + itemCount: assets.length, + ); } } diff --git a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart index df8a1079cd..01ddd3d95a 100644 --- a/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart +++ b/lib/views/wallet/wallet_page/wallet_main/all_coins_list.dart @@ -9,14 +9,14 @@ import 'package:web_dex/views/wallet/wallet_page/common/wallet_coins_list.dart'; class AllCoinsList extends StatefulWidget { const AllCoinsList({ - Key? key, + super.key, required this.searchPhrase, required this.withBalance, - required this.onCoinItemTap, - }) : super(key: key); + required this.onCoinSelected, + }); final String searchPhrase; final bool withBalance; - final Function(Coin) onCoinItemTap; + final Function(Coin) onCoinSelected; @override _AllCoinsListState createState() => _AllCoinsListState(); @@ -74,9 +74,13 @@ class _AllCoinsListState extends State { ), ), ) - : WalletCoinsList( - coins: displayedCoins, - onCoinItemTap: widget.onCoinItemTap, + : KnownAssetsList( + assets: displayedCoins.map((c) => c.id).toList(), + onAssetItemTap: (id) { + final coin = + displayedCoins.firstWhere((coin) => coin.id == id); + widget.onCoinSelected(coin); + }, ); }, ); diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart index 9143536053..85395b6d32 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -30,8 +30,8 @@ import 'package:web_dex/views/common/pages/page_layout.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/animated_portfolio_charts.dart'; import 'package:web_dex/views/wallet/wallet_page/charts/coin_prices_chart.dart'; +import 'package:web_dex/views/wallet/wallet_page/common/assets_list.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/active_coins_list.dart'; -import 'package:web_dex/views/wallet/wallet_page/wallet_main/all_coins_list.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart'; import 'package:web_dex/views/wallet/wallet_page/wallet_main/wallet_overview.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; @@ -139,6 +139,9 @@ class _WalletMainState extends State mode: authStateMode, ), ), + SliverToBoxAdapter( + child: SizedBox(height: 8), + ), _buildCoinList(authStateMode), ], ), @@ -219,10 +222,25 @@ class _WalletMainState extends State ); case AuthorizeMode.hiddenLogin: case AuthorizeMode.noLogin: - return AllCoinsList( + return AssetsList( + useGroupedView: true, + assets: context + .read() + .state + .coins + .values + .map((coin) => coin.assetId) + .toList(), + withBalance: false, searchPhrase: _searchKey, - withBalance: _showCoinWithBalance, - onCoinItemTap: _onCoinItemTap, + onAssetItemTap: (assetId) => _onAssetItemTap( + context + .read() + .state + .coins + .values + .firstWhere((coin) => coin.assetId == assetId), + ), ); } } @@ -249,6 +267,11 @@ class _WalletMainState extends State _popupDispatcher!.show(); } + void _onAssetItemTap(Coin coin) { + _popupDispatcher = _createPopupDispatcher(); + _popupDispatcher!.show(); + } + PopupDispatcher _createPopupDispatcher() { final TakerBloc takerBloc = context.read(); final BridgeBloc bridgeBloc = context.read(); diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f793cd6771..7b628da70f 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -209,7 +209,7 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - komodo_defi_framework: f0b88d0dfa7907a9ece425bf4f1435cd94cfdc73 + komodo_defi_framework: c01710e1bf0eaa1fad46b3e4ba596f24dba394fd local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 mobile_scanner: 07710d6b9b2c220ae899de2d7ecf5d77ffa56333 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 diff --git a/packages/komodo_persistence_layer/pubspec.yaml b/packages/komodo_persistence_layer/pubspec.yaml index 9b0136644a..63fc0d8536 100644 --- a/packages/komodo_persistence_layer/pubspec.yaml +++ b/packages/komodo_persistence_layer/pubspec.yaml @@ -9,11 +9,7 @@ environment: # Add regular dependencies here. dependencies: # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - hive: - git: - url: https://github.com/KomodoPlatform/hive.git - path: hive/ - ref: 470473ffc1ba39f6c90f31ababe0ee63b76b69fe #2.2.3 + hive: 2.2.3 dev_dependencies: lints: ^5.1.1 diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index e707312e51..ccc527512c 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -95,7 +95,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "1d88178643b01448a887f3da1bb5f8d02bce5da3" + resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -104,7 +104,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "1d88178643b01448a887f3da1bb5f8d02bce5da3" + resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -113,7 +113,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "1d88178643b01448a887f3da1bb5f8d02bce5da3" + resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" diff --git a/pubspec.lock b/pubspec.lock index e8d259dcf9..6e390881fe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -36,18 +36,18 @@ packages: dependency: "direct main" description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "068190d6c99c436287936ba5855af2e1fa78d8083ae65b4db6a281780da727ae" + sha256: e02d018628c870ef2d7f03e33f9ad179d89ff6ec52ca6c56bcb80bcef979867f url: "https://pub.dev" source: hosted - version: "1.6.0" + version: "1.6.2" async: dependency: transitive description: @@ -252,10 +252,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b" + sha256: "6a8720a68b0e048d97b328d853bd0443e2e9cbf49754dd19b36f8347fd1eea7f" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "9.2.2" file_system_access_api: dependency: transitive description: @@ -553,20 +553,18 @@ packages: hive: dependency: "direct main" description: - path: hive - ref: "470473ffc1ba39f6c90f31ababe0ee63b76b69fe" - resolved-ref: "470473ffc1ba39f6c90f31ababe0ee63b76b69fe" - url: "https://github.com/KomodoPlatform/hive.git" - source: git + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted version: "2.2.3" hive_flutter: dependency: "direct main" description: - path: hive_flutter - ref: "0cbaab793be77b19b4740bc85d7ea6461b9762b4" - resolved-ref: "0cbaab793be77b19b4740bc85d7ea6461b9762b4" - url: "https://github.com/KomodoPlatform/hive.git" - source: git + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted version: "1.1.0" html: dependency: transitive @@ -642,7 +640,7 @@ packages: description: path: "packages/komodo_cex_market_data" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -651,7 +649,7 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -660,7 +658,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0" @@ -669,7 +667,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -678,7 +676,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -687,7 +685,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -696,7 +694,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -712,7 +710,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -728,7 +726,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "2d41133d7dca37b0b544eacb859e0c617f142e98" + resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -1024,10 +1022,10 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" pub_semver: dependency: transitive description: @@ -1080,10 +1078,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_android: dependency: transitive description: @@ -1405,10 +1403,10 @@ packages: dependency: "direct main" description: name: video_player - sha256: "48941c8b05732f9582116b1c01850b74dbee1d8520cd7e34ad4609d6df666845" + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" url: "https://pub.dev" source: hosted - version: "2.9.3" + version: "2.9.5" video_player_android: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d8f93af91..acc0cfd66f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,8 @@ dependencies: dragon_logs: 1.1.0 ## ---- Dart.dev, Flutter.dev - args: 2.6.0 # dart.dev + args: ^2.7.0 # dart.dev + flutter_markdown: 0.7.6+2 # flutter.dev http: 1.3.0 # dart.dev intl: 0.20.2 # dart.dev @@ -64,7 +65,7 @@ dependencies: url_launcher: 6.3.1 # flutter.dev crypto: 3.0.6 # dart.dev cross_file: 0.3.4+2 # flutter.dev - video_player: 2.9.3 # flutter.dev + video_player: ^2.9.5 # flutter.dev logging: 1.3.0 ## ---- google.com @@ -110,18 +111,10 @@ dependencies: universal_html: 2.2.4 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - hive: - git: - url: https://github.com/KomodoPlatform/hive.git - path: hive/ - ref: 470473ffc1ba39f6c90f31ababe0ee63b76b69fe #2.2.3 + hive: 2.2.3 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 - hive_flutter: - git: - url: https://github.com/KomodoPlatform/hive.git - path: hive_flutter/ - ref: 0cbaab793be77b19b4740bc85d7ea6461b9762b4 #1.1.0 + hive_flutter: 1.1.0 # Approved via https://github.com/KomodoPlatform/komodo-wallet/pull/1106 (Outdated) badges: 3.1.2 @@ -142,7 +135,7 @@ dependencies: # TODO: review required - SDK integration path_provider: 2.1.5 # flutter.dev - shared_preferences: 2.5.2 # flutter.dev + shared_preferences: ^2.5.3 # flutter.dev decimal: 3.2.1 # transitive dependency that is required to fix breaking changes in rational package rational: 2.2.3 # sdk depends on decimal ^3.0.2, which depends on rational ^2.0.0 uuid: 4.5.1 # sdk depends on this version From 77b2bdcf090b137f6f6a2c9ad611ff3844c5f018 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:42:54 +0100 Subject: [PATCH 02/13] fix: revert changed Firebase site ID --- firebase.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase.json b/firebase.json index bef402c83b..3c3e731ab5 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,6 @@ { "hosting": { - "site": "wallet-rc-device-preview", + "site": "walletrc", "public": "build/web", "ignore": [ "firebase.json", From c79f8348dcbe0ba695488489f19f2c5bce2db500 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:45:14 +0100 Subject: [PATCH 03/13] fix(ui): Fix expanded state issues in assets list when searching --- .../wallet_page/common/assets_list.dart | 2 +- .../common/grouped_asset_ticker_item.dart | 238 +++++++++--------- .../common/grouped_assets_list.dart | 2 +- 3 files changed, 126 insertions(+), 116 deletions(-) diff --git a/lib/views/wallet/wallet_page/common/assets_list.dart b/lib/views/wallet/wallet_page/common/assets_list.dart index 6290cc4414..3d6b394285 100644 --- a/lib/views/wallet/wallet_page/common/assets_list.dart +++ b/lib/views/wallet/wallet_page/common/assets_list.dart @@ -66,10 +66,10 @@ class AssetsList extends StatelessWidget { : Theme.of(context).colorScheme.onSurface; return GroupedAssetTickerItem( + key: Key(group.key), assets: group.value, backgroundColor: backgroundColor, onTap: onAssetItemTap, - // priceChangePercentage24h: priceChangePercentages[primaryAsset.id], ); }, separatorBuilder: (BuildContext context, int index) { diff --git a/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart b/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart index d968363c45..5b88a60d2c 100644 --- a/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart +++ b/lib/views/wallet/wallet_page/common/grouped_asset_ticker_item.dart @@ -10,116 +10,6 @@ import 'package:web_dex/shared/widgets/asset_item/asset_item.dart'; import 'package:web_dex/shared/widgets/asset_item/asset_item_size.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/charts/coin_sparkline.dart'; -class _ExpandedView extends StatelessWidget { - const _ExpandedView({ - required this.assets, - required this.theme, - }); - - final List assets; - final ThemeData theme; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.only( - left: 16.0, - right: 16.0, - bottom: 16.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Divider(), - Padding( - padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), - child: Text( - 'Available on Networks:', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.secondary, - ), - ), - ), - _AssetIconsRow(assets: assets), - ], - ), - ); - } -} - -class _AssetIconsRow extends StatelessWidget { - const _AssetIconsRow({ - required this.assets, - }); - - final List assets; - - @override - Widget build(BuildContext context) { - final relatedAssets = assets.length > 1 ? assets.toList() : assets; - - return Wrap( - spacing: 12.0, - runSpacing: 12.0, - children: relatedAssets.map((asset) { - return _AssetIconItem(asset: asset); - }).toList(), - ); - } -} - -class _AssetIconItem extends StatelessWidget { - const _AssetIconItem({ - required this.asset, - }); - - final AssetId asset; - - @override - Widget build(BuildContext context) { - final size = isMobile ? 50.0 : 70.0; - final theme = Theme.of(context); - - return Tooltip( - message: asset.id, - child: InkWell( - onTap: null, - borderRadius: BorderRadius.circular(8.0), - child: Container( - height: size, - constraints: BoxConstraints( - minWidth: size, - ), - decoration: BoxDecoration( - color: theme.colorScheme.surface.withOpacity(0.3), - borderRadius: BorderRadius.circular(8.0), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AssetIcon.ofTicker( - asset.subClass.iconTicker, - size: min(36, size * 0.6), - ), - const SizedBox(height: 2), - Text( - asset.subClass.formatted, - style: theme.textTheme.labelSmall, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ), - ), - ); - } -} - /// A widget that displays a group of assets sharing the same ticker symbol. /// /// Shows the same layout as AssetListItemDesktop but adds an expansion button @@ -130,14 +20,14 @@ class GroupedAssetTickerItem extends StatefulWidget { required this.assets, required this.backgroundColor, required this.onTap, - this.initiallyExpanded = false, + this.expanded, this.isActivating = false, }); final List assets; final Color backgroundColor; final void Function(AssetId)? onTap; - final bool initiallyExpanded; + final bool? expanded; final bool isActivating; @override @@ -150,7 +40,17 @@ class _GroupedAssetTickerItemState extends State { @override void initState() { super.initState(); - _isExpanded = widget.initiallyExpanded; + _isExpanded = widget.expanded ?? false; + } + + @override + void didUpdateWidget(GroupedAssetTickerItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.expanded != null && widget.expanded != oldWidget.expanded) { + setState(() { + _isExpanded = widget.expanded!; + }); + } } void _toggleExpanded() { @@ -163,7 +63,7 @@ class _GroupedAssetTickerItemState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final theme = Theme.of(context); return Opacity( opacity: widget.isActivating ? 0.3 : 1, child: Material( @@ -267,3 +167,113 @@ class _GroupedAssetTickerItemState extends State { ); } } + +class _ExpandedView extends StatelessWidget { + const _ExpandedView({ + required this.assets, + required this.theme, + }); + + final List assets; + final ThemeData theme; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 16.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), + child: Text( + 'Available on Networks:', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.secondary, + ), + ), + ), + _AssetIconsRow(assets: assets), + ], + ), + ); + } +} + +class _AssetIconsRow extends StatelessWidget { + const _AssetIconsRow({ + required this.assets, + }); + + final List assets; + + @override + Widget build(BuildContext context) { + final relatedAssets = assets.length > 1 ? assets.toList() : assets; + + return Wrap( + spacing: 12.0, + runSpacing: 12.0, + children: relatedAssets.map((asset) { + return _AssetIconItem(asset: asset); + }).toList(), + ); + } +} + +class _AssetIconItem extends StatelessWidget { + const _AssetIconItem({ + required this.asset, + }); + + final AssetId asset; + + @override + Widget build(BuildContext context) { + final size = isMobile ? 50.0 : 70.0; + final theme = Theme.of(context); + + return Tooltip( + message: asset.id, + child: InkWell( + onTap: null, + borderRadius: BorderRadius.circular(8.0), + child: Container( + height: size, + constraints: BoxConstraints( + minWidth: size, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surface.withOpacity(0.3), + borderRadius: BorderRadius.circular(8.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AssetIcon.ofTicker( + asset.subClass.iconTicker, + size: min(36, size * 0.6), + ), + const SizedBox(height: 2), + Text( + asset.subClass.formatted, + style: theme.textTheme.labelSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/wallet/wallet_page/common/grouped_assets_list.dart b/lib/views/wallet/wallet_page/common/grouped_assets_list.dart index 6732d99996..ba2de7b140 100644 --- a/lib/views/wallet/wallet_page/common/grouped_assets_list.dart +++ b/lib/views/wallet/wallet_page/common/grouped_assets_list.dart @@ -41,10 +41,10 @@ class GroupedAssetsList extends StatelessWidget { : Theme.of(context).colorScheme.onSurface; return GroupedAssetTickerItem( + key: Key(ticker), assets: assetGroup, backgroundColor: backgroundColor, onTap: onAssetItemTap, - initiallyExpanded: false, ); }, itemCount: groupedAssets.length, From 7238db8c227fc294db53b4cb85f78de7c6bd0797 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:53:44 +0100 Subject: [PATCH 04/13] Feat: Coin message signing --- assets/translations/en.json | 19 +- docs/BLOC_NAMING_CONVENTIONS.md | 82 ++ lib/bloc/auth_bloc/auth_bloc.dart | 32 +- lib/generated/codegen_loader.g.dart | 15 + lib/shared/ui/ui_primary_button.dart | 162 +-- .../connect_wallet/connect_wallet_button.dart | 1 + lib/shared/widgets/disclaimer/disclaimer.dart | 2 +- lib/shared/widgets/disclaimer/eula.dart | 2 +- .../launch_native_explorer_button.dart | 2 +- .../bitrefill/bitrefill_button_view.dart | 2 +- ...itrefill_transaction_completed_dialog.dart | 2 +- .../trezor_steps/trezor_dialog_error.dart | 2 +- lib/views/common/page_header/page_header.dart | 2 +- .../mobile/dex_list_header_mobile.dart | 2 +- .../common/widgets/nft_connect_wallet.dart | 2 +- .../backup_seed_notification.dart | 2 +- .../wallet/coin_details/coin_details.dart | 6 + .../coin_details_common_buttons.dart | 87 +- .../wallet/coin_details/coin_page_type.dart | 2 +- .../coin_details/faucet/faucet_button.dart | 2 +- .../message_signing_screen.dart | 952 ++++++++++++++++++ .../rewards/kmd_reward_claim_success.dart | 2 +- .../withdraw_form/pages/failed_page.dart | 194 ---- .../buttons/convert_address_button.dart | 2 +- .../send_complete_form_buttons.dart | 2 +- .../send_confirm_buttons.dart | 2 +- .../widgets/wallet_list_item.dart | 2 +- .../wallets_manager/widgets/wallet_login.dart | 18 +- .../widgets/wallets_manager_controls.dart | 2 +- packages/komodo_ui_kit/pubspec.lock | 24 +- packages/komodo_ui_kit/pubspec.yaml | 20 +- pubspec.lock | 72 +- pubspec.yaml | 40 +- 33 files changed, 1270 insertions(+), 490 deletions(-) create mode 100644 docs/BLOC_NAMING_CONVENTIONS.md create mode 100644 lib/views/wallet/coin_details/message_signing/message_signing_screen.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index cfb76d96fc..ce2b8d8477 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -74,7 +74,7 @@ "create": "Create", "import": "Import", "enterDataToSend": "Enter data to send", - "address": "Address: ", + "address": "Address", "request": "Request", "disable": "Disable", "usdPrice": "USD Price", @@ -85,6 +85,17 @@ "transactions": "Transactions", "send": "Send", "receive": "Receive", + "message": "Message", + "signMessage": "Sign Message", + "selectedAddress": "Selected Address", + "selectAddress": "Select Address", + "messageToSign": "Message to Sign", + "enterMessage": "Enter message", + "signMessageButton": "Sign Message", + "signedMessage": "Signed Message", + "pleaseSelectAddress": "Please select an address first", + "pleaseEnterMessage": "Please enter a message to sign", + "failedToSignMessage": "Failed to sign message: {}", "faucet": "Faucet", "reward": "Reward", "loadingSwap": "Loading Swaps...", @@ -447,6 +458,7 @@ "unknown": "Unknown", "unableToActiveCoin": "Unable to activate {}", "feedback": "Feedback", + "failedToLoadAddresses": "Failed to load addresses: {}", "feedbackViewTitle": "Send us your feedback", "feedbackPageDescription": "Help us improve by sharing your suggestions, reporting bugs, or giving general feedback.", "sendFeedbackButton": "Share your feedback", @@ -472,6 +484,7 @@ "cancelOrder": "Cancel Order", "version": "version", "copyToClipboard": "Copy to clipboard", + "copyAllDetails": "Copy all details", "createdAt": "Created at", "coin": "Coin", "token": "Token", @@ -636,5 +649,7 @@ "allTimeInvestment": "All-time Investment", "allTimeProfit": "All-time Profit", "profitAndLoss": "Profit & Loss", - "searchAddresses": "Search addresses" + "searchAddresses": "Search addresses", + "userNotFoundError": "User not found", + "loginFailedError": "Login failed" } \ No newline at end of file diff --git a/docs/BLOC_NAMING_CONVENTIONS.md b/docs/BLOC_NAMING_CONVENTIONS.md new file mode 100644 index 0000000000..e6443a9c42 --- /dev/null +++ b/docs/BLOC_NAMING_CONVENTIONS.md @@ -0,0 +1,82 @@ +--- +title: Naming Conventions +description: Overview of the recommended naming conventions when using bloc. +--- + +import EventExamplesGood1 from '~/components/naming-conventions/EventExamplesGood1Snippet.astro'; +import EventExamplesBad1 from '~/components/naming-conventions/EventExamplesBad1Snippet.astro'; +import StateExamplesGood1Snippet from '~/components/naming-conventions/StateExamplesGood1Snippet.astro'; +import SingleStateExamplesGood1Snippet from '~/components/naming-conventions/SingleStateExamplesGood1Snippet.astro'; +import StateExamplesBad1Snippet from '~/components/naming-conventions/StateExamplesBad1Snippet.astro'; + +The following naming conventions are simply recommendations and are completely optional. Feel free to use whatever naming conventions you prefer. You may find some of the examples/documentation do not follow the naming conventions mainly for simplicity/conciseness. These conventions are strongly recommended for large projects with multiple developers. + +## Event Conventions + +Events should be named in the **past tense** because events are things that have already occurred from the bloc's perspective. + +### Anatomy + +`BlocSubject` + `Noun (optional)` + `Verb (event)` + +Initial load events should follow the convention: `BlocSubject` + `Started` + +:::note +The base event class should be name: `BlocSubject` + `Event`. +::: + +### Examples + +✅ **Good** + + + +❌ **Bad** + + + +## State Conventions + +States should be nouns because a state is just a snapshot at a particular point in time. There are two common ways to represent state: using subclasses or using a single class. + +### Anatomy + +#### Subclasses + +`BlocSubject` + `Verb (action)` + `State` + +When representing the state as multiple subclasses `State` should be one of the following: + +`Initial` | `Success` | `Failure` | `InProgress` + +:::note +Initial states should follow the convention: `BlocSubject` + `Initial`. +::: + +#### Single Class + +`BlocSubject` + `State` + +When representing the state as a single base class an enum named `BlocSubject` + `Status` should be used to represent the status of the state: + +`initial` | `success` | `failure` | `loading`. + +:::note +The base state class should always be named: `BlocSubject` + `State`. +::: + +### Examples + +✅ **Good** + +##### Subclasses + + + +##### Single Class + + + +❌ **Bad** + + diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 6998adf3cd..227934ba28 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -70,18 +70,28 @@ class AuthBloc extends Bloc { _log.info('login from a wallet'); emit(AuthBlocState.loading()); - await _kdfSdk.auth.signIn( - walletName: event.wallet.name, - password: event.password, - options: AuthOptions( - derivationMethod: event.wallet.config.type == WalletType.hdwallet - ? DerivationMethod.hdWallet - : DerivationMethod.iguana, - ), - ); + try { + await _kdfSdk.auth.signIn( + walletName: event.wallet.name, + password: event.password, + options: AuthOptions( + derivationMethod: event.wallet.config.type == WalletType.hdwallet + ? DerivationMethod.hdWallet + : DerivationMethod.iguana, + ), + ); + } catch (e) { + // Handle specific SDK authentication errors + if (e.toString().contains('Invalid password')) { + return emit(AuthBlocState.error('invalid_password')); + } + // For other SDK authentication errors, pass through the specific error + return emit(AuthBlocState.error(e.toString())); + } + final KdfUser? currentUser = await _kdfSdk.auth.currentUser; if (currentUser == null) { - return emit(AuthBlocState.error('Failed to login')); + return emit(AuthBlocState.error('user_not_found')); } _log.info('logged in from a wallet'); @@ -90,7 +100,7 @@ class AuthBloc extends Bloc { } catch (e, s) { final error = 'Failed to login wallet ${event.wallet.name}'; _log.shout(error, e, s); - emit(AuthBlocState.error(error)); + emit(AuthBlocState.error('login_failed')); await _authChangesSubscription?.cancel(); } } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 00d1903d7b..af3e61fb13 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -89,6 +89,17 @@ abstract class LocaleKeys { static const transactions = 'transactions'; static const send = 'send'; static const receive = 'receive'; + static const message = 'message'; + static const signMessage = 'signMessage'; + static const selectedAddress = 'selectedAddress'; + static const selectAddress = 'selectAddress'; + static const messageToSign = 'messageToSign'; + static const enterMessage = 'enterMessage'; + static const signMessageButton = 'signMessageButton'; + static const signedMessage = 'signedMessage'; + static const pleaseSelectAddress = 'pleaseSelectAddress'; + static const pleaseEnterMessage = 'pleaseEnterMessage'; + static const failedToSignMessage = 'failedToSignMessage'; static const faucet = 'faucet'; static const reward = 'reward'; static const loadingSwap = 'loadingSwap'; @@ -444,6 +455,7 @@ abstract class LocaleKeys { static const unknown = 'unknown'; static const unableToActiveCoin = 'unableToActiveCoin'; static const feedback = 'feedback'; + static const failedToLoadAddresses = 'failedToLoadAddresses'; static const feedbackViewTitle = 'feedbackViewTitle'; static const feedbackPageDescription = 'feedbackPageDescription'; static const sendFeedbackButton = 'sendFeedbackButton'; @@ -469,6 +481,7 @@ abstract class LocaleKeys { static const cancelOrder = 'cancelOrder'; static const version = 'version'; static const copyToClipboard = 'copyToClipboard'; + static const copyAllDetails = 'copyAllDetails'; static const createdAt = 'createdAt'; static const coin = 'coin'; static const token = 'token'; @@ -634,5 +647,7 @@ abstract class LocaleKeys { static const allTimeProfit = 'allTimeProfit'; static const profitAndLoss = 'profitAndLoss'; static const searchAddresses = 'searchAddresses'; + static const userNotFoundError = 'userNotFoundError'; + static const loginFailedError = 'loginFailedError'; } diff --git a/lib/shared/ui/ui_primary_button.dart b/lib/shared/ui/ui_primary_button.dart index 22a1f98015..20e0a05100 100644 --- a/lib/shared/ui/ui_primary_button.dart +++ b/lib/shared/ui/ui_primary_button.dart @@ -1,160 +1,2 @@ -import 'package:app_theme/app_theme.dart'; -import 'package:flutter/material.dart'; - -class UiPrimaryButton extends StatelessWidget { - const UiPrimaryButton({ - Key? key, - this.buttonKey, - this.text = '', - this.width = double.infinity, - this.height = 48.0, - this.backgroundColor, - this.textStyle, - this.prefix, - this.border, - required this.onPressed, - this.focusNode, - this.shadowColor, - this.child, - }) : super(key: key); - - final String text; - final TextStyle? textStyle; - final double width; - final double height; - final Color? backgroundColor; - final Widget? prefix; - final Key? buttonKey; - final BoxBorder? border; - final void Function()? onPressed; - final FocusNode? focusNode; - final Color? shadowColor; - final Widget? child; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: onPressed == null, - child: Opacity( - opacity: onPressed == null ? 0.4 : 1, - child: Container( - constraints: BoxConstraints.tightFor(width: width, height: height), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: border, - ), - child: _Button( - focusNode: focusNode, - onPressed: onPressed, - buttonKey: buttonKey, - shadowColor: shadowColor, - backgroundColor: backgroundColor, - text: text, - textStyle: textStyle, - prefix: prefix, - child: child, - ), - ), - ), - ); - } -} - -class _Button extends StatefulWidget { - final FocusNode? focusNode; - final void Function()? onPressed; - final Key? buttonKey; - final Color? shadowColor; - final Color? backgroundColor; - final Widget? child; - final String text; - final TextStyle? textStyle; - final Widget? prefix; - const _Button({ - Key? key, - this.focusNode, - this.onPressed, - this.buttonKey, - this.shadowColor, - this.backgroundColor, - this.child, - required this.text, - this.textStyle, - this.prefix, - }) : super(key: key); - - @override - State<_Button> createState() => _ButtonState(); -} - -class _ButtonState extends State<_Button> { - bool _hasFocus = false; - - _ButtonState(); - @override - Widget build(BuildContext context) { - return ElevatedButton( - focusNode: widget.focusNode, - onFocusChange: (value) { - setState(() { - _hasFocus = value; - }); - }, - onPressed: widget.onPressed ?? () {}, - key: widget.buttonKey, - style: ElevatedButton.styleFrom( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(18)), - ), - shadowColor: _hasFocus - ? widget.shadowColor ?? Theme.of(context).colorScheme.primary - : Colors.transparent, - elevation: 1, - backgroundColor: _backgroundColor, - foregroundColor: - ThemeData.estimateBrightnessForColor(_backgroundColor) == - Brightness.dark - ? theme.global.light.colorScheme.onSurface - : Theme.of(context).colorScheme.secondary, - ), - child: widget.child ?? - _ButtonChild( - text: widget.text, - textStyle: widget.textStyle, - prefix: widget.prefix, - ), - ); - } - - Color get _backgroundColor { - return widget.backgroundColor ?? Theme.of(context).colorScheme.primary; - } -} - -class _ButtonChild extends StatelessWidget { - final Widget? prefix; - final String text; - final TextStyle? textStyle; - const _ButtonChild({ - Key? key, - required this.text, - this.prefix, - this.textStyle, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final textStyle = Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w700, - fontSize: 14, - color: theme.custom.defaultGradientButtonTextColor, - ); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (prefix != null) prefix!, - Text(text, style: textStyle ?? textStyle), - ], - ); - } -} +// This file has been deprecated. Use komodo_ui_kit's UiPrimaryButton instead. +// See: packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart diff --git a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart index 6bb41cefec..f14fc27d81 100644 --- a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart +++ b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart @@ -3,6 +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/flutter_svg.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/bridge_form/bridge_bloc.dart'; import 'package:web_dex/bloc/bridge_form/bridge_event.dart'; diff --git a/lib/shared/widgets/disclaimer/disclaimer.dart b/lib/shared/widgets/disclaimer/disclaimer.dart index a6f2c2c8e4..f1aae09586 100644 --- a/lib/shared/widgets/disclaimer/disclaimer.dart +++ b/lib/shared/widgets/disclaimer/disclaimer.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/shared/widgets/disclaimer/constants.dart'; import 'package:web_dex/shared/widgets/disclaimer/tos_content.dart'; diff --git a/lib/shared/widgets/disclaimer/eula.dart b/lib/shared/widgets/disclaimer/eula.dart index db7fe15363..7a0f1cca7f 100644 --- a/lib/shared/widgets/disclaimer/eula.dart +++ b/lib/shared/widgets/disclaimer/eula.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/widgets/disclaimer/constants.dart'; import 'package:web_dex/shared/widgets/disclaimer/tos_content.dart'; diff --git a/lib/shared/widgets/launch_native_explorer_button.dart b/lib/shared/widgets/launch_native_explorer_button.dart index 35b1e0b485..fadc3247ed 100644 --- a/lib/shared/widgets/launch_native_explorer_button.dart +++ b/lib/shared/widgets/launch_native_explorer_button.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/utils/utils.dart'; class LaunchNativeExplorerButton extends StatelessWidget { diff --git a/lib/views/bitrefill/bitrefill_button_view.dart b/lib/views/bitrefill/bitrefill_button_view.dart index 90e6084384..2c554356be 100644 --- a/lib/views/bitrefill/bitrefill_button_view.dart +++ b/lib/views/bitrefill/bitrefill_button_view.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class BitrefillButtonView extends StatelessWidget { const BitrefillButtonView({ diff --git a/lib/views/bitrefill/bitrefill_transaction_completed_dialog.dart b/lib/views/bitrefill/bitrefill_transaction_completed_dialog.dart index acf2bb572b..680c64ea0c 100644 --- a/lib/views/bitrefill/bitrefill_transaction_completed_dialog.dart +++ b/lib/views/bitrefill/bitrefill_transaction_completed_dialog.dart @@ -4,7 +4,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class BitrefillTransactionCompletedDialog extends StatelessWidget { const BitrefillTransactionCompletedDialog({ diff --git a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart index 56efd5bbd3..4352bef2df 100644 --- a/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart +++ b/lib/views/common/hw_wallet_dialog/trezor_steps/trezor_dialog_error.dart @@ -7,7 +7,7 @@ import 'package:web_dex/bloc/trezor_init_bloc/trezor_init_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/hw_wallet/trezor_status_error.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/views/common/hw_wallet_dialog/constants.dart'; class TrezorDialogError extends StatelessWidget { diff --git a/lib/views/common/page_header/page_header.dart b/lib/views/common/page_header/page_header.dart index 8b41f3c5f5..4565a48384 100644 --- a/lib/views/common/page_header/page_header.dart +++ b/lib/views/common/page_header/page_header.dart @@ -84,7 +84,7 @@ class _MobileHeader extends StatelessWidget { actions: [ if (actions != null) ...actions!, if (context.watch().state.mode != AuthorizeMode.logIn) - const Padding( + Padding( padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 20.0), child: ConnectWalletButton( eventType: WalletsManagerEventType.header, diff --git a/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart b/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart index eedde8512f..ade54b8129 100644 --- a/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart +++ b/lib/views/dex/dex_list_filter/mobile/dex_list_header_mobile.dart @@ -9,7 +9,7 @@ import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/dex_list_type.dart'; import 'package:web_dex/model/my_orders/my_order.dart'; import 'package:web_dex/model/trading_entities_filter.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class DexListHeaderMobile extends StatelessWidget { const DexListHeaderMobile({ diff --git a/lib/views/nfts/common/widgets/nft_connect_wallet.dart b/lib/views/nfts/common/widgets/nft_connect_wallet.dart index ca447e4d10..4854a60dac 100644 --- a/lib/views/nfts/common/widgets/nft_connect_wallet.dart +++ b/lib/views/nfts/common/widgets/nft_connect_wallet.dart @@ -18,7 +18,7 @@ class NftConnectWallet extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 210), child: NftNoLogin(text: LocaleKeys.nftMainLoggedOut.tr())), if (isMobile) - const Padding( + Padding( padding: EdgeInsets.only(top: 16), child: ConnectWalletButton( eventType: WalletsManagerEventType.nft, diff --git a/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart b/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart index c9db206cb5..e1ab8a8c65 100644 --- a/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart +++ b/lib/views/settings/widgets/security_settings/seed_settings/backup_seed_notification.dart @@ -7,7 +7,7 @@ import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/router/state/routing_state.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class BackupSeedNotification extends StatefulWidget { const BackupSeedNotification({ diff --git a/lib/views/wallet/coin_details/coin_details.dart b/lib/views/wallet/coin_details/coin_details.dart index 5059a5de39..857e08e26d 100644 --- a/lib/views/wallet/coin_details/coin_details.dart +++ b/lib/views/wallet/coin_details/coin_details.dart @@ -8,6 +8,7 @@ import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_details_info.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; import 'package:web_dex/views/wallet/coin_details/faucet/faucet_page.dart'; +import 'package:web_dex/views/wallet/coin_details/message_signing/message_signing_screen.dart'; import 'package:web_dex/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart'; import 'package:web_dex/views/wallet/coin_details/rewards/kmd_rewards_info.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/withdraw_form.dart'; @@ -97,6 +98,11 @@ class _CoinDetailsState extends State { formattedUsd: _formattedUsdPrice, onBackButtonPressed: _openInfo, ); + + case CoinPageType.signMessage: + return MessageSigningScreen( + coin: widget.coin, + ); } } 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 a9c084c305..35ed6871b2 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 @@ -4,13 +4,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui/komodo_ui.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'; import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/wallet.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/views/bitrefill/bitrefill_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/contract_address_button.dart'; @@ -102,18 +102,16 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (isBitrefillIntegrationEnabled) - Flexible( - child: BitrefillButton( - key: Key( - 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}', - ), - coin: coin, - onPaymentRequested: (_) => selectWidget(CoinPageType.send), - ), + Flexible( + child: CoinDetailsMessageSigningButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, ), - if (isBitrefillIntegrationEnabled) const SizedBox(width: 15), - if (!coin.walletOnly) + ), + if (!coin.walletOnly) ...[ + const SizedBox(width: 15), Flexible( child: CoinDetailsSwapButton( isMobile: isMobile, @@ -122,8 +120,20 @@ class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { context: context, ), ), + ], ], ), + if (isBitrefillIntegrationEnabled) + Padding( + padding: const EdgeInsets.only(top: 12), + child: BitrefillButton( + key: Key( + 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}', + ), + coin: coin, + onPaymentRequested: (_) => selectWidget(CoinPageType.send), + ), + ), ], ); } @@ -168,6 +178,16 @@ class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { context: context, ), ), + Container( + margin: const EdgeInsets.only(left: 21), + constraints: const BoxConstraints(maxWidth: 120), + child: CoinDetailsMessageSigningButton( + isMobile: isMobile, + coin: coin, + selectWidget: selectWidget, + context: context, + ), + ), if (!coin.walletOnly && !kIsWalletOnly) Container( margin: const EdgeInsets.only(left: 21), @@ -421,3 +441,46 @@ class CoinDetailsSwapButton extends StatelessWidget { ); } } + +class CoinDetailsMessageSigningButton extends StatelessWidget { + const CoinDetailsMessageSigningButton({ + required this.isMobile, + required this.coin, + required this.selectWidget, + required this.context, + super.key, + }); + + final bool isMobile; + final Coin coin; + final void Function(CoinPageType) selectWidget; + final BuildContext context; + + @override + Widget build(BuildContext context) { + final hasAddresses = + context.watch().state.addresses.isNotEmpty; + final ThemeData themeData = Theme.of(context); + + return UiPrimaryButton( + key: const Key('coin-details-sign-message-button'), + height: isMobile ? 52 : 40, + width: 210, + prefix: Container( + padding: const EdgeInsets.only(right: 14), + // child: SvgPicture.asset( + // '$assetsPath/others/signature.svg', + // ), + child: Icon(Icons.fingerprint)), + textStyle: themeData.textTheme.labelLarge + ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), + backgroundColor: themeData.colorScheme.tertiary, + onPressed: coin.isSuspended || !hasAddresses + ? null + : () { + selectWidget(CoinPageType.signMessage); + }, + text: LocaleKeys.signMessage.tr(), + ); + } +} diff --git a/lib/views/wallet/coin_details/coin_page_type.dart b/lib/views/wallet/coin_details/coin_page_type.dart index 87589a5a48..9a14c9ce47 100644 --- a/lib/views/wallet/coin_details/coin_page_type.dart +++ b/lib/views/wallet/coin_details/coin_page_type.dart @@ -1 +1 @@ -enum CoinPageType { send, faucet, claim, info, claimSuccess } +enum CoinPageType { send, faucet, claim, info, claimSuccess, signMessage } diff --git a/lib/views/wallet/coin_details/faucet/faucet_button.dart b/lib/views/wallet/coin_details/faucet/faucet_button.dart index fd18beef33..28ef925a01 100644 --- a/lib/views/wallet/coin_details/faucet/faucet_button.dart +++ b/lib/views/wallet/coin_details/faucet/faucet_button.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class FaucetButton extends StatelessWidget { const FaucetButton({ diff --git a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart new file mode 100644 index 0000000000..7f01aeebc8 --- /dev/null +++ b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart @@ -0,0 +1,952 @@ +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:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; +import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/utils.dart'; + +class MessageSigningScreen extends StatefulWidget { + final Coin coin; + + const MessageSigningScreen({ + super.key, + required this.coin, + }); + + @override + State createState() => _MessageSigningScreenState(); +} + +class _MessageSigningScreenState extends State { + late Asset asset; + + @override + void initState() { + super.initState(); + asset = widget.coin.toSdkAsset(context.sdk); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CoinAddressesBloc( + context.sdk, + widget.coin.abbr, + )..add(const LoadAddressesEvent()), + child: _MessageSigningScreenContent( + coin: widget.coin, + asset: asset, + ), + ); + } +} + +class _MessageSigningScreenContent extends StatefulWidget { + final Coin coin; + final Asset asset; + + const _MessageSigningScreenContent({ + required this.coin, + required this.asset, + }); + + @override + State<_MessageSigningScreenContent> createState() => + _MessageSigningScreenContentState(); +} + +class _MessageSigningScreenContentState + extends State<_MessageSigningScreenContent> { + PubkeyInfo? selectedAddress; + final TextEditingController messageController = TextEditingController(); + String? signedMessage; + String? errorMessage; + bool isLoading = false; + bool isLoadingAddresses = true; + AssetPubkeys? pubkeys; + + @override + void initState() { + super.initState(); + _loadAddresses(); + } + + Future _loadAddresses() async { + setState(() { + isLoadingAddresses = true; + }); + + try { + final addresses = await context.sdk.pubkeys.getPubkeys(widget.asset); + + setState(() { + pubkeys = addresses; + isLoadingAddresses = false; + if (addresses.keys.isNotEmpty) { + selectedAddress = addresses.keys.first; + } + }); + } catch (e) { + setState(() { + isLoadingAddresses = false; + errorMessage = + LocaleKeys.failedToLoadAddresses.tr(args: [e.toString()]); + }); + } + } + + Future _signMessage() async { + if (selectedAddress == null) { + setState(() { + errorMessage = LocaleKeys.pleaseSelectAddress.tr(); + }); + return; + } + + if (messageController.text.isEmpty) { + setState(() { + errorMessage = LocaleKeys.pleaseEnterMessage.tr(); + }); + return; + } + + setState(() { + isLoading = true; + signedMessage = null; + errorMessage = null; + }); + + try { + final signResult = await context.sdk.messageSigning.signMessage( + coin: widget.coin.abbr, + address: selectedAddress!.address, + message: messageController.text, + ); + + setState(() { + signedMessage = signResult; + isLoading = false; + }); + } catch (e) { + setState(() { + errorMessage = LocaleKeys.failedToSignMessage.tr(args: [e.toString()]); + isLoading = false; + }); + } + } + + void _copyToClipboard(String text) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(LocaleKeys.clipBoard.tr()), + duration: const Duration(seconds: 2), + ), + ); + } + + @override + void dispose() { + messageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isSelectEnabled = pubkeys != null && pubkeys!.keys.length > 1; + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.scaffoldBackgroundColor, + theme.scaffoldBackgroundColor.withOpacity(0.95), + ], + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 2, + shape: RoundedRectangleBorder( + side: BorderSide( + color: theme.colorScheme.primary.withOpacity(0.2), + ), + borderRadius: BorderRadius.circular(16), + ), + color: theme.colorScheme.surface.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Signing Address', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (errorMessage != null && !isLoadingAddresses) + ErrorMessageWidget(errorMessage: errorMessage!) + else + EnhancedAddressDropdown( + addresses: pubkeys?.keys ?? [], + selectedAddress: selectedAddress, + onAddressSelected: !isSelectEnabled + ? null + : (address) { + setState(() { + selectedAddress = address; + signedMessage = null; + errorMessage = null; + }); + }, + assetName: widget.asset.id.name, + ), + const SizedBox(height: 20), + Text( + LocaleKeys.messageToSign.tr(), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + EnhancedMessageInput( + controller: messageController, + hintText: LocaleKeys.enterMessage.tr(), + ), + const SizedBox(height: 24), + Center( + // child: EnhancedSignButton( + // text: LocaleKeys.signMessageButton.tr(), + // onPressed: isLoading ? null : _signMessage, + // isLoading: isLoading, + // ), + child: UiPrimaryButton( + text: LocaleKeys.signMessageButton.tr(), + onPressed: isLoading ? null : _signMessage, + width: double.infinity, + height: 56, + ), + ), + ], + ), + ), + ), + if (signedMessage != null) ...[ + const SizedBox(height: 24), + EnhancedSignedMessageCard( + selectedAddress: selectedAddress!, + message: messageController.text, + signedMessage: signedMessage!, + onCopyToClipboard: _copyToClipboard, + ), + ], + ], + ), + ), + ); + } +} + +// Enhanced Signed Message Card +class EnhancedSignedMessageCard extends StatelessWidget { + final PubkeyInfo selectedAddress; + final String message; + final String signedMessage; + final Function(String) onCopyToClipboard; + + const EnhancedSignedMessageCard({ + super.key, + required this.selectedAddress, + required this.message, + required this.signedMessage, + required this.onCopyToClipboard, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + elevation: 8, + shadowColor: theme.colorScheme.shadow.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + colors: [ + theme.colorScheme.surface, + theme.colorScheme.surfaceVariant.withOpacity(0.5), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionHeader(context, LocaleKeys.address.tr()), + _buildContentSection( + context, + selectedAddress.address, + icon: Icons.account_balance_wallet, + onCopy: () => onCopyToClipboard(selectedAddress.address), + ), + const SizedBox(height: 24), + _buildSectionHeader(context, LocaleKeys.message.tr()), + _buildContentSection( + context, + message, + icon: Icons.chat_bubble_outline, + onCopy: () => onCopyToClipboard(message), + ), + const SizedBox(height: 24), + _buildSectionHeader(context, LocaleKeys.signedMessage.tr()), + _buildContentSection( + context, + signedMessage, + icon: Icons.vpn_key_outlined, + onCopy: () => onCopyToClipboard(signedMessage), + isSignature: true, + ), + const SizedBox(height: 24), + _buildCopyAllButton(context), + ], + ), + ), + ), + ); + } + + Widget _buildSectionHeader(BuildContext context, String title) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + Text( + title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + letterSpacing: 0.5, + ), + ), + ], + ), + ); + } + + Widget _buildContentSection( + BuildContext context, + String content, { + required IconData icon, + required VoidCallback onCopy, + bool isSignature = false, + }) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorScheme.surface.withOpacity(0.7), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: CopyableTextField( + content: content, + onCopy: onCopy, + icon: icon, + isSignature: isSignature, + ), + ); + } + + Widget _buildCopyAllButton(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Container( + width: 200, + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + gradient: LinearGradient( + colors: [ + theme.colorScheme.secondary.withOpacity(0.9), + theme.colorScheme.tertiary.withOpacity(0.9), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.secondary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: MaterialButton( + onPressed: () => onCopyToClipboard( + 'Address: ${selectedAddress.address}\n\n' + 'Message: $message\n\n' + 'Signature: $signedMessage', + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.copy_all, + color: Colors.white, + size: 18, + ), + const SizedBox(width: 8), + Text( + LocaleKeys.copyAllDetails.tr(), + style: theme.textTheme.titleSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ); + } +} + +// Copyable Text Field Component +class CopyableTextField extends StatefulWidget { + final String content; + final VoidCallback onCopy; + final IconData icon; + final bool isSignature; + + const CopyableTextField({ + super.key, + required this.content, + required this.onCopy, + required this.icon, + this.isSignature = false, + }); + + @override + State createState() => _CopyableTextFieldState(); +} + +class _CopyableTextFieldState extends State + with SingleTickerProviderStateMixin { + bool _isCopied = false; + late AnimationController _controller; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + setState(() { + _isCopied = false; + }); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _copyWithAnimation() { + widget.onCopy(); + setState(() { + _isCopied = true; + }); + _controller.reset(); + _controller.forward(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return InkWell( + onTap: _copyWithAnimation, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + widget.icon, + size: 20, + color: theme.colorScheme.primary.withOpacity(0.7), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + widget.content, + style: theme.textTheme.bodyMedium?.copyWith( + letterSpacing: widget.isSignature ? 0 : 0.5, + fontFamily: widget.isSignature ? 'monospace' : null, + height: 1.5, + ), + ), + ], + ), + ), + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Opacity( + opacity: _isCopied ? _fadeAnimation.value : 1.0, + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: _isCopied + ? theme.colorScheme.primary.withOpacity(0.1) + : null, + borderRadius: BorderRadius.circular(6), + ), + child: Center( + child: Icon( + _isCopied ? Icons.check : Icons.copy, + size: 16, + color: _isCopied + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} + +// Error Message Widget +class ErrorMessageWidget extends StatelessWidget { + final String errorMessage; + + const ErrorMessageWidget({ + super.key, + required this.errorMessage, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + errorMessage, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ); + } +} + +// Enhanced Address Selection Widget +class EnhancedAddressDropdown extends StatelessWidget { + final List addresses; + final PubkeyInfo? selectedAddress; + final Function(PubkeyInfo)? onAddressSelected; + final String assetName; + + const EnhancedAddressDropdown({ + super.key, + required this.addresses, + required this.selectedAddress, + required this.onAddressSelected, + required this.assetName, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isEnabled = onAddressSelected != null; + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isEnabled + ? theme.colorScheme.primary.withOpacity(0.2) + : theme.colorScheme.outline.withOpacity(0.2), + ), + gradient: LinearGradient( + colors: [ + theme.colorScheme.surface, + theme.colorScheme.surface.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: selectedAddress, + onChanged: isEnabled + ? (newValue) { + if (newValue != null) { + onAddressSelected!(newValue); + } + } + : null, + borderRadius: BorderRadius.circular(12), + icon: AnimatedContainer( + duration: const Duration(milliseconds: 200), + child: Icon( + Icons.keyboard_arrow_down, + color: isEnabled + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withOpacity(0.4), + ), + ), + items: addresses.map((address) { + return DropdownMenuItem( + value: address, + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: _getColorFromAddress(address.address), + ), + child: Center( + child: Text( + address.address.substring(0, 1).toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _formatAddress(address.address), + style: theme.textTheme.bodyMedium?.copyWith( + letterSpacing: 0.5, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }).toList(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + ), + ); + } + + String _formatAddress(String address) { + if (address.length <= 16) return address; + return '${address.substring(0, 8)}...${address.substring(address.length - 8)}'; + } + + Color _getColorFromAddress(String address) { + final hash = address.codeUnits.fold(0, (a, b) => a + b); + return HSLColor.fromAHSL(1.0, hash % 360, 0.7, 0.5).toColor(); + } +} + +// Enhanced Message Input Widget +class EnhancedMessageInput extends StatefulWidget { + final TextEditingController controller; + final String hintText; + + const EnhancedMessageInput({ + super.key, + required this.controller, + required this.hintText, + }); + + @override + State createState() => _EnhancedMessageInputState(); +} + +class _EnhancedMessageInputState extends State { + late int charCount = 0; + + @override + void initState() { + super.initState(); + charCount = widget.controller.text.length; + widget.controller.addListener(_updateCharCount); + } + + void _updateCharCount() { + setState(() { + charCount = widget.controller.text.length; + }); + } + + @override + void dispose() { + widget.controller.removeListener(_updateCharCount); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: widget.controller, + decoration: InputDecoration( + hintText: widget.hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.5), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + contentPadding: const EdgeInsets.all(16), + fillColor: theme.colorScheme.surfaceVariant.withOpacity(0.3), + filled: true, + ), + style: theme.textTheme.bodyMedium?.copyWith( + letterSpacing: 0.5, + height: 1.5, + ), + maxLines: 4, + cursorColor: theme.colorScheme.primary, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + '$charCount characters', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + ], + ); + } +} + +// Enhanced Sign Button +class EnhancedSignButton extends StatefulWidget { + final String text; + final VoidCallback? onPressed; + final bool isLoading; + + const EnhancedSignButton({ + super.key, + required this.text, + required this.onPressed, + this.isLoading = false, + }); + + @override + State createState() => _EnhancedSignButtonState(); +} + +class _EnhancedSignButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _scaleAnimation = Tween(begin: 1.0, end: 1.03).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isEnabled = widget.onPressed != null && !widget.isLoading; + + if (isEnabled) { + _animationController.forward(); + } else { + _animationController.stop(); + _animationController.reset(); + } + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: isEnabled ? _scaleAnimation.value : 1.0, + child: Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + gradient: isEnabled + ? LinearGradient( + colors: [ + theme.colorScheme.primary, + Color.fromARGB( + 255, + theme.colorScheme.primary.red + 20, + theme.colorScheme.primary.green + 20, + theme.colorScheme.primary.blue + 40, + ), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: + isEnabled ? null : theme.colorScheme.primary.withOpacity(0.5), + boxShadow: isEnabled + ? [ + BoxShadow( + color: theme.colorScheme.primary.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ] + : null, + ), + child: MaterialButton( + onPressed: widget.onPressed, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + padding: EdgeInsets.zero, + child: widget.isLoading + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + theme.colorScheme.onPrimary, + ), + ), + ) + : Text( + widget.text, + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onPrimary, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart b/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart index 4beee093d5..8494e9cd63 100644 --- a/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart +++ b/lib/views/wallet/coin_details/rewards/kmd_reward_claim_success.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/common/app_assets.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/views/common/page_header/page_header.dart'; import 'package:web_dex/views/common/pages/page_layout.dart'; 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 d2fbdf7ad3..8b13789179 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 @@ -1,195 +1 @@ -import 'package:app_theme/app_theme.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:web_dex/bloc/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({super.key}); - - @override - Widget build(BuildContext context) { - final maxWidth = isMobile ? double.infinity : withdrawWidth; - - return ConstrainedBox( - constraints: BoxConstraints(maxWidth: maxWidth), - child: const Column( - children: [ - DexSvgImage(path: Assets.assetsDenied), - SizedBox(height: 20), - _SendErrorText(), - SizedBox(height: 20), - _SendErrorHeader(), - SizedBox(height: 15), - _SendErrorBody(), - SizedBox(height: 20), - _CloseButton(), - ], - ), - ); - } -} - -class _SendErrorText extends StatelessWidget { - const _SendErrorText(); - - @override - Widget build(BuildContext context) { - return Text( - LocaleKeys.tryAgain.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14, - color: Theme.of(context).colorScheme.error, - ), - ); - } -} - -class _SendErrorHeader extends StatelessWidget { - const _SendErrorHeader(); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - Text( - LocaleKeys.errorDescription.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ); - } -} - -class _SendErrorBody extends StatelessWidget { - const _SendErrorBody(); - - @override - Widget build(BuildContext context) { - return BlocSelector( - // TODO: Confirm this is the correct error - selector: (state) => state.transactionError, - builder: (BuildContext context, error) { - final iconColor = Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withValues(alpha: .7); - - return Material( - color: theme.custom.buttonColorDefault, - borderRadius: BorderRadius.circular(18), - child: InkWell( - onTap: error == null - ? null - : () => copyToClipBoard(context, error.error), - borderRadius: BorderRadius.circular(18), - child: Padding( - padding: const EdgeInsets.all(20.0), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 70, maxWidth: 300), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(child: _MultilineText(error?.error ?? '')), - const SizedBox(width: 16), - Icon( - Icons.copy_rounded, - color: iconColor, - size: 22, - ), - ], - ), - ), - ), - ), - ); - }, - ); - } -} - -class _MultilineText extends StatelessWidget { - const _MultilineText(this.text); - - final String text; - - @override - Widget build(BuildContext context) { - return Text( - text, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.left, - style: Theme.of(context).textTheme.bodyMedium, - softWrap: true, - maxLines: 3, - ); - } -} - -class _CloseButton extends StatelessWidget { - const _CloseButton(); - - @override - Widget build(BuildContext context) { - final height = isMobile ? 52.0 : 40.0; - return UiPrimaryButton( - height: height, - onPressed: () => - context.read().add(const WithdrawFormReset()), - text: LocaleKeys.close.tr(), - ); - } -} - -// class _PageContent extends StatelessWidget { -// const _PageContent(); -// -// @override -// Widget build(BuildContext context) { -// if (isMobile) return const _MobileContent(); -// return const _DesktopContent(); -// } -// } -// -// class _DesktopContent extends StatelessWidget { -// const _DesktopContent(); -// -// @override -// Widget build(BuildContext context) { -// return Column( -// children: [ -// const SizedBox(height: 20), -// assets.denied, -// const SizedBox(height: 19), -// const _Content(), -// ], -// ); -// } -// } -// -// class _MobileContent extends StatelessWidget { -// const _MobileContent(); -// -// @override -// Widget build(BuildContext context) { -// return Column( -// children: [ -// const SizedBox(height: 22), -// assets.denied, -// const SizedBox(height: 19), -// const _Content(), -// ], -// ); -// } -// } 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 6faa12e407..c234b52a36 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 @@ -4,7 +4,7 @@ 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/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class ConvertAddressButton extends StatelessWidget { const ConvertAddressButton({super.key}); 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 420817c359..397c1dc555 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 @@ -7,8 +7,8 @@ import 'package:web_dex/bloc/bitrefill/bloc/bitrefill_bloc.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'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/ui/app_button.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'; 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 27395b4d38..2b8b14b562 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 @@ -4,8 +4,8 @@ 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/generated/codegen_loader.g.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/shared/ui/app_button.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/views/wallet/coin_details/constants.dart'; class SendConfirmButtons extends StatelessWidget { diff --git a/lib/views/wallets_manager/widgets/wallet_list_item.dart b/lib/views/wallets_manager/widgets/wallet_list_item.dart index 94fa5cd0d7..ba52024780 100644 --- a/lib/views/wallets_manager/widgets/wallet_list_item.dart +++ b/lib/views/wallets_manager/widgets/wallet_list_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/model/wallets_manager_models.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/shared/widgets/auto_scroll_text.dart'; class WalletListItem extends StatelessWidget { diff --git a/lib/views/wallets_manager/widgets/wallet_login.dart b/lib/views/wallets_manager/widgets/wallet_login.dart index ea55373f54..5133e74c74 100644 --- a/lib/views/wallets_manager/widgets/wallet_login.dart +++ b/lib/views/wallets_manager/widgets/wallet_login.dart @@ -79,9 +79,8 @@ class _WalletLogInState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - // TODO: expand to parse SDK errors and show more specific messages - final errorMessage = state.errorMessage != null - ? LocaleKeys.invalidPasswordError.tr() + final String? errorMessage = state.errorMessage != null + ? _mapErrorToMessage(state.errorMessage!) : null; return Column( @@ -143,6 +142,19 @@ class _WalletLogInState extends State { }, ); } + + String _mapErrorToMessage(String error) { + switch (error) { + case 'invalid_password': + return LocaleKeys.invalidPasswordError.tr(); + case 'user_not_found': + return LocaleKeys.userNotFoundError.tr(); + case 'login_failed': + return LocaleKeys.loginFailedError.tr(); + default: + return error; + } + } } class PasswordTextField extends StatefulWidget { diff --git a/lib/views/wallets_manager/widgets/wallets_manager_controls.dart b/lib/views/wallets_manager/widgets/wallets_manager_controls.dart index 2424bcf907..974ee41a86 100644 --- a/lib/views/wallets_manager/widgets/wallets_manager_controls.dart +++ b/lib/views/wallets_manager/widgets/wallets_manager_controls.dart @@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/wallets_manager_models.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; class WalletsManagerControls extends StatelessWidget { const WalletsManagerControls({ diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index ccc527512c..55a67f9896 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -93,29 +93,23 @@ packages: komodo_defi_rpc_methods: dependency: transitive description: - path: "packages/komodo_defi_rpc_methods" - ref: dev - resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "../../sdk/packages/komodo_defi_rpc_methods" + relative: true + source: path version: "0.2.0+0" komodo_defi_types: dependency: "direct main" description: - path: "packages/komodo_defi_types" - ref: dev - resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "../../sdk/packages/komodo_defi_types" + relative: true + source: path version: "0.2.0+0" komodo_ui: dependency: "direct main" description: - path: "packages/komodo_ui" - ref: dev - resolved-ref: "4ae7d0d97197e607c3fe2d38ab92fa611de7cf89" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "../../sdk/packages/komodo_ui" + relative: true + source: path version: "0.2.0+0" lints: dependency: transitive diff --git a/packages/komodo_ui_kit/pubspec.yaml b/packages/komodo_ui_kit/pubspec.yaml index d511fc5298..2f063dcca1 100644 --- a/packages/komodo_ui_kit/pubspec.yaml +++ b/packages/komodo_ui_kit/pubspec.yaml @@ -14,18 +14,18 @@ dependencies: 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 + 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 + 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: ^5.0.0 # flutter.dev diff --git a/pubspec.lock b/pubspec.lock index 6e390881fe..bd1cb4dbed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -638,65 +638,51 @@ packages: komodo_cex_market_data: dependency: "direct main" description: - path: "packages/komodo_cex_market_data" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_cex_market_data" + relative: true + source: path version: "0.0.1" komodo_coins: dependency: transitive description: - path: "packages/komodo_coins" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_coins" + relative: true + source: path version: "0.2.0+0" komodo_defi_framework: dependency: transitive description: - path: "packages/komodo_defi_framework" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_defi_framework" + relative: true + source: path version: "0.2.0" komodo_defi_local_auth: dependency: transitive description: - path: "packages/komodo_defi_local_auth" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_defi_local_auth" + relative: true + source: path version: "0.2.0+0" komodo_defi_rpc_methods: dependency: transitive description: - path: "packages/komodo_defi_rpc_methods" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_defi_rpc_methods" + relative: true + source: path version: "0.2.0+0" komodo_defi_sdk: dependency: "direct main" description: - path: "packages/komodo_defi_sdk" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_defi_sdk" + relative: true + source: path version: "0.2.0+0" komodo_defi_types: dependency: "direct main" description: - path: "packages/komodo_defi_types" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_defi_types" + relative: true + source: path version: "0.2.0+0" komodo_persistence_layer: dependency: "direct main" @@ -708,11 +694,9 @@ packages: komodo_ui: dependency: "direct main" description: - path: "packages/komodo_ui" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_ui" + relative: true + source: path version: "0.2.0+0" komodo_ui_kit: dependency: "direct main" @@ -724,11 +708,9 @@ packages: komodo_wallet_build_transformer: dependency: transitive description: - path: "packages/komodo_wallet_build_transformer" - ref: dev - resolved-ref: "15abce0f075490d4cc1c189d459347eb4b40eb84" - url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" - source: git + path: "sdk/packages/komodo_wallet_build_transformer" + relative: true + source: path version: "0.2.0+0" leak_tracker: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index acc0cfd66f..dd5c50e5ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,11 +45,11 @@ dependencies: path: packages/komodo_persistence_layer komodo_cex_market_data: - # path: sdk/packages/komodo_cex_market_data # 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_cex_market_data - ref: dev + path: sdk/packages/komodo_cex_market_data # 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_cex_market_data + # ref: dev ## ---- KomodoPlatform pub.dev packages (First-party) @@ -142,25 +142,25 @@ dependencies: flutter_bloc: ^9.1.0 # sdk depends on this version, and hosted instead of git reference get_it: ^8.0.3 # 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 + 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 + 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 + 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 feedback: ^3.1.0 dev_dependencies: From e95b273ed26beaefaafaaa46028dc1e32b3a3ea3 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:54:32 +0200 Subject: [PATCH 05/13] fix: revert sdk reference to git --- packages/komodo_ui_kit/pubspec.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/komodo_ui_kit/pubspec.yaml b/packages/komodo_ui_kit/pubspec.yaml index 29db686f85..fa80bbd5e9 100644 --- a/packages/komodo_ui_kit/pubspec.yaml +++ b/packages/komodo_ui_kit/pubspec.yaml @@ -14,18 +14,18 @@ dependencies: 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 + # 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 + # 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: ^5.0.0 # flutter.dev From b63f600284c0a1335ab42b94a2465a20db43cac9 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:17:02 +0200 Subject: [PATCH 06/13] feat(ui-kit): Allow for UI kit buttons to inherit parent constraints --- .../lib/src/buttons/ui_base_button.dart | 22 ++++++--- .../lib/src/buttons/ui_border_button.dart | 44 +++++++++++++++--- .../lib/src/buttons/ui_primary_button.dart | 45 +++++++++++++++++-- .../lib/src/buttons/ui_secondary_button.dart | 21 ++++++++- 4 files changed, 115 insertions(+), 17 deletions(-) diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart index c0a9261bed..a524ecf3d1 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart @@ -4,14 +4,14 @@ class UIBaseButton extends StatelessWidget { const UIBaseButton({ required this.isEnabled, required this.child, - required this.width, - required this.height, + this.width, + this.height, required this.border, super.key, }); final bool isEnabled; - final double width; - final double height; + final double? width; + final double? height; final BoxBorder? border; final Widget child; @@ -22,7 +22,7 @@ class UIBaseButton extends StatelessWidget { child: Opacity( opacity: isEnabled ? 1 : 0.4, child: Container( - constraints: BoxConstraints.tightFor(width: width, height: height), + constraints: _buildConstraints(), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(18)), border: border, @@ -32,4 +32,16 @@ class UIBaseButton extends StatelessWidget { ), ); } + + BoxConstraints _buildConstraints() { + if (width != null && height != null) { + return BoxConstraints.tightFor(width: width, height: height); + } else if (width != null) { + return BoxConstraints.tightFor(width: width); + } else if (height != null) { + return BoxConstraints.tightFor(height: height); + } else { + return const BoxConstraints(); + } + } } diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart index baf85dc34e..433830bbca 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart @@ -19,10 +19,28 @@ class UiBorderButton extends StatelessWidget { this.fontSize = 14, this.textColor, }); - final String text; - final double width; - final double height; + /// Constructor for a border button which inherits its size from the parent widget. + const UiBorderButton.minSize({ + required this.text, + required this.onPressed, + super.key, + this.borderColor, + this.borderWidth = 3, + this.backgroundColor, + this.prefix, + this.suffix, + this.icon, + this.allowMultiline = false, + this.fontWeight = FontWeight.w700, + this.fontSize = 14, + this.textColor, + }) : width = null, + height = null; + + final String text; + final double? width; + final double? height; final Widget? prefix; final Widget? suffix; final Color? borderColor; @@ -42,10 +60,7 @@ class UiBorderButton extends StatelessWidget { return Opacity( opacity: onPressed == null ? 0.4 : 1, child: Container( - constraints: BoxConstraints.tightFor( - width: width, - height: allowMultiline ? null : height, - ), + constraints: _buildConstraints(), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(18)), color: borderColor ?? theme.custom.defaultBorderButtonBorder, @@ -108,6 +123,21 @@ class UiBorderButton extends StatelessWidget { ), ); } + + BoxConstraints _buildConstraints() { + if (width != null && height != null) { + if (allowMultiline) { + return BoxConstraints.tightFor(width: width); + } + return BoxConstraints.tightFor(width: width, height: height); + } else if (width != null) { + return BoxConstraints.tightFor(width: width); + } else if (height != null && !allowMultiline) { + return BoxConstraints.tightFor(height: height); + } else { + return const BoxConstraints(); + } + } } class _ButtonText extends StatelessWidget { diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart index b23846d9ec..06082b77db 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart @@ -3,6 +3,11 @@ import 'package:flutter/material.dart'; import 'package:komodo_ui_kit/src/buttons/ui_base_button.dart'; class UiPrimaryButton extends StatefulWidget { + /// Creates a primary button with the given properties. + /// + /// NB! Prefer using the [UiPrimaryButton.minSize] constructor. The [width] + /// and [height] parameters will be deprecated in the future and the button + /// will have the same behavior as the [UiPrimaryButton.minSize] constructor. const UiPrimaryButton({ required this.onPressed, this.buttonKey, @@ -12,6 +17,7 @@ class UiPrimaryButton extends StatefulWidget { this.backgroundColor, this.textStyle, this.prefix, + this.prefixPadding, this.border, this.focusNode, this.shadowColor, @@ -21,12 +27,38 @@ class UiPrimaryButton extends StatefulWidget { super.key, }); + /// Constructor for a primary button which inherits its size from the parent + /// widget. + /// + /// By default, the button will take up the minimum width required to fit its + /// content and use the minimum height needed. If you want it to take up the + /// full width of its parent, wrap it in a [SizedBox] or a [Container] with + /// `width: double.infinity`. + const UiPrimaryButton.minSize({ + required this.onPressed, + this.buttonKey, + this.text = '', + this.backgroundColor, + this.textStyle, + this.prefix, + this.prefixPadding = const EdgeInsets.only(right: 12), + this.border, + this.focusNode, + this.shadowColor, + this.child, + this.padding, + this.borderRadius, + super.key, + }) : width = null, + height = null; + final String text; final TextStyle? textStyle; - final double width; - final double height; + final double? width; + final double? height; final Color? backgroundColor; final Widget? prefix; + final EdgeInsets? prefixPadding; final Key? buttonKey; final BoxBorder? border; final void Function()? onPressed; @@ -71,6 +103,7 @@ class _UiPrimaryButtonState extends State { text: widget.text, textStyle: widget.textStyle, prefix: widget.prefix, + prefixPadding: widget.prefixPadding, ), ), ); @@ -104,18 +137,24 @@ class _ButtonContent extends StatelessWidget { required this.text, required this.textStyle, required this.prefix, + this.prefixPadding, }); final String text; final TextStyle? textStyle; final Widget? prefix; + final EdgeInsets? prefixPadding; @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (prefix != null) prefix!, + if (prefix != null) + Padding( + padding: prefixPadding ?? const EdgeInsets.only(right: 12), + child: prefix!, + ), Text(text, style: textStyle ?? _defaultTextStyle(context)), ], ); diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart index 422109051c..1e84485127 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart @@ -18,10 +18,27 @@ class UiSecondaryButton extends StatefulWidget { super.key, }); + /// Constructor for a secondary button which inherits its size from the parent + /// widget. + const UiSecondaryButton.minSize({ + required this.onPressed, + this.buttonKey, + this.text = '', + this.borderColor, + this.textStyle, + this.prefix, + this.border, + this.focusNode, + this.shadowColor, + this.child, + super.key, + }) : width = null, + height = null; + final String text; final TextStyle? textStyle; - final double width; - final double height; + final double? width; + final double? height; final Color? borderColor; final Widget? prefix; final Key? buttonKey; From 0cd55dbc86318f2583f1094b82fb609448fe7ab9 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 28 Apr 2025 08:06:35 +0200 Subject: [PATCH 07/13] fix(auth-bloc): update error state to use the new AuthException approach --- lib/bloc/auth_bloc/auth_bloc.dart | 10 ++++++++-- lib/views/wallets_manager/widgets/wallet_login.dart | 13 ------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 05e9fdebe0..9d98388428 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -83,10 +83,16 @@ class AuthBloc extends Bloc { } catch (e) { // Handle specific SDK authentication errors if (e.toString().contains('Invalid password')) { - return emit(AuthBlocState.error('invalid_password')); + return emit(AuthBlocState.error(AuthException( + 'incorrect_password', + type: AuthExceptionType.incorrectPassword, + ))); } // For other SDK authentication errors, pass through the specific error - return emit(AuthBlocState.error(e.toString())); + return emit(AuthBlocState.error(AuthException( + e.toString(), + type: AuthExceptionType.generalAuthError, + ))); } final KdfUser? currentUser = await _kdfSdk.auth.currentUser; diff --git a/lib/views/wallets_manager/widgets/wallet_login.dart b/lib/views/wallets_manager/widgets/wallet_login.dart index d5c8006bd5..8050210077 100644 --- a/lib/views/wallets_manager/widgets/wallet_login.dart +++ b/lib/views/wallets_manager/widgets/wallet_login.dart @@ -148,19 +148,6 @@ class _WalletLogInState extends State { }, ); } - - String _mapErrorToMessage(String error) { - switch (error) { - case 'invalid_password': - return LocaleKeys.invalidPasswordError.tr(); - case 'user_not_found': - return LocaleKeys.userNotFoundError.tr(); - case 'login_failed': - return LocaleKeys.loginFailedError.tr(); - default: - return error; - } - } } class PasswordTextField extends StatefulWidget { From 3e56023ba53c106c0aee76f615b974579a05867e Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:55:32 +0200 Subject: [PATCH 08/13] feat(ui): Rework UI button widgets Rework UI button widgets (e.g. `UiPrimaryButton`) to bring in line with Flutter layout best practices. These changes will be required for full localisation support since we will need dynamic width buttons. --- assets/translations/en.json | 1 + lib/bloc/auth_bloc/auth_bloc.dart | 4 +- lib/generated/codegen_loader.g.dart | 1 + lib/shared/utils/coin_filter_optimizer.dart | 0 lib/views/bitrefill/bitrefill_button.dart | 35 +- .../bitrefill/bitrefill_button_view.dart | 11 +- .../orders_table/grouped_list_view.dart | 26 +- .../orders_table/orders_table_content.dart | 1 + .../coin_search_dropdown.dart | 264 ++------------- .../coin_selection_and_amount_input.dart | 12 +- .../wallet/coin_details/coin_details.dart | 1 + .../coin_details_common_buttons.dart | 279 ++++++---------- .../coin_details_info/coin_details_info.dart | 5 +- .../coin_details/faucet/faucet_button.dart | 37 +-- .../message_signing_screen.dart | 311 +----------------- .../coins_manager_switch_button.dart | 2 +- .../wallet_main/wallet_manage_section.dart | 4 +- .../widgets/wallets_type_list.dart | 10 +- .../lib/src/buttons/ui_base_button.dart | 111 ++++--- .../lib/src/buttons/ui_border_button.dart | 51 ++- .../lib/src/buttons/ui_primary_button.dart | 243 +++++++++++--- .../lib/src/buttons/ui_secondary_button.dart | 99 ++++-- 22 files changed, 589 insertions(+), 919 deletions(-) create mode 100644 lib/shared/utils/coin_filter_optimizer.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index fd3a204e0e..da46feb07e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -565,6 +565,7 @@ "qrScannerErrorGenericError": "An error occurred", "qrScannerErrorTitle": "ERROR", "spend": "Spend", + "zeroBalanceTooltip": "Insufficient balance to use Bitrefill", "viewInvoice": "View Invoice", "systemTimeWarning": "System Time Incorrect: Your system time is more than 60 seconds off from the network time. Please update your system time before starting any swaps.", "errorCode": "Error code", diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index 9d98388428..d5ee23da87 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -68,7 +68,7 @@ class AuthBloc extends Bloc { ); } - _log.info('login from a wallet'); + _log.info('login from a wallet'); emit(AuthBlocState.loading()); try { await _kdfSdk.auth.signIn( @@ -100,7 +100,7 @@ class AuthBloc extends Bloc { return emit(AuthBlocState.error(AuthException.notSignedIn())); } - _log.info('logged in from a wallet'); + _log.info('logged in from a wallet'); emit(AuthBlocState.loggedIn(currentUser)); _listenToAuthStateChanges(); } catch (e, s) { diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 6ec67197cb..ff4afee21c 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -562,6 +562,7 @@ abstract class LocaleKeys { static const qrScannerErrorGenericError = 'qrScannerErrorGenericError'; static const qrScannerErrorTitle = 'qrScannerErrorTitle'; static const spend = 'spend'; + static const zeroBalanceTooltip = 'zeroBalanceTooltip'; static const viewInvoice = 'viewInvoice'; static const systemTimeWarning = 'systemTimeWarning'; static const errorCode = 'errorCode'; diff --git a/lib/shared/utils/coin_filter_optimizer.dart b/lib/shared/utils/coin_filter_optimizer.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/views/bitrefill/bitrefill_button.dart b/lib/views/bitrefill/bitrefill_button.dart index 0000aeb68b..3f5f0b90e9 100644 --- a/lib/views/bitrefill/bitrefill_button.dart +++ b/lib/views/bitrefill/bitrefill_button.dart @@ -10,6 +10,8 @@ import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/bitrefill/bitrefill_inappwebview_button.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:get_it/get_it.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; /// A button that opens the Bitrefill widget in a new window or tab. /// The Bitrefill widget is a web page that allows the user to purchase gift @@ -67,26 +69,31 @@ class _BitrefillButtonState extends State { sdk.balances.lastKnown(widget.coin.id)?.spendable.toDouble() ?? 0.0; final bool hasNonZeroBalance = coinBalance > 0; - final bool isEnabled = bitrefillLoadSuccess && - isCoinSupported && - !widget.coin.isSuspended && - hasNonZeroBalance; + final bool shouldShow = + bitrefillLoadSuccess && isCoinSupported && !widget.coin.isSuspended; + + final bool isEnabled = shouldShow && hasNonZeroBalance; final String url = state is BitrefillLoadSuccess ? state.url : ''; - if (!isEnabled) { + if (!shouldShow) { return const SizedBox.shrink(); } - return Column( - children: [ - BitrefillInAppWebviewButton( - windowTitle: widget.windowTitle, - url: url, - enabled: isEnabled, - onMessage: handleMessage, - ), - ], + // Show tooltip if balance is zero + final String tooltipMessage = + !hasNonZeroBalance ? LocaleKeys.zeroBalanceTooltip.tr() : ''; + + return Tooltip( + message: tooltipMessage, + child: BitrefillInAppWebviewButton( + key: Key( + 'coin-details-bitrefill-button-${widget.coin.abbr.toLowerCase()}'), + windowTitle: widget.windowTitle, + url: url, + enabled: isEnabled, + onMessage: handleMessage, + ), ); }, ); diff --git a/lib/views/bitrefill/bitrefill_button_view.dart b/lib/views/bitrefill/bitrefill_button_view.dart index 2c554356be..1fd287c5e6 100644 --- a/lib/views/bitrefill/bitrefill_button_view.dart +++ b/lib/views/bitrefill/bitrefill_button_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/app_config/app_config.dart'; -import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; @@ -17,13 +16,9 @@ class BitrefillButtonView extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( - height: isMobile ? 52 : 40, - prefix: Container( - padding: const EdgeInsets.only(right: 14), - child: SvgPicture.asset( - '$assetsPath/others/bitrefill_logo.svg', - ), + return UiPrimaryButton.flexible( + prefix: SvgPicture.asset( + '$assetsPath/others/bitrefill_logo.svg', ), textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), diff --git a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart index 4afc1d8b63..0e37f0b22c 100644 --- a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart +++ b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart @@ -1,9 +1,11 @@ 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/komodo_ui.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'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; @@ -102,17 +104,18 @@ class GroupedListView extends StatelessWidget { final firstCoin = getCoin(context, list.first); final KomodoDefiSdk sdk = GetIt.I(); - double totalBalance = list.fold(0.0, (sum, item) { + final totalBalance = list.fold(BalanceInfo.zero(), (sum, item) { final coin = getCoin(context, item); - final coinBalance = - sdk.balances.lastKnown(coin.id)?.spendable.toDouble() ?? 0.0; + final coinBalance = sdk.balances.lastKnown(coin.id) ?? BalanceInfo.zero(); return sum + coinBalance; }); final coin = firstCoin.dummyCopyWithoutProtocolData(); // Since we can't use 'balance' property directly anymore, we need to // construct the coin without using the balance property - return coin; + return coin.copyWith( + sendableBalance: totalBalance.spendable.toDouble(), + ); } Map> _groupList(BuildContext context, List list) { @@ -128,11 +131,16 @@ class GroupedListView extends StatelessWidget { final coinsState = RepositoryProvider.of(context).state; if (item is Coin) { return item as Coin; - } else if (item is SelectItem) { - return (coinsState.walletCoins[item.id] ?? coinsState.coins[item.id])!; - } else { - final String coinId = (item as BestOrder).coin; - return (coinsState.walletCoins[coinId] ?? coinsState.coins[coinId])!; } + + final coinsRepo = RepositoryProvider.of(context); + + final idString = (item is SelectItem) + ? (item as SelectItem).id + : (item as BestOrder).coin; + + return (coinsState.walletCoins[idString] ?? coinsState.coins[idString]) ?? + coinsRepo.getCoin(idString) ?? + (throw Exception('Coin $idString not found')); } } diff --git a/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart b/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart index a1d321403f..7fb2c20a7d 100644 --- a/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart +++ b/lib/views/dex/simple/form/tables/orders_table/orders_table_content.dart @@ -55,6 +55,7 @@ class OrdersTableContent extends StatelessWidget { if (orders.isEmpty) return const NothingFound(); return GroupedListView( + key: const Key('orders_table'), items: orders, onSelect: onSelect, maxHeight: maxHeight, diff --git a/lib/views/market_maker_bot/coin_search_dropdown.dart b/lib/views/market_maker_bot/coin_search_dropdown.dart index 65f0854017..04fe9b91e4 100644 --- a/lib/views/market_maker_bot/coin_search_dropdown.dart +++ b/lib/views/market_maker_bot/coin_search_dropdown.dart @@ -1,78 +1,18 @@ -import 'dart:async'; - -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/komodo_ui.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; -import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/widgets/coin_icon.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_body.dart'; -import 'package:komodo_ui/komodo_ui.dart'; - -bool doesCoinMatchSearch(String searchQuery, DropdownMenuItem item) { - final lowerCaseQuery = searchQuery.toLowerCase(); - if (item.value == null) return false; - - final name = item.value!; - final nameContains = name.toLowerCase().contains(lowerCaseQuery); - final idMatches = name.toLowerCase().contains(lowerCaseQuery); - - return nameContains || idMatches; -} - -Future showCoinSearch( - BuildContext context, { - required List coins, - DropdownMenuItem Function(String coinId)? customCoinItemBuilder, - double maxHeight = 330, -}) async { - final isMobile = MediaQuery.of(context).size.width < 600; - - final items = coins - .map( - (coin) => - customCoinItemBuilder?.call(coin) ?? _defaultCoinItemBuilder(coin), - ) - .toList(); - - if (isMobile) { - return showSearch( - context: context, - delegate: SearchableSelectorDelegate( - items, - searchHint: 'Search coins', - ), - ); - } else { - return showSearchableSelect( - context: context, - items: items, - searchHint: 'Search coins', - ); - } -} - -DropdownMenuItem _defaultCoinItemBuilder(String coin) { - return DropdownMenuItem( - value: coin, - child: Row( - children: [ - CoinIcon(coin), - const SizedBox(width: 12), - Text(coin), - ], - ), - ); -} class CoinDropdown extends StatefulWidget { - final List> items; + final List coins; final Widget? child; - final Function(String) onItemSelected; + final Function(AssetId) onItemSelected; const CoinDropdown({ super.key, - required this.items, + required this.coins, required this.onItemSelected, this.child, }); @@ -82,186 +22,36 @@ class CoinDropdown extends StatefulWidget { } class _CoinDropdownState extends State { - String? selectedItem; - final LayerLink _layerLink = LayerLink(); - OverlayEntry? _overlayEntry; + AssetId? selectedAssetId; void _showSearch() async { - _overlayEntry = _createOverlayEntry(); - Overlay.of(context).insert(_overlayEntry!); - } - - OverlayEntry _createOverlayEntry() { - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - final offset = renderBox.localToGlobal(Offset.zero); - - final screenSize = MediaQuery.of(context).size; - final availableHeightBelow = screenSize.height - offset.dy - size.height; - final availableHeightAbove = offset.dy; - - final showAbove = availableHeightBelow < widget.items.length * 48 && - availableHeightAbove > availableHeightBelow; - - final dropdownHeight = - (showAbove ? availableHeightAbove : availableHeightBelow) - .clamp(100.0, 330.0); - - return OverlayEntry( - builder: (context) { - return GestureDetector( - onTap: () { - _overlayEntry?.remove(); - _overlayEntry = null; - }, - behavior: HitTestBehavior.translucent, - child: Stack( - children: [ - Positioned( - left: offset.dx, - top: showAbove - ? offset.dy - dropdownHeight - : offset.dy + size.height, - width: size.width, - child: _SearchableDropdown( - items: widget.items, - onItemSelected: (value) { - if (value != null) { - setState(() => selectedItem = value); - widget.onItemSelected(value); - } - _overlayEntry?.remove(); - _overlayEntry = null; - }, - maxHeight: dropdownHeight, - ), - ), - ], - ), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - final coinsRepository = RepositoryProvider.of(context); - final coin = - selectedItem == null ? null : coinsRepository.getCoin(selectedItem!); - - return CompositedTransformTarget( - link: _layerLink, - child: InkWell( - onTap: _showSearch, - child: widget.child ?? - Padding( - padding: const EdgeInsets.only(left: 15), - child: CoinItemBody(coin: coin), - ), - ), + final selectedCoin = await showCoinSearch( + context, + coins: widget.coins, ); - } -} - -class _SearchableDropdown extends StatefulWidget { - final List> items; - final ValueChanged onItemSelected; - final double maxHeight; - - const _SearchableDropdown({ - required this.items, - required this.onItemSelected, - this.maxHeight = 300, - }); - @override - State<_SearchableDropdown> createState() => _SearchableDropdownState(); -} - -class _SearchableDropdownState extends State<_SearchableDropdown> { - late List> 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) => doesCoinMatchSearch(query, item)) - .toList(); - }); - } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); + if (selectedCoin != null && mounted) { + setState(() { + selectedAssetId = selectedCoin; + }); + widget.onItemSelected(selectedCoin); + } } @override Widget build(BuildContext context) { - return Card( - elevation: 4, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - color: Theme.of(context).colorScheme.surfaceContainer, - child: Container( - constraints: BoxConstraints(maxHeight: widget.maxHeight), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(12), - child: TextFormField( - focusNode: _focusNode, - autofocus: true, - decoration: InputDecoration( - hintText: 'Search', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - prefixIcon: const Icon(Icons.search), - ), - onChanged: updateSearchQuery, - ), - ), - if (filteredItems.isNotEmpty) - Expanded( - child: ListView.builder( - itemCount: filteredItems.length, - itemBuilder: (context, index) { - final item = filteredItems[index]; - return ListTile( - leading: item.child is Row - ? (item.child as Row).children.first - : item.child, - title: item.child is Row - ? Row( - children: - (item.child as Row).children.skip(1).toList(), - ) - : null, - onTap: () => widget.onItemSelected(item.value), - ); - }, - ), - ) - else - Padding( - padding: const EdgeInsets.all(16), - child: Text(LocaleKeys.nothingFound.tr()), - ), - ], - ), - ), + final coinsRepository = RepositoryProvider.of(context); + final coin = selectedAssetId == null + ? null + : coinsRepository.getCoinFromId(selectedAssetId!); + + return InkWell( + onTap: _showSearch, + child: widget.child ?? + Padding( + padding: const EdgeInsets.only(left: 15), + child: CoinItemBody(coin: coin), + ), ); } } diff --git a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart index 7d4087c34b..ef06996e5d 100644 --- a/lib/views/market_maker_bot/coin_selection_and_amount_input.dart +++ b/lib/views/market_maker_bot/coin_selection_and_amount_input.dart @@ -1,9 +1,11 @@ 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/komodo_defi_types.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; import 'package:web_dex/bloc/settings/settings_bloc.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:komodo_ui/komodo_ui.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_body.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_logo.dart'; @@ -118,9 +120,13 @@ class _CoinSelectionAndAmountInputState final coinsRepository = RepositoryProvider.of(context); return CoinDropdown( - items: _items, - onItemSelected: (item) async => widget.onItemSelected - ?.call(await coinsRepository.getEnabledCoin(item)), + coins: widget.coins.map((coin) => coin.assetId).toList(), + onItemSelected: (assetId) async { + final coin = coinsRepository.getCoinFromId(assetId); + if (coin != null) { + widget.onItemSelected?.call(coin); + } + }, child: content, ); } diff --git a/lib/views/wallet/coin_details/coin_details.dart b/lib/views/wallet/coin_details/coin_details.dart index 1b5ac4d40e..0b1ed7814b 100644 --- a/lib/views/wallet/coin_details/coin_details.dart +++ b/lib/views/wallet/coin_details/coin_details.dart @@ -94,6 +94,7 @@ class _CoinDetailsState extends State { case CoinPageType.signMessage: return MessageSigningScreen( coin: widget.coin, + onBackButtonPressed: _openInfo, ); } } 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 04d527fabd..cfa56eb969 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 @@ -15,225 +15,143 @@ import 'package:web_dex/views/bitrefill/bitrefill_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/contract_address_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; +import 'package:web_dex/views/wallet/coin_details/faucet/faucet_button.dart'; class CoinDetailsCommonButtons extends StatelessWidget { const CoinDetailsCommonButtons({ - required this.isMobile, required this.selectWidget, required this.onClickSwapButton, required this.coin, super.key, }); - final bool isMobile; final Coin coin; final void Function(CoinPageType) selectWidget; final VoidCallback? onClickSwapButton; @override Widget build(BuildContext context) { - return isMobile - ? CoinDetailsCommonButtonsMobileLayout( - coin: coin, - isMobile: isMobile, - selectWidget: selectWidget, - clickSwapButton: onClickSwapButton, - context: context, - ) - : CoinDetailsCommonButtonsDesktopLayout( - isMobile: isMobile, - coin: coin, - selectWidget: selectWidget, - clickSwapButton: onClickSwapButton, - context: context, - ); + return ResponsiveButtonLayout( + coin: coin, + selectWidget: selectWidget, + onClickSwapButton: onClickSwapButton, + ); } } -class CoinDetailsCommonButtonsMobileLayout extends StatelessWidget { - const CoinDetailsCommonButtonsMobileLayout({ +class ResponsiveButtonLayout extends StatelessWidget { + const ResponsiveButtonLayout({ required this.coin, - required this.isMobile, required this.selectWidget, - required this.clickSwapButton, - required this.context, + required this.onClickSwapButton, super.key, }); final Coin coin; - final bool isMobile; - final void Function(CoinPageType p1) selectWidget; - final VoidCallback? clickSwapButton; - final BuildContext context; + final void Function(CoinPageType) selectWidget; + final VoidCallback? onClickSwapButton; @override Widget build(BuildContext context) { - return Column( - children: [ - Visibility( - visible: coin.protocolData?.contractAddress.isNotEmpty ?? false, - child: ContractAddressButton(coin), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: CoinDetailsSendButton( - isMobile: isMobile, - coin: coin, - selectWidget: selectWidget, - context: context, - ), - ), - const SizedBox(width: 15), - Flexible( - child: CoinDetailsReceiveButton( - isMobile: isMobile, - coin: coin, - selectWidget: selectWidget, - context: context, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: CoinDetailsMessageSigningButton( - isMobile: isMobile, - coin: coin, - selectWidget: selectWidget, - context: context, - ), - ), - if (!coin.walletOnly) ...[ - const SizedBox(width: 15), - Flexible( - child: CoinDetailsSwapButton( - isMobile: isMobile, - coin: coin, - onClickSwapButton: clickSwapButton, - context: context, - ), - ), - ], - ], - ), - if (isBitrefillIntegrationEnabled) - Padding( - padding: const EdgeInsets.only(top: 12), - child: BitrefillButton( - key: Key( - 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}', - ), - coin: coin, - onPaymentRequested: (_) => selectWidget(CoinPageType.send), - ), - ), - ], - ); - } -} - -class CoinDetailsCommonButtonsDesktopLayout extends StatelessWidget { - const CoinDetailsCommonButtonsDesktopLayout({ - required this.isMobile, - required this.coin, - required this.selectWidget, - required this.clickSwapButton, - required this.context, - super.key, - }); + final hasContractAddress = + coin.protocolData?.contractAddress.isNotEmpty ?? false; + final double spacing = 12.0; - final bool isMobile; - final Coin coin; - final void Function(CoinPageType p1) selectWidget; - final VoidCallback? clickSwapButton; - final BuildContext context; + return LayoutBuilder( + builder: (context, constraints) { + final isNarrowLayout = constraints.maxWidth < 600; + final List buttons = []; - @override - Widget build(BuildContext context) { - return Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 120), - child: CoinDetailsSendButton( - isMobile: isMobile, + // Add send button + buttons.add( + CoinDetailsSendButton( coin: coin, selectWidget: selectWidget, context: context, ), - ), - Container( - margin: const EdgeInsets.only(left: 21), - constraints: const BoxConstraints(maxWidth: 120), - child: CoinDetailsReceiveButton( - isMobile: isMobile, + ); + + // Add receive button + buttons.add( + CoinDetailsReceiveButton( coin: coin, selectWidget: selectWidget, context: context, ), - ), - Container( - margin: const EdgeInsets.only(left: 21), - constraints: const BoxConstraints(maxWidth: 120), - child: CoinDetailsMessageSigningButton( - isMobile: isMobile, + ); + + // Add message signing button + buttons.add( + CoinDetailsMessageSigningButton( coin: coin, selectWidget: selectWidget, context: context, ), - ), - if (!coin.walletOnly && !kIsWalletOnly) - Container( - margin: const EdgeInsets.only(left: 21), - constraints: const BoxConstraints(maxWidth: 120), - child: CoinDetailsSwapButton( - isMobile: isMobile, + ); + + // Add swap button if needed + if (!coin.walletOnly && !kIsWalletOnly) { + buttons.add( + CoinDetailsSwapButton( coin: coin, - onClickSwapButton: clickSwapButton, + onClickSwapButton: onClickSwapButton, context: context, ), - ), - if (isBitrefillIntegrationEnabled) - Container( - margin: const EdgeInsets.only(left: 21), - constraints: const BoxConstraints(maxWidth: 120), - child: BitrefillButton( + ); + } + + if (isBitrefillIntegrationEnabled) { + buttons.add( + BitrefillButton( key: Key( - 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}', - ), + 'coin-details-bitrefill-button-${coin.abbr.toLowerCase()}'), coin: coin, onPaymentRequested: (_) => selectWidget(CoinPageType.send), ), - ), - Flexible( - flex: 2, - child: Align( - alignment: Alignment.centerRight, - child: coin.protocolData?.contractAddress.isNotEmpty ?? false - ? SizedBox(width: 230, child: ContractAddressButton(coin)) - : null, - ), - ), - ], + ); + } + + // Add contract address button if the coin has a contract address + if (hasContractAddress) { + buttons.add( + ContractAddressButton( + coin, + key: const Key('coin-details-contract-address-button'), + ), + ); + } + + // // Determine button height based on layout + final buttonHeight = isNarrowLayout ? 52.0 : 48.0; + + return Wrap( + spacing: spacing, + runSpacing: spacing, + alignment: WrapAlignment.start, + children: buttons + .map( + (button) => IntrinsicWidth( + child: SizedBox( + height: buttonHeight, + child: button, + ), + ), + ) + .toList(), + ); + }, ); } } class CoinDetailsReceiveButton extends StatelessWidget { const CoinDetailsReceiveButton({ - required this.isMobile, required this.coin, required this.selectWidget, required this.context, super.key, }); - final bool isMobile; final Coin coin; final void Function(CoinPageType p1) selectWidget; final BuildContext context; @@ -265,14 +183,12 @@ class CoinDetailsReceiveButton extends StatelessWidget { final hasAddresses = context.watch().state.addresses.isNotEmpty; final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( + + return UiPrimaryButton.flexible( key: const Key('coin-details-receive-button'), - height: isMobile ? 52 : 40, - prefix: Container( - padding: const EdgeInsets.only(right: 14), - child: SvgPicture.asset( - '$assetsPath/others/receive.svg', - ), + // height: isNarrowLayout ? 52 : 40, + prefix: SvgPicture.asset( + '$assetsPath/others/receive.svg', ), textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), @@ -354,14 +270,12 @@ class AddressListItem extends StatelessWidget { class CoinDetailsSendButton extends StatelessWidget { const CoinDetailsSendButton({ - required this.isMobile, required this.coin, required this.selectWidget, required this.context, super.key, }); - final bool isMobile; final Coin coin; final void Function(CoinPageType p1) selectWidget; final BuildContext context; @@ -369,9 +283,9 @@ class CoinDetailsSendButton extends StatelessWidget { @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( + + return UiPrimaryButton.flexible( key: const Key('coin-details-send-button'), - height: isMobile ? 52 : 40, prefix: Container( padding: const EdgeInsets.only(right: 14), child: SvgPicture.asset( @@ -381,8 +295,9 @@ class CoinDetailsSendButton extends StatelessWidget { textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, + optimisticEnabledDuration: const Duration(seconds: 5), onPressed: coin.isSuspended - //TODO!.sdk || coin.balance == 0 + //TODO!: coin.balance == 0 ? null : () { selectWidget(CoinPageType.send); @@ -394,14 +309,12 @@ class CoinDetailsSendButton extends StatelessWidget { class CoinDetailsSwapButton extends StatelessWidget { const CoinDetailsSwapButton({ - required this.isMobile, required this.coin, required this.onClickSwapButton, required this.context, super.key, }); - final bool isMobile; final Coin coin; final VoidCallback? onClickSwapButton; final BuildContext context; @@ -415,9 +328,9 @@ class CoinDetailsSwapButton extends StatelessWidget { } final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( + + return UiPrimaryButton.flexible( key: const Key('coin-details-swap-button'), - height: isMobile ? 52 : 40, textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, @@ -435,14 +348,12 @@ class CoinDetailsSwapButton extends StatelessWidget { class CoinDetailsMessageSigningButton extends StatelessWidget { const CoinDetailsMessageSigningButton({ - required this.isMobile, required this.coin, required this.selectWidget, required this.context, super.key, }); - final bool isMobile; final Coin coin; final void Function(CoinPageType) selectWidget; final BuildContext context; @@ -453,19 +364,13 @@ class CoinDetailsMessageSigningButton extends StatelessWidget { context.watch().state.addresses.isNotEmpty; final ThemeData themeData = Theme.of(context); - return UiPrimaryButton( + return UiPrimaryButton.flexible( key: const Key('coin-details-sign-message-button'), - height: isMobile ? 52 : 40, - width: 210, - prefix: Container( - padding: const EdgeInsets.only(right: 14), - // child: SvgPicture.asset( - // '$assetsPath/others/signature.svg', - // ), - child: Icon(Icons.fingerprint)), + prefix: Icon(Icons.fingerprint), textStyle: themeData.textTheme.labelLarge ?.copyWith(fontSize: 14, fontWeight: FontWeight.w600), backgroundColor: themeData.colorScheme.tertiary, + optimisticEnabledDuration: const Duration(seconds: 5), onPressed: coin.isSuspended || !hasAddresses ? null : () { @@ -474,4 +379,4 @@ class CoinDetailsMessageSigningButton extends StatelessWidget { text: LocaleKeys.signMessage.tr(), ); } -} +} \ No newline at end of file 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 cb52765f99..597382117d 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 @@ -257,10 +257,10 @@ class _DesktopCoinDetails extends StatelessWidget { ), ], ), - Padding( + Container( padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), + width: double.infinity, child: CoinDetailsCommonButtons( - isMobile: false, selectWidget: setPageType, onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() ? null @@ -361,7 +361,6 @@ class _CoinDetailsInfoHeader extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 12.0, bottom: 14.0), child: CoinDetailsCommonButtons( - isMobile: true, selectWidget: setPageType, onClickSwapButton: MainMenuValue.dex.isEnabledInCurrentMode() ? () => _goToSwap(context, coin) diff --git a/lib/views/wallet/coin_details/faucet/faucet_button.dart b/lib/views/wallet/coin_details/faucet/faucet_button.dart index 60070af208..64fd08248b 100644 --- a/lib/views/wallet/coin_details/faucet/faucet_button.dart +++ b/lib/views/wallet/coin_details/faucet/faucet_button.dart @@ -68,9 +68,8 @@ class _FaucetButtonState extends State { : themeData.colorScheme.tertiary, borderRadius: BorderRadius.circular(16.0), ), - child: UiPrimaryButton( + child: UiPrimaryButton.flexible( key: Key('coin-details-faucet-button-${widget.address.address}'), - height: isMobile ? 24.0 : 32.0, backgroundColor: themeData.colorScheme.tertiary, onPressed: isLoading ? null @@ -80,24 +79,22 @@ class _FaucetButtonState extends State { address: widget.address.address, )); }, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: isMobile ? 6.0 : 8.0, - horizontal: isMobile ? 8.0 : 12.0, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.local_drink_rounded, - color: Colors.blue, size: isMobile ? 14 : 16), - Text( - LocaleKeys.faucet.tr(), - style: TextStyle( - fontSize: isMobile ? 9 : 12, - fontWeight: FontWeight.w500), - ), - ], - ), + padding: EdgeInsets.symmetric( + vertical: isMobile ? 6.0 : 8.0, + horizontal: isMobile ? 8.0 : 12.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.local_drink_rounded, + color: Colors.blue, size: isMobile ? 14 : 16), + Text( + LocaleKeys.faucet.tr(), + style: TextStyle( + fontSize: isMobile ? 9 : 12, + fontWeight: FontWeight.w500), + ), + ], ), ), ), diff --git a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart index 7f01aeebc8..3f7ba8b7df 100644 --- a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart +++ b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart @@ -13,10 +13,12 @@ import 'package:web_dex/shared/utils/utils.dart'; class MessageSigningScreen extends StatefulWidget { final Coin coin; + final VoidCallback? onBackButtonPressed; const MessageSigningScreen({ super.key, required this.coin, + this.onBackButtonPressed, }); @override @@ -42,6 +44,7 @@ class _MessageSigningScreenState extends State { child: _MessageSigningScreenContent( coin: widget.coin, asset: asset, + onBackButtonPressed: widget.onBackButtonPressed, ), ); } @@ -50,10 +53,12 @@ class _MessageSigningScreenState extends State { class _MessageSigningScreenContent extends StatefulWidget { final Coin coin; final Asset asset; + final VoidCallback? onBackButtonPressed; const _MessageSigningScreenContent({ required this.coin, required this.asset, + this.onBackButtonPressed, }); @override @@ -202,7 +207,7 @@ class _MessageSigningScreenContentState if (errorMessage != null && !isLoadingAddresses) ErrorMessageWidget(errorMessage: errorMessage!) else - EnhancedAddressDropdown( + AddressSelectInput( addresses: pubkeys?.keys ?? [], selectedAddress: selectedAddress, onAddressSelected: !isSelectEnabled @@ -230,11 +235,6 @@ class _MessageSigningScreenContentState ), const SizedBox(height: 24), Center( - // child: EnhancedSignButton( - // text: LocaleKeys.signMessageButton.tr(), - // onPressed: isLoading ? null : _signMessage, - // isLoading: isLoading, - // ), child: UiPrimaryButton( text: LocaleKeys.signMessageButton.tr(), onPressed: isLoading ? null : _signMessage, @@ -302,7 +302,6 @@ class EnhancedSignedMessageCard extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(20.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader(context, LocaleKeys.address.tr()), _buildContentSection( @@ -399,60 +398,12 @@ class EnhancedSignedMessageCard extends StatelessWidget { } Widget _buildCopyAllButton(BuildContext context) { - final theme = Theme.of(context); - - return Center( - child: Container( - width: 200, - height: 48, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - gradient: LinearGradient( - colors: [ - theme.colorScheme.secondary.withOpacity(0.9), - theme.colorScheme.tertiary.withOpacity(0.9), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - boxShadow: [ - BoxShadow( - color: theme.colorScheme.secondary.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: MaterialButton( - onPressed: () => onCopyToClipboard( - 'Address: ${selectedAddress.address}\n\n' - 'Message: $message\n\n' - 'Signature: $signedMessage', - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), - padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.copy_all, - color: Colors.white, - size: 18, - ), - const SizedBox(width: 8), - Text( - LocaleKeys.copyAllDetails.tr(), - style: theme.textTheme.titleSmall?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ], - ), - ), + return UiPrimaryButton.flexible( + child: Text(LocaleKeys.copyAllDetails.tr()), + onPressed: () => onCopyToClipboard( + 'Address:\n${selectedAddress.address}\n\n' + 'Message:\n$message\n\n' + 'Signature:\n$signedMessage', ), ); } @@ -573,7 +524,7 @@ class _CopyableTextFieldState extends State size: 16, color: _isCopied ? theme.colorScheme.primary - : theme.colorScheme.onSurface.withOpacity(0.6), + : theme.colorScheme.onPrimaryContainer, ), ), ), @@ -611,116 +562,6 @@ class ErrorMessageWidget extends StatelessWidget { } // Enhanced Address Selection Widget -class EnhancedAddressDropdown extends StatelessWidget { - final List addresses; - final PubkeyInfo? selectedAddress; - final Function(PubkeyInfo)? onAddressSelected; - final String assetName; - - const EnhancedAddressDropdown({ - super.key, - required this.addresses, - required this.selectedAddress, - required this.onAddressSelected, - required this.assetName, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isEnabled = onAddressSelected != null; - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isEnabled - ? theme.colorScheme.primary.withOpacity(0.2) - : theme.colorScheme.outline.withOpacity(0.2), - ), - gradient: LinearGradient( - colors: [ - theme.colorScheme.surface, - theme.colorScheme.surface.withOpacity(0.8), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: selectedAddress, - onChanged: isEnabled - ? (newValue) { - if (newValue != null) { - onAddressSelected!(newValue); - } - } - : null, - borderRadius: BorderRadius.circular(12), - icon: AnimatedContainer( - duration: const Duration(milliseconds: 200), - child: Icon( - Icons.keyboard_arrow_down, - color: isEnabled - ? theme.colorScheme.primary - : theme.colorScheme.onSurface.withOpacity(0.4), - ), - ), - items: addresses.map((address) { - return DropdownMenuItem( - value: address, - child: Row( - children: [ - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: _getColorFromAddress(address.address), - ), - child: Center( - child: Text( - address.address.substring(0, 1).toUpperCase(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - _formatAddress(address.address), - style: theme.textTheme.bodyMedium?.copyWith( - letterSpacing: 0.5, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }).toList(), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - ), - ); - } - - String _formatAddress(String address) { - if (address.length <= 16) return address; - return '${address.substring(0, 8)}...${address.substring(address.length - 8)}'; - } - - Color _getColorFromAddress(String address) { - final hash = address.codeUnits.fold(0, (a, b) => a + b); - return HSLColor.fromAHSL(1.0, hash % 360, 0.7, 0.5).toColor(); - } -} // Enhanced Message Input Widget class EnhancedMessageInput extends StatefulWidget { @@ -824,129 +665,3 @@ class _EnhancedMessageInputState extends State { ); } } - -// Enhanced Sign Button -class EnhancedSignButton extends StatefulWidget { - final String text; - final VoidCallback? onPressed; - final bool isLoading; - - const EnhancedSignButton({ - super.key, - required this.text, - required this.onPressed, - this.isLoading = false, - }); - - @override - State createState() => _EnhancedSignButtonState(); -} - -class _EnhancedSignButtonState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _scaleAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - )..repeat(reverse: true); - - _scaleAnimation = Tween(begin: 1.0, end: 1.03).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - ), - ); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final isEnabled = widget.onPressed != null && !widget.isLoading; - - if (isEnabled) { - _animationController.forward(); - } else { - _animationController.stop(); - _animationController.reset(); - } - - return AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return Transform.scale( - scale: isEnabled ? _scaleAnimation.value : 1.0, - child: Container( - width: double.infinity, - height: 56, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28), - gradient: isEnabled - ? LinearGradient( - colors: [ - theme.colorScheme.primary, - Color.fromARGB( - 255, - theme.colorScheme.primary.red + 20, - theme.colorScheme.primary.green + 20, - theme.colorScheme.primary.blue + 40, - ), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ) - : null, - color: - isEnabled ? null : theme.colorScheme.primary.withOpacity(0.5), - boxShadow: isEnabled - ? [ - BoxShadow( - color: theme.colorScheme.primary.withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ] - : null, - ), - child: MaterialButton( - onPressed: widget.onPressed, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), - padding: EdgeInsets.zero, - child: widget.isLoading - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - theme.colorScheme.onPrimary, - ), - ), - ) - : Text( - widget.text, - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/lib/views/wallet/coins_manager/coins_manager_switch_button.dart b/lib/views/wallet/coins_manager/coins_manager_switch_button.dart index 7fecd3e406..b56090f2ce 100644 --- a/lib/views/wallet/coins_manager/coins_manager_switch_button.dart +++ b/lib/views/wallet/coins_manager/coins_manager_switch_button.dart @@ -15,7 +15,7 @@ class CoinsManagerSwitchButton extends StatelessWidget { final state = context.watch().state; return UiPrimaryButton( - buttonKey: const Key('coins-manager-switch-button'), + key: const Key('coins-manager-switch-button'), prefix: state.isSwitching ? Padding( padding: const EdgeInsets.only(right: 8), diff --git a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart index e97fcd975c..d311f6a4d9 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_manage_section.dart @@ -65,7 +65,7 @@ class WalletManageSection extends StatelessWidget { ), SizedBox(width: 24), UiPrimaryButton( - buttonKey: const Key('add-assets-button'), + key: const Key('add-assets-button'), onPressed: () => _onAddAssetsPress(context), text: LocaleKeys.addAssets.tr(), height: 36, @@ -101,7 +101,7 @@ class WalletManageSection extends StatelessWidget { Spacer(), if (isAuthenticated) UiPrimaryButton( - buttonKey: const Key('asset-management-button'), + key: const Key('asset-management-button'), onPressed: () => _onAddAssetsPress(context), text: 'Asset management', height: 36, diff --git a/lib/views/wallets_manager/widgets/wallets_type_list.dart b/lib/views/wallets_manager/widgets/wallets_type_list.dart index 445c0aadcd..b7ac2191ad 100644 --- a/lib/views/wallets_manager/widgets/wallets_type_list.dart +++ b/lib/views/wallets_manager/widgets/wallets_type_list.dart @@ -3,15 +3,19 @@ import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/views/wallets_manager/widgets/wallet_type_list_item.dart'; class WalletsTypeList extends StatelessWidget { - const WalletsTypeList({Key? key, required this.onWalletTypeClick}) - : super(key: key); + const WalletsTypeList({super.key, required this.onWalletTypeClick}); final void Function(WalletType) onWalletTypeClick; + static const _excludedWalletTypes = [ + WalletType.hdwallet, + WalletType.metamask, + WalletType.keplr, + ]; @override Widget build(BuildContext context) { return Column( children: WalletType.values - .where((type) => type != WalletType.hdwallet) + .where((type) => !_excludedWalletTypes.contains(type)) .map((type) => Padding( padding: const EdgeInsets.only(bottom: 12.0), child: WalletTypeListItem( diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart index a524ecf3d1..a4e616518d 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_base_button.dart @@ -1,47 +1,78 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; -class UIBaseButton extends StatelessWidget { - const UIBaseButton({ - required this.isEnabled, - required this.child, - this.width, - this.height, - required this.border, - super.key, - }); - final bool isEnabled; - final double? width; - final double? height; - final BoxBorder? border; - final Widget child; +/// Button type enum to differentiate between different Material Design button types +enum ButtonType { + /// Text button with minimum width of 64dp, height of 36dp + text, - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: !isEnabled, - child: Opacity( - opacity: isEnabled ? 1 : 0.4, - child: Container( - constraints: _buildConstraints(), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(18)), - border: border, - ), - child: child, - ), - ), - ); - } + /// Contained or outlined button with minimum width of 88dp, height of 36dp + containedOrOutlined, + + /// Icon button with touch target of 48x48dp + icon +} + +/// Utility functions for buttons +class ButtonUtils { + /// Get the constraints for a button based on its type and configuration + static BoxConstraints getButtonConstraints({ + double? width, + double? height, + ButtonType buttonType = ButtonType.containedOrOutlined, + bool shouldEnforceMinimumSize = false, + bool expandToFillParent = false, + }) { + // For backward compatibility, if explicit dimensions are provided or + // we're not enforcing minimum size, use the provided dimensions directly + if (!shouldEnforceMinimumSize || (width != null && height != null)) { + if (width != null && height != null) { + return BoxConstraints.tightFor(width: width, height: height); + } else if (width != null) { + return BoxConstraints(minWidth: width); + } else if (height != null) { + return BoxConstraints(minHeight: height); + } else if (expandToFillParent) { + // If we're expanding to fill parent and no other constraints are set, + // make sure we at least apply minimum height + return const BoxConstraints(minHeight: 36); + } else { + return const BoxConstraints(); + } + } + + // Only apply Material Design minimum dimensions for flexible constructors + // when shouldEnforceMinimumSize is true + double minWidth; + double minHeight; + + // Determine minimum dimensions based on button type + switch (buttonType) { + case ButtonType.text: + minWidth = 64; + minHeight = 36; + break; + case ButtonType.containedOrOutlined: + minWidth = 88; + minHeight = 36; + break; + case ButtonType.icon: + minWidth = 48; + minHeight = 48; + break; + } - BoxConstraints _buildConstraints() { - if (width != null && height != null) { - return BoxConstraints.tightFor(width: width, height: height); - } else if (width != null) { - return BoxConstraints.tightFor(width: width); - } else if (height != null) { - return BoxConstraints.tightFor(height: height); + // For flexible constructors, use constraints that allow the button to grow + // beyond the minimum dimensions while still respecting the minimums + if (expandToFillParent) { + return BoxConstraints( + minWidth: double.infinity, + minHeight: minHeight, + ); } else { - return const BoxConstraints(); + return BoxConstraints( + minWidth: minWidth, + minHeight: minHeight, + ); } } } diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart index 433830bbca..7e16cca186 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_border_button.dart @@ -18,10 +18,16 @@ class UiBorderButton extends StatelessWidget { this.fontWeight = FontWeight.w700, this.fontSize = 14, this.textColor, + this.padding, }); - /// Constructor for a border button which inherits its size from the parent widget. - const UiBorderButton.minSize({ + /// Constructor for a border button which inherits its size from the parent + /// widget. See [UiPrimaryButton.flexible] for more details. + /// + /// The padding defaults to 16dp horizontal and 8dp vertical, following Material Design + /// specifications. The button maintains the minimum dimensions of an outlined button + /// (88dp width, 36dp height) unless explicitly overridden. + const UiBorderButton.flexible({ required this.text, required this.onPressed, super.key, @@ -35,6 +41,7 @@ class UiBorderButton extends StatelessWidget { this.fontWeight = FontWeight.w700, this.fontSize = 14, this.textColor, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), }) : width = null, height = null; @@ -52,6 +59,7 @@ class UiBorderButton extends StatelessWidget { final FontWeight fontWeight; final double fontSize; final Color? textColor; + final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -83,7 +91,7 @@ class UiBorderButton extends StatelessWidget { focusColor: secondaryColor.withValues(alpha: 0.2), splashColor: secondaryColor.withValues(alpha: 0.4), child: Padding( - padding: const EdgeInsets.fromLTRB(12, 6, 12, 6), + padding: padding ?? const EdgeInsets.fromLTRB(12, 6, 12, 6), child: Builder( builder: (context) { if (icon == null) { @@ -125,18 +133,35 @@ class UiBorderButton extends StatelessWidget { } BoxConstraints _buildConstraints() { - if (width != null && height != null) { - if (allowMultiline) { - return BoxConstraints.tightFor(width: width); + // Material Design minimum dimensions for outlined buttons + const double materialMinWidth = 88; + const double materialMinHeight = 36; + + // For fixed sizes (backward compatibility) + if (width != null || height != null) { + if (width != null && height != null) { + if (allowMultiline) { + return BoxConstraints(minWidth: width!); + } + return BoxConstraints(minWidth: width!, minHeight: height!); + } else if (width != null) { + return BoxConstraints(minWidth: width!); + } else if (height != null && !allowMultiline) { + return BoxConstraints(minHeight: height!); } - return BoxConstraints.tightFor(width: width, height: height); - } else if (width != null) { - return BoxConstraints.tightFor(width: width); - } else if (height != null && !allowMultiline) { - return BoxConstraints.tightFor(height: height); - } else { - return const BoxConstraints(); } + + // For flexible constructor - apply Material Design minimums + // but allow growing to fit content + if (width == null && height == null) { + return BoxConstraints( + minWidth: materialMinWidth, + minHeight: materialMinHeight, + ); + } + + // Fallback + return const BoxConstraints(); } } diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart index 06082b77db..0079153bf7 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_primary_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:app_theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:komodo_ui_kit/src/buttons/ui_base_button.dart'; @@ -5,12 +7,12 @@ import 'package:komodo_ui_kit/src/buttons/ui_base_button.dart'; class UiPrimaryButton extends StatefulWidget { /// Creates a primary button with the given properties. /// - /// NB! Prefer using the [UiPrimaryButton.minSize] constructor. The [width] + /// NB! Prefer using the [UiPrimaryButton.flexible] constructor. The [width] /// and [height] parameters will be deprecated in the future and the button - /// will have the same behavior as the [UiPrimaryButton.minSize] constructor. + /// will have the same behavior as the [UiPrimaryButton.flexible] constructor. + @Deprecated('Use UiPrimaryButton.flexible instead.') const UiPrimaryButton({ - required this.onPressed, - this.buttonKey, + this.onPressed, this.text = '', this.width = double.infinity, this.height = 48.0, @@ -24,6 +26,8 @@ class UiPrimaryButton extends StatefulWidget { this.child, this.padding, this.borderRadius, + this.optimisticEnabledDuration, + this.onOptimisticEnabledTimeout, super.key, }); @@ -34,9 +38,23 @@ class UiPrimaryButton extends StatefulWidget { /// content and use the minimum height needed. If you want it to take up the /// full width of its parent, wrap it in a [SizedBox] or a [Container] with /// `width: double.infinity`. - const UiPrimaryButton.minSize({ - required this.onPressed, - this.buttonKey, + /// + /// The padding defaults to 16dp horizontal and 8dp vertical, following Material Design + /// specifications. The button maintains the minimum dimensions of a contained button + /// (88dp width, 36dp height) unless explicitly overridden. + /// + /// For displaying text, use the [child] parameter with a [Text] widget. For example: + /// ```dart + /// UiPrimaryButton.flexible( + /// onPressed: () {}, + /// child: Text('Button Text'), + /// ) + /// ``` + const UiPrimaryButton.flexible({ + this.onPressed, + super.key, + // TODO: Remove this in the future in favor of using the `child` parameter + // to better follow the Flutter conventions. this.text = '', this.backgroundColor, this.textStyle, @@ -46,67 +64,123 @@ class UiPrimaryButton extends StatefulWidget { this.focusNode, this.shadowColor, this.child, - this.padding, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), this.borderRadius, - super.key, + this.optimisticEnabledDuration, + this.onOptimisticEnabledTimeout, }) : width = null, height = null; + /// The text to display on the button final String text; + + /// The style to apply to the button's text final TextStyle? textStyle; + + /// The width of the button. If null, the button will size itself to its content final double? width; + + /// The height of the button. If null, the button will size itself to its content final double? height; + + /// The background color of the button final Color? backgroundColor; + + /// A widget to display before the button's text final Widget? prefix; + + /// The padding to apply to the prefix widget final EdgeInsets? prefixPadding; - final Key? buttonKey; + + /// The border to apply to the button final BoxBorder? border; + + /// Called when the button is tapped final void Function()? onPressed; + + /// The focus node to use for the button final FocusNode? focusNode; + + /// The color of the button's shadow when focused final Color? shadowColor; + + /// A custom child widget to display instead of text final Widget? child; + + /// The padding to apply to the button's content final EdgeInsets? padding; + + /// The border radius of the button final double? borderRadius; + /// Duration for which a disabled button should appear enabled and show a loading + /// state if tapped. If [onPressed] becomes non-null during this period, it will + /// be called immediately. + /// + /// This creates an "optimistic UI" where buttons appear ready for interaction even + /// if they are technically disabled, improving perceived performance when the app + /// is waiting for some condition that will enable the button. + final Duration? optimisticEnabledDuration; + + /// Called when the [optimisticEnabledDuration] expires after the user taps + /// the button and if the button is still not enabled ([onPressed] is still null). + final VoidCallback? onOptimisticEnabledTimeout; + @override State createState() => _UiPrimaryButtonState(); } class _UiPrimaryButtonState extends State { bool _hasFocus = false; + bool _isLoading = false; + Timer? _loadingTimer; + @override - Widget build(BuildContext context) { - return UIBaseButton( - isEnabled: widget.onPressed != null, - width: widget.width, - height: widget.height, - border: widget.border, - child: ElevatedButton( - focusNode: widget.focusNode, - onFocusChange: (value) { - setState(() { - _hasFocus = value; - }); - }, - onPressed: widget.onPressed ?? () {}, - key: widget.buttonKey, - style: ElevatedButton.styleFrom( - shape: _shape, - shadowColor: _shadowColor, - elevation: 1, - backgroundColor: _backgroundColor, - foregroundColor: _foregroundColor, - padding: widget.padding, - ), - child: widget.child ?? - _ButtonContent( - text: widget.text, - textStyle: widget.textStyle, - prefix: widget.prefix, - prefixPadding: widget.prefixPadding, - ), - ), - ); + void didUpdateWidget(UiPrimaryButton oldWidget) { + super.didUpdateWidget(oldWidget); + // If the button becomes enabled while in loading state, immediately execute onPressed + if (widget.onPressed != null && oldWidget.onPressed == null && _isLoading) { + _loadingTimer?.cancel(); + setState(() => _isLoading = false); + widget.onPressed!(); + } + } + + @override + void dispose() { + _loadingTimer?.cancel(); + super.dispose(); + } + + void _handlePress() { + // If onPressed is available, execute it and clear any loading state + if (widget.onPressed != null) { + _loadingTimer?.cancel(); + if (_isLoading) { + setState(() => _isLoading = false); + } + widget.onPressed!(); + return; + } + + // Only show loading state if optimisticEnabledDuration is specified and not already loading + if (!_isLoading && widget.optimisticEnabledDuration != null) { + setState(() => _isLoading = true); + _loadingTimer?.cancel(); + _loadingTimer = Timer(widget.optimisticEnabledDuration!, () { + if (mounted) { + setState(() => _isLoading = false); + } + widget.onOptimisticEnabledTimeout?.call(); + }); + } + } + + /// Determines if the button should appear enabled, even if it's technically disabled + bool get _shouldAppearEnabled { + return widget.onPressed != null || + (widget.optimisticEnabledDuration != null) || + _isLoading; } Color get _backgroundColor { @@ -130,6 +204,85 @@ class _UiPrimaryButtonState extends State { borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 18)), ); + + @override + Widget build(BuildContext context) { + final shouldEnforceMinimumSize = + widget.width == null && widget.height == null; + final constraints = ButtonUtils.getButtonConstraints( + width: widget.width, + height: widget.height, + shouldEnforceMinimumSize: shouldEnforceMinimumSize, + expandToFillParent: widget.width == double.infinity, + ); + + // This is the key change - we determine if the button should appear enabled + // based on our new logic that includes optimisticEnabledDuration + final shouldAppearEnabled = _shouldAppearEnabled; + + // Create the base button widget + final button = ElevatedButton( + focusNode: widget.focusNode, + onFocusChange: (value) { + setState(() => _hasFocus = value); + }, + // Always allow the button to be pressed if it should appear enabled + onPressed: shouldAppearEnabled ? _handlePress : null, + style: ElevatedButton.styleFrom( + shape: _shape, + shadowColor: _shadowColor, + elevation: 1, + backgroundColor: _backgroundColor, + foregroundColor: _foregroundColor, + padding: widget.padding, + minimumSize: shouldEnforceMinimumSize + ? null + : Size( + constraints.minWidth > 0 ? constraints.minWidth : 0, + constraints.minHeight > 0 ? constraints.minHeight : 0, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: widget.border != null + ? BorderSide( + width: 1, + color: widget.border is Border + ? (widget.border as Border).top.color + : Theme.of(context).colorScheme.primary, + ) + : null, + ), + child: _isLoading + ? _buildLoadingIndicator() + : widget.child ?? + _ButtonContent( + text: widget.text, + textStyle: widget.textStyle, + prefix: widget.prefix, + prefixPadding: widget.prefixPadding, + ), + ); + + if (widget.width != null || widget.height != null) { + return SizedBox( + width: widget.width, + height: widget.height, + child: button, + ); + } + + return button; + } + + Widget _buildLoadingIndicator() { + return SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(_foregroundColor), + ), + ); + } } class _ButtonContent extends StatelessWidget { @@ -148,11 +301,11 @@ class _ButtonContent extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ if (prefix != null) - Padding( - padding: prefixPadding ?? const EdgeInsets.only(right: 12), + Container( + padding: prefixPadding, child: prefix!, ), Text(text, style: textStyle ?? _defaultTextStyle(context)), diff --git a/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart b/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart index 1e84485127..b739b1c292 100644 --- a/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart +++ b/packages/komodo_ui_kit/lib/src/buttons/ui_secondary_button.dart @@ -15,12 +15,18 @@ class UiSecondaryButton extends StatefulWidget { this.focusNode, this.shadowColor, this.child, + this.padding, + this.borderRadius, super.key, }); /// Constructor for a secondary button which inherits its size from the parent - /// widget. - const UiSecondaryButton.minSize({ + /// widget. See [UiPrimaryButton.flexible] for more details. + /// + /// The padding defaults to 16dp horizontal and 8dp vertical, following Material Design + /// specifications. The button maintains the minimum dimensions of an outlined button + /// (88dp width, 36dp height) unless explicitly overridden. + const UiSecondaryButton.flexible({ required this.onPressed, this.buttonKey, this.text = '', @@ -31,6 +37,8 @@ class UiSecondaryButton extends StatefulWidget { this.focusNode, this.shadowColor, this.child, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + this.borderRadius, super.key, }) : width = null, height = null; @@ -47,6 +55,8 @@ class UiSecondaryButton extends StatefulWidget { final FocusNode? focusNode; final Color? shadowColor; final Widget? child; + final EdgeInsets? padding; + final double? borderRadius; @override State createState() => _UiSecondaryButtonState(); @@ -54,42 +64,62 @@ class UiSecondaryButton extends StatefulWidget { class _UiSecondaryButtonState extends State { bool _hasFocus = false; + @override Widget build(BuildContext context) { - return UIBaseButton( - isEnabled: widget.onPressed != null, + final shouldEnforceMinimumSize = + widget.width == null && widget.height == null; + final constraints = ButtonUtils.getButtonConstraints( width: widget.width, height: widget.height, - border: widget.border, - child: ElevatedButton( - focusNode: widget.focusNode, - onFocusChange: (value) { - setState(() { - _hasFocus = value; - }); - }, - onPressed: widget.onPressed ?? () {}, - key: widget.buttonKey, - style: ElevatedButton.styleFrom( - shape: _shape, - side: BorderSide( - color: _borderColor, - width: 1, - ), - shadowColor: _shadowColor, - elevation: 1, - backgroundColor: Colors.transparent, - foregroundColor: _borderColor, - padding: EdgeInsets.zero, + shouldEnforceMinimumSize: shouldEnforceMinimumSize, + expandToFillParent: widget.width == double.infinity, + ); + + final buttonWidget = ElevatedButton( + focusNode: widget.focusNode, + onFocusChange: (value) { + setState(() => _hasFocus = value); + }, + onPressed: widget.onPressed, + key: widget.buttonKey, + style: ElevatedButton.styleFrom( + shape: _shape, + side: BorderSide( + color: _borderColor, + width: 1, ), - child: widget.child ?? - _ButtonContent( - text: widget.text, - textStyle: widget.textStyle, - prefix: widget.prefix, - ), + shadowColor: _shadowColor, + elevation: 1, + backgroundColor: Colors.transparent, + foregroundColor: _borderColor, + padding: widget.padding, + minimumSize: shouldEnforceMinimumSize + ? null + : Size( + constraints.minWidth > 0 ? constraints.minWidth : 0, + constraints.minHeight > 0 ? constraints.minHeight : 0, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), + child: widget.child ?? + _ButtonContent( + text: widget.text, + textStyle: widget.textStyle, + prefix: widget.prefix, + ), ); + + // Only apply size wrapper if needed + if (widget.width != null || widget.height != null) { + return SizedBox( + width: widget.width, + height: widget.height, + child: buttonWidget, + ); + } + + return buttonWidget; } Color get _borderColor { @@ -102,8 +132,9 @@ class _UiSecondaryButtonState extends State { : Colors.transparent; } - OutlinedBorder get _shape => const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(18)), + OutlinedBorder get _shape => RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(widget.borderRadius ?? 18)), ); } @@ -121,7 +152,7 @@ class _ButtonContent extends StatelessWidget { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ if (prefix != null) prefix!, Text(text, style: textStyle ?? _defaultTextStyle(context)), From f123da5916d1564756472f321fd37c3d169daa84 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Tue, 29 Apr 2025 16:42:47 +0200 Subject: [PATCH 09/13] fix(ui): Minor UI fixes --- lib/shared/utils/utils.dart | 19 +++++++++++++------ .../connect_wallet/connect_wallet_button.dart | 6 ++---- lib/views/fiat/fiat_page.dart | 2 +- .../coin_details_common_buttons.dart | 4 ++-- .../lib/src/images/coin_icon.dart | 14 +++++++++++--- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index f8e1b4f367..f94e823fa0 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -316,12 +316,19 @@ String abbr2Ticker(String abbr) { 'IBC_NUCLEUSTEST', ]; - // Join the suffixes with '|' to form the regex pattern - final String regexPattern = '(${filteredSuffixes.join('|')})'; - - final String ticker = abbr - .replaceAll(RegExp('-$regexPattern'), '') - .replaceAll(RegExp('_$regexPattern'), ''); + const List filteredPrefixes = ['NFT']; + + // Create regex patterns for both suffixes and prefixes + String suffixPattern = '(${filteredSuffixes.join('|')})'; + String prefixPattern = '(${filteredPrefixes.join('|')})'; + + String ticker = abbr + // Remove suffixes + .replaceAll(RegExp('-$suffixPattern'), '') + .replaceAll(RegExp('_$suffixPattern'), '') + // Remove prefixes + .replaceAll(RegExp('^$prefixPattern-'), '') + .replaceAll(RegExp('^${prefixPattern}_'), ''); _abbr2TickerCache[abbr] = ticker; return ticker; diff --git a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart index f14fc27d81..554fbf7e59 100644 --- a/lib/shared/widgets/connect_wallet/connect_wallet_button.dart +++ b/lib/shared/widgets/connect_wallet/connect_wallet_button.dart @@ -12,20 +12,18 @@ import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/dispatchers/popup_dispatcher.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/shared/ui/ui_primary_button.dart'; import 'package:web_dex/views/dex/dex_helpers.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_events_factory.dart'; import 'package:web_dex/views/wallets_manager/wallets_manager_wrapper.dart'; class ConnectWalletButton extends StatefulWidget { const ConnectWalletButton({ - Key? key, + super.key, required this.eventType, this.withText = true, this.withIcon = false, Size? buttonSize, - }) : buttonSize = buttonSize ?? const Size(double.infinity, 40), - super(key: key); + }) : buttonSize = buttonSize ?? const Size(double.infinity, 40); final Size buttonSize; final bool withIcon; final bool withText; diff --git a/lib/views/fiat/fiat_page.dart b/lib/views/fiat/fiat_page.dart index e59e94616b..8697a54f85 100644 --- a/lib/views/fiat/fiat_page.dart +++ b/lib/views/fiat/fiat_page.dart @@ -172,7 +172,7 @@ class FiatPageLayout extends StatelessWidget { class _TabContent extends StatelessWidget { const _TabContent({ required int activeTabIndex, - // ignore: unused_element + // ignore: unused_element, unused_element_parameter super.key, }) : _activeTabIndex = activeTabIndex; 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 cfa56eb969..ebbcfa6275 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 @@ -15,7 +15,6 @@ import 'package:web_dex/views/bitrefill/bitrefill_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/coin_addresses.dart'; import 'package:web_dex/views/wallet/coin_details/coin_details_info/contract_address_button.dart'; import 'package:web_dex/views/wallet/coin_details/coin_page_type.dart'; -import 'package:web_dex/views/wallet/coin_details/faucet/faucet_button.dart'; class CoinDetailsCommonButtons extends StatelessWidget { const CoinDetailsCommonButtons({ @@ -196,6 +195,7 @@ class CoinDetailsReceiveButton extends StatelessWidget { onPressed: coin.isSuspended || !hasAddresses ? null : () => _handleReceive(context), + optimisticEnabledDuration: const Duration(seconds: 5), text: LocaleKeys.receive.tr(), ); } @@ -379,4 +379,4 @@ class CoinDetailsMessageSigningButton extends StatelessWidget { text: LocaleKeys.signMessage.tr(), ); } -} \ No newline at end of file +} 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 2cbf40f50d..ac47e38aa9 100644 --- a/packages/komodo_ui_kit/lib/src/images/coin_icon.dart +++ b/packages/komodo_ui_kit/lib/src/images/coin_icon.dart @@ -306,11 +306,19 @@ String abbr2Ticker(String abbr) { 'IBC_NUCLEUSTEST', ]; - String regexPattern = '(${filteredSuffixes.join('|')})'; + const List filteredPrefixes = ['NFT']; + + // Create regex patterns for both suffixes and prefixes + String suffixPattern = '(${filteredSuffixes.join('|')})'; + String prefixPattern = '(${filteredPrefixes.join('|')})'; String ticker = abbr - .replaceAll(RegExp('-$regexPattern'), '') - .replaceAll(RegExp('_$regexPattern'), ''); + // Remove suffixes + .replaceAll(RegExp('-$suffixPattern'), '') + .replaceAll(RegExp('_$suffixPattern'), '') + // Remove prefixes + .replaceAll(RegExp('^$prefixPattern-'), '') + .replaceAll(RegExp('^${prefixPattern}_'), ''); _abbr2TickerCache[abbr] = ticker; return ticker; From 9b50c3fd5e1d25d30fd05080b7de30134d4f89cc Mon Sep 17 00:00:00 2001 From: WesleyAButt Date: Thu, 1 May 2025 05:24:32 +0200 Subject: [PATCH 10/13] feat(kyc): update message signing screen to match coin sub-page layout and show result in dialog - Replaced inline result display with a dialog using AlertDialog - Dialog content is now wrapped in a Column with mainAxisSize.min and 32px padding for better presentation - Refactored screen layout to include PageHeader (Back to Wallet) to match Send and Receive screens --- .../message_signing_screen.dart | 216 +++++++++++------- 1 file changed, 131 insertions(+), 85 deletions(-) diff --git a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart index 3f7ba8b7df..305af66071 100644 --- a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart +++ b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart @@ -10,6 +10,7 @@ import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/utils/utils.dart'; +import 'package:web_dex/views/common/page_header/page_header.dart'; class MessageSigningScreen extends StatefulWidget { final Coin coin; @@ -123,7 +124,6 @@ class _MessageSigningScreenContentState setState(() { isLoading = true; - signedMessage = null; errorMessage = null; }); @@ -135,13 +135,38 @@ class _MessageSigningScreenContentState ); setState(() { - signedMessage = signResult; isLoading = false; }); + + if (!mounted) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + contentPadding: EdgeInsets.all(32), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + EnhancedSignedMessageCard( + selectedAddress: selectedAddress!, + message: messageController.text, + signedMessage: signResult, + onCopyToClipboard: _copyToClipboard, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(LocaleKeys.ok.tr()), + ), + ], + ), + ); } catch (e) { setState(() { - errorMessage = LocaleKeys.failedToSignMessage.tr(args: [e.toString()]); isLoading = false; + errorMessage = LocaleKeys.failedToSignMessage.tr(args: [e.toString()]); }); } } @@ -167,97 +192,118 @@ class _MessageSigningScreenContentState final theme = Theme.of(context); final isSelectEnabled = pubkeys != null && pubkeys!.keys.length > 1; - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - theme.scaffoldBackgroundColor, - theme.scaffoldBackgroundColor.withOpacity(0.95), - ], + return Column( + children: [ + MessageSigningHeader( + title: LocaleKeys.signMessage.tr(), + onBackButtonPressed: widget.onBackButtonPressed, ), - ), - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - elevation: 2, - shape: RoundedRectangleBorder( - side: BorderSide( - color: theme.colorScheme.primary.withOpacity(0.2), - ), - borderRadius: BorderRadius.circular(16), + Expanded( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + theme.scaffoldBackgroundColor, + theme.scaffoldBackgroundColor.withOpacity(0.95), + ], ), - color: theme.colorScheme.surface.withOpacity(0.95), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Signing Address', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - if (errorMessage != null && !isLoadingAddresses) - ErrorMessageWidget(errorMessage: errorMessage!) - else - AddressSelectInput( - addresses: pubkeys?.keys ?? [], - selectedAddress: selectedAddress, - onAddressSelected: !isSelectEnabled - ? null - : (address) { - setState(() { - selectedAddress = address; - signedMessage = null; - errorMessage = null; - }); - }, - assetName: widget.asset.id.name, - ), - const SizedBox(height: 20), - Text( - LocaleKeys.messageToSign.tr(), - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 2, + shape: RoundedRectangleBorder( + side: BorderSide( + color: theme.colorScheme.primary.withOpacity(0.2), ), + borderRadius: BorderRadius.circular(16), ), - const SizedBox(height: 8), - EnhancedMessageInput( - controller: messageController, - hintText: LocaleKeys.enterMessage.tr(), - ), - const SizedBox(height: 24), - Center( - child: UiPrimaryButton( - text: LocaleKeys.signMessageButton.tr(), - onPressed: isLoading ? null : _signMessage, - width: double.infinity, - height: 56, + color: theme.colorScheme.surface.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Signing Address', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (errorMessage != null && !isLoadingAddresses) + ErrorMessageWidget(errorMessage: errorMessage!) + else + AddressSelectInput( + addresses: pubkeys?.keys ?? [], + selectedAddress: selectedAddress, + onAddressSelected: !isSelectEnabled + ? null + : (address) { + setState(() { + selectedAddress = address; + signedMessage = null; + errorMessage = null; + }); + }, + assetName: widget.asset.id.name, + ), + const SizedBox(height: 20), + Text( + LocaleKeys.messageToSign.tr(), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + EnhancedMessageInput( + controller: messageController, + hintText: LocaleKeys.enterMessage.tr(), + ), + const SizedBox(height: 24), + Center( + child: UiPrimaryButton( + text: LocaleKeys.signMessageButton.tr(), + onPressed: isLoading ? null : _signMessage, + width: double.infinity, + height: 56, + ), + ), + ], ), ), - ], - ), + ), + ], ), ), - if (signedMessage != null) ...[ - const SizedBox(height: 24), - EnhancedSignedMessageCard( - selectedAddress: selectedAddress!, - message: messageController.text, - signedMessage: signedMessage!, - onCopyToClipboard: _copyToClipboard, - ), - ], - ], + ), ), - ), + ], + ); + } +} + +class MessageSigningHeader extends StatelessWidget { + final VoidCallback? onBackButtonPressed; + final String title; + + const MessageSigningHeader({ + super.key, + required this.title, + this.onBackButtonPressed, + }); + + @override + Widget build(BuildContext context) { + return PageHeader( + title: title, + backText: LocaleKeys.backToWallet.tr(), + onBackButtonPressed: onBackButtonPressed, ); } } From 60b5b45d4ded12371e32185383697eaf0fa24fea Mon Sep 17 00:00:00 2001 From: WesleyAButt Date: Fri, 16 May 2025 09:38:49 +0200 Subject: [PATCH 11/13] feat(message-signing): implement multi-step signing flow with confirmation and result states - Introduced a complete message signing flow using Bloc architecture - Added MessageSigningBloc, events, and state with Equatable integration - Implemented multi-step UI: - Signing form with address selector, message input, QR, and paste actions - Confirmation step with address and message preview and acknowledgment checkbox - Result card showing signed message and sharing options - Extracted form, confirmation, and result views into dedicated widgets for clarity - Enhanced UI styling with unified card designs and split field containers --- assets/translations/en.json | 6 +- .../message_signing/message_signing_bloc.dart | 101 +++ .../message_signing_event.dart | 28 + .../message_signing_state.dart | 61 ++ lib/generated/codegen_loader.g.dart | 4 + .../Widgets/message_signed_result.dart | 162 +++++ .../Widgets/message_signing_confirmation.dart | 161 +++++ .../Widgets/message_signing_form.dart | 285 ++++++++ .../Widgets/message_signing_header.dart | 24 + .../message_signing_screen.dart | 683 +++--------------- 10 files changed, 912 insertions(+), 603 deletions(-) create mode 100644 lib/bloc/message_signing/message_signing_bloc.dart create mode 100644 lib/bloc/message_signing/message_signing_event.dart create mode 100644 lib/bloc/message_signing/message_signing_state.dart create mode 100644 lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart create mode 100644 lib/views/wallet/coin_details/message_signing/Widgets/message_signing_confirmation.dart create mode 100644 lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart create mode 100644 lib/views/wallet/coin_details/message_signing/Widgets/message_signing_header.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index da46feb07e..5159eb94a8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -677,5 +677,9 @@ "customNetworkFee": "Custom Network Fee", "previewWithdrawal": "Preview Withdrawal", "createNewAddress": "Create New Address", - "chart": "Chart" + "chart": "Chart", + "confirmMessageSigning" : "Confirm Message Signing", + "messageSigningWarning" : "Only sign messages from trusted sources.", + "messageSigningCheckboxText" : "I understand that signing proves ownership of this address.", + "messageSigned" : "Message signed" } \ No newline at end of file diff --git a/lib/bloc/message_signing/message_signing_bloc.dart b/lib/bloc/message_signing/message_signing_bloc.dart new file mode 100644 index 0000000000..306dba711c --- /dev/null +++ b/lib/bloc/message_signing/message_signing_bloc.dart @@ -0,0 +1,101 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_event.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_state.dart'; + +class MessageSigningBloc + extends Bloc { + final KomodoDefiSdk sdk; + + MessageSigningBloc(this.sdk) : super(MessageSigningState.initial()) { + on(_onLoadAddresses); + on(_onSelectAddress); + on(_onSubmitMessage); + on(_onRequestConfirmation); + on(_onCancelConfirmation); + } + + Future _onLoadAddresses( + MessageSigningAddressesRequested event, + Emitter emit, + ) async { + emit(state.copyWith( + status: MessageSigningStatus.loading, errorMessage: null)); + + try { + final result = await sdk.pubkeys.getPubkeys(event.asset); + final keys = result.keys; + + emit(state.copyWith( + addresses: keys, + selected: keys.isNotEmpty ? keys.first : null, + status: MessageSigningStatus.ready, + )); + } catch (e) { + emit(state.copyWith( + status: MessageSigningStatus.failure, + errorMessage: e.toString(), + )); + } + } + + void _onSelectAddress( + MessageSigningAddressSelected event, + Emitter emit, + ) { + emit(state.copyWith(selected: event.address)); + } + + void _onRequestConfirmation( + MessageSigningInputConfirmed event, + Emitter emit, + ) { + emit(state.copyWith(status: MessageSigningStatus.confirming)); + } + + void _onCancelConfirmation( + MessageSigningConfirmationCancelled event, + Emitter emit, + ) { + emit(state.copyWith(status: MessageSigningStatus.ready)); + } + + Future _onSubmitMessage( + MessageSigningFormSubmitted event, + Emitter emit, + ) async { + final address = state.selected; + if (address == null) { + emit(state.copyWith( + errorMessage: LocaleKeys.pleaseSelectAddress.tr(), + status: MessageSigningStatus.failure, + )); + return; + } + + emit(state.copyWith( + status: MessageSigningStatus.submitting, + errorMessage: null, + )); + + try { + final signed = await sdk.messageSigning.signMessage( + coin: event.coinAbbr, + address: address.address, + message: event.message, + ); + + emit(state.copyWith( + signedMessage: signed, + status: MessageSigningStatus.success, + )); + } catch (e) { + emit(state.copyWith( + errorMessage: LocaleKeys.failedToSignMessage.tr(args: [e.toString()]), + status: MessageSigningStatus.failure, + )); + } + } +} diff --git a/lib/bloc/message_signing/message_signing_event.dart b/lib/bloc/message_signing/message_signing_event.dart new file mode 100644 index 0000000000..475f07cfb4 --- /dev/null +++ b/lib/bloc/message_signing/message_signing_event.dart @@ -0,0 +1,28 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +sealed class MessageSigningEvent {} + +class MessageSigningAddressesRequested extends MessageSigningEvent { + final Asset asset; + + MessageSigningAddressesRequested(this.asset); +} + +class MessageSigningAddressSelected extends MessageSigningEvent { + final PubkeyInfo address; + + MessageSigningAddressSelected(this.address); +} + +class MessageSigningFormSubmitted extends MessageSigningEvent { + final String message; + final String coinAbbr; + + MessageSigningFormSubmitted({ + required this.message, + required this.coinAbbr, + }); +} + +class MessageSigningInputConfirmed extends MessageSigningEvent {} +class MessageSigningConfirmationCancelled extends MessageSigningEvent {} \ No newline at end of file diff --git a/lib/bloc/message_signing/message_signing_state.dart b/lib/bloc/message_signing/message_signing_state.dart new file mode 100644 index 0000000000..3ef8c68cca --- /dev/null +++ b/lib/bloc/message_signing/message_signing_state.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +enum MessageSigningStatus { + initial, + loading, + ready, + confirming, + submitting, + success, + failure, +} + +class MessageSigningState extends Equatable { + final List addresses; + final PubkeyInfo? selected; + final String? signedMessage; + final String? errorMessage; + final MessageSigningStatus status; + + const MessageSigningState({ + required this.addresses, + required this.selected, + required this.signedMessage, + required this.errorMessage, + required this.status, + }); + + factory MessageSigningState.initial() => const MessageSigningState( + addresses: [], + selected: null, + signedMessage: null, + errorMessage: null, + status: MessageSigningStatus.initial, + ); + + MessageSigningState copyWith({ + List? addresses, + PubkeyInfo? selected, + String? signedMessage, + String? errorMessage, + MessageSigningStatus? status, + }) { + return MessageSigningState( + addresses: addresses ?? this.addresses, + selected: selected ?? this.selected, + signedMessage: signedMessage ?? this.signedMessage, + errorMessage: errorMessage ?? this.errorMessage, + status: status ?? this.status, + ); + } + + @override + List get props => [ + addresses, + selected, + signedMessage, + errorMessage, + status, + ]; +} \ No newline at end of file diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index ff4afee21c..a3add72b9b 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -675,5 +675,9 @@ abstract class LocaleKeys { static const previewWithdrawal = 'previewWithdrawal'; static const createNewAddress = 'createNewAddress'; static const chart = 'chart'; + static const confirmMessageSigning = 'confirmMessageSigning'; + static const messageSigningWarning = 'messageSigningWarning'; + static const messageSigningCheckboxText = 'messageSigningCheckboxText'; + static const messageSigned = 'messageSigned'; } diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart new file mode 100644 index 0000000000..f1b05a870d --- /dev/null +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class MessageSignedResult extends StatelessWidget { + final ThemeData theme; + final PubkeyInfo selected; + final String message; + final String signedMessage; + + const MessageSignedResult({ + super.key, + required this.theme, + required this.selected, + required this.message, + required this.signedMessage, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: theme.colorScheme.primary.withOpacity(0.2), + ), + ), + color: theme.colorScheme.surface.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + LocaleKeys.messageSigned.tr(), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + const SizedBox(height: 24), + Container( + margin: const EdgeInsets.symmetric(vertical: 15.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + border: Border.all(color: theme.colorScheme.primary, width: 4), + ), + child: Icon( + Icons.check_rounded, + size: 66, + color: theme.colorScheme.primary, + ), + ), + Center( + child: Text( + 'Address - ${selected.address}', + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.6), + ), + ), + ), + const SizedBox(height: 24), + _buildStyledSection(context, + content: message, + icon: Icons.chat_bubble_outline, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(12)), + showTopBorder: true), + _buildStyledSection( + context, + content: signedMessage, + icon: Icons.vpn_key_outlined, + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(12)), + showTopBorder: false, + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: UiSecondaryButton( + text: 'Share', + onPressed: () { + Clipboard.setData(ClipboardData(text: signedMessage)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied to clipboard')), + ); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UiPrimaryButton( + text: 'Copy', + onPressed: () { + // TODO: Add share logic + }, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStyledSection( + BuildContext context, { + required String content, + required IconData icon, + required BorderRadius borderRadius, + bool showTopBorder = true, + }) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: theme.colorScheme.surface.withOpacity(0.7), + border: Border( + top: showTopBorder + ? BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)) + : BorderSide.none, + bottom: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + left: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + right: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: theme.colorScheme.primary), + const SizedBox(width: 12), + Expanded( + child: SelectableText( + content, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_confirmation.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_confirmation.dart new file mode 100644 index 0000000000..9713ac3525 --- /dev/null +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_confirmation.dart @@ -0,0 +1,161 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_bloc.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_event.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; + +class MessageSigningConfirmationCard extends StatelessWidget { + final ThemeData theme; + final String message; + final String coinAbbr; + final bool understood; + final VoidCallback onCancel; + final ValueChanged onUnderstoodChanged; + + const MessageSigningConfirmationCard({ + super.key, + required this.theme, + required this.message, + required this.coinAbbr, + required this.understood, + required this.onCancel, + required this.onUnderstoodChanged, + }); + + @override + Widget build(BuildContext context) { + final selected = context.read().state.selected; + + return Card( + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: theme.colorScheme.primary.withOpacity(0.2), + ), + ), + color: theme.colorScheme.surface.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + LocaleKeys.confirmMessageSigning.tr(), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + _buildStyledSection(context, + content: selected?.address ?? '', + icon: Icons.account_balance_wallet, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(12)), + showTopBorder: true), + _buildStyledSection(context, + content: message, + icon: Icons.chat_bubble_outline, + borderRadius: + const BorderRadius.vertical(bottom: Radius.circular(12)), + showTopBorder: false), + const SizedBox(height: 24), + Text( + LocaleKeys.messageSigningWarning.tr(), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.error, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + UiCheckbox( + value: understood, + onChanged: (val) => onUnderstoodChanged(val), + text: LocaleKeys.messageSigningCheckboxText.tr(), + textColor: theme.textTheme.bodyMedium?.color, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: UiSecondaryButton( + text: LocaleKeys.cancel.tr(), + onPressed: onCancel, + ), + ), + const SizedBox(width: 12), + Expanded( + child: UiPrimaryButton( + text: LocaleKeys.confirm.tr(), + onPressed: understood + ? () { + context.read().add( + MessageSigningFormSubmitted( + message: message, + coinAbbr: coinAbbr, + ), + ); + onCancel(); + } + : null, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStyledSection( + BuildContext context, { + required String content, + required IconData icon, + required BorderRadius borderRadius, + bool showTopBorder = true, + }) { + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: theme.colorScheme.surface.withOpacity(0.7), + border: Border( + top: showTopBorder + ? BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)) + : BorderSide.none, + bottom: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + left: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + right: BorderSide(color: theme.colorScheme.outline.withOpacity(0.2)), + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, size: 20, color: theme.colorScheme.primary), + const SizedBox(width: 12), + Expanded( + child: SelectableText( + content, + style: theme.textTheme.bodyMedium?.copyWith( + fontFamily: 'monospace', + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart new file mode 100644 index 0000000000..99d153a205 --- /dev/null +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_bloc.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_event.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_state.dart'; +import 'package:web_dex/model/coin.dart'; +import 'package:komodo_ui_kit/komodo_ui_kit.dart'; + +class MessageSigningForm extends StatelessWidget { + final MessageSigningState state; + final ThemeData theme; + final Coin coin; + final Asset asset; + final TextEditingController messageController; + final VoidCallback onSignPressed; + + const MessageSigningForm({ + super.key, + required this.state, + required this.theme, + required this.coin, + required this.asset, + required this.messageController, + required this.onSignPressed, + }); + + @override + Widget build(BuildContext context) { + final isSelectEnabled = state.addresses.length > 1; + final isSubmitting = state.status == MessageSigningStatus.submitting; + final hasError = state.status == MessageSigningStatus.failure && + state.errorMessage != null; + + return Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: theme.colorScheme.primary.withOpacity(0.2), + ), + ), + color: theme.colorScheme.surface.withOpacity(0.95), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Signing Address', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + if (hasError) + ErrorMessageWidget(errorMessage: state.errorMessage!) + else if (state.addresses.isEmpty) + const SizedBox() + else + Builder( + builder: (context) { + final selected = state.selected ?? state.addresses.first; + return AddressSelectInput( + addresses: state.addresses, + selectedAddress: selected, + onAddressSelected: isSelectEnabled + ? (address) { + if (address != null) { + context.read().add( + MessageSigningAddressSelected(address), + ); + } + } + : null, + assetName: asset.id.name, + ); + }, + ), + const SizedBox(height: 20), + Text( + LocaleKeys.messageToSign.tr(), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: EnhancedMessageInput( + controller: messageController, + hintText: LocaleKeys.enterMessage.tr(), + showCopyButton: true, + onCopyPressed: () async { + final data = await Clipboard.getData('text/plain'); + if (data?.text != null) { + messageController.text = data!.text!; + } + }, + trailingIcon: IconButton.filled( + icon: const Icon(Icons.qr_code_scanner, size: 16), + splashRadius: 18, + color: theme.textTheme.bodyMedium!.color, + onPressed: () async { + final result = await QrCodeReaderOverlay.show(context); + if (result != null) { + messageController.text = result; + } + }, + ), + ), + ), + ], + ), + const SizedBox(height: 24), + UiPrimaryButton( + text: LocaleKeys.signMessageButton.tr(), + onPressed: isSubmitting ? null : onSignPressed, + width: double.infinity, + height: 56, + ), + ], + ), + ), + ); + } +} + +class ErrorMessageWidget extends StatelessWidget { + final String errorMessage; + + const ErrorMessageWidget({ + super.key, + required this.errorMessage, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + errorMessage, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ); + } +} + +class EnhancedMessageInput extends StatefulWidget { + final TextEditingController controller; + final String hintText; + final bool showCopyButton; + final VoidCallback? onCopyPressed; + final Widget? trailingIcon; + + const EnhancedMessageInput({ + required this.controller, + required this.hintText, + this.showCopyButton = false, + this.onCopyPressed, + this.trailingIcon, + super.key, + }); + + @override + State createState() => _EnhancedMessageInputState(); +} + +class _EnhancedMessageInputState extends State { + late int charCount = widget.controller.text.length; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_updateCharCount); + } + + void _updateCharCount() => + setState(() => charCount = widget.controller.text.length); + + @override + void dispose() { + widget.controller.removeListener(_updateCharCount); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.shadow.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextField( + controller: widget.controller, + decoration: InputDecoration( + hintText: widget.hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.5), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: theme.colorScheme.primary, + width: 2, + ), + ), + contentPadding: const EdgeInsets.all(16), + fillColor: + theme.colorScheme.surfaceVariant.withOpacity(0.3), + filled: true, + ), + style: theme.textTheme.bodyMedium?.copyWith( + letterSpacing: 0.5, + height: 1.5, + ), + maxLines: 4, + cursorColor: theme.colorScheme.primary, + ), + ), + const SizedBox(width: 8), + Container( + width: 48, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (widget.trailingIcon != null) widget.trailingIcon!, + if (widget.showCopyButton) + IconButton.filled( + icon: const Icon(Icons.content_paste, size: 16), + splashRadius: 18, + color: theme.textTheme.bodyMedium!.color, + onPressed: widget.onCopyPressed, + ), + ], + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, right: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + '$charCount characters', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_header.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_header.dart new file mode 100644 index 0000000000..3406c8e17b --- /dev/null +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_header.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; +import 'package:web_dex/views/common/page_header/page_header.dart'; + +class MessageSigningHeader extends StatelessWidget { + final VoidCallback? onBackButtonPressed; + final String title; + + const MessageSigningHeader({ + super.key, + required this.title, + this.onBackButtonPressed, + }); + + @override + Widget build(BuildContext context) { + return PageHeader( + title: title, + backText: LocaleKeys.backToWallet.tr(), + onBackButtonPressed: onBackButtonPressed, + ); + } +} diff --git a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart index 305af66071..5c9d873683 100644 --- a/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart +++ b/lib/views/wallet/coin_details/message_signing/message_signing_screen.dart @@ -1,16 +1,17 @@ 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: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_kit/komodo_ui_kit.dart'; -import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_bloc.dart'; -import 'package:web_dex/bloc/coin_addresses/bloc/coin_addresses_event.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/shared/utils/utils.dart'; -import 'package:web_dex/views/common/page_header/page_header.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_event.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_state.dart'; +import 'package:web_dex/bloc/message_signing/message_signing_bloc.dart'; +import 'package:web_dex/views/wallet/coin_details/message_signing/Widgets/message_signing_confirmation.dart'; +import 'package:web_dex/views/wallet/coin_details/message_signing/Widgets/message_signing_header.dart'; +import 'package:web_dex/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart'; +import 'package:web_dex/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart'; class MessageSigningScreen extends StatefulWidget { final Coin coin; @@ -27,21 +28,20 @@ class MessageSigningScreen extends StatefulWidget { } class _MessageSigningScreenState extends State { - late Asset asset; + late final Asset asset; @override void initState() { super.initState(); - asset = widget.coin.toSdkAsset(context.sdk); + final sdk = context.read(); + asset = widget.coin.toSdkAsset(sdk); } @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CoinAddressesBloc( - context.sdk, - widget.coin.abbr, - )..add(const LoadAddressesEvent()), + create: (context) => MessageSigningBloc(context.read()) + ..add(MessageSigningAddressesRequested(asset)), child: _MessageSigningScreenContent( coin: widget.coin, asset: asset, @@ -69,116 +69,37 @@ class _MessageSigningScreenContent extends StatefulWidget { class _MessageSigningScreenContentState extends State<_MessageSigningScreenContent> { - PubkeyInfo? selectedAddress; final TextEditingController messageController = TextEditingController(); - String? signedMessage; - String? errorMessage; - bool isLoading = false; - bool isLoadingAddresses = true; - AssetPubkeys? pubkeys; + bool showConfirmation = false; + bool understood = false; @override void initState() { super.initState(); - _loadAddresses(); } - Future _loadAddresses() async { - setState(() { - isLoadingAddresses = true; - }); - - try { - final addresses = await context.sdk.pubkeys.getPubkeys(widget.asset); - - setState(() { - pubkeys = addresses; - isLoadingAddresses = false; - if (addresses.keys.isNotEmpty) { - selectedAddress = addresses.keys.first; - } - }); - } catch (e) { - setState(() { - isLoadingAddresses = false; - errorMessage = - LocaleKeys.failedToLoadAddresses.tr(args: [e.toString()]); - }); - } - } + void _handleSignMessage(BuildContext context) { + final message = messageController.text.trim(); + final selected = context.read().state.selected; - Future _signMessage() async { - if (selectedAddress == null) { - setState(() { - errorMessage = LocaleKeys.pleaseSelectAddress.tr(); - }); + if (selected == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(LocaleKeys.pleaseSelectAddress.tr())), + ); return; } - if (messageController.text.isEmpty) { - setState(() { - errorMessage = LocaleKeys.pleaseEnterMessage.tr(); - }); + if (message.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(LocaleKeys.pleaseEnterMessage.tr())), + ); return; } setState(() { - isLoading = true; - errorMessage = null; + showConfirmation = true; + understood = false; }); - - try { - final signResult = await context.sdk.messageSigning.signMessage( - coin: widget.coin.abbr, - address: selectedAddress!.address, - message: messageController.text, - ); - - setState(() { - isLoading = false; - }); - - if (!mounted) return; - - showDialog( - context: context, - builder: (context) => AlertDialog( - contentPadding: EdgeInsets.all(32), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - EnhancedSignedMessageCard( - selectedAddress: selectedAddress!, - message: messageController.text, - signedMessage: signResult, - onCopyToClipboard: _copyToClipboard, - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(LocaleKeys.ok.tr()), - ), - ], - ), - ); - } catch (e) { - setState(() { - isLoading = false; - errorMessage = LocaleKeys.failedToSignMessage.tr(args: [e.toString()]); - }); - } - } - - void _copyToClipboard(String text) { - Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(LocaleKeys.clipBoard.tr()), - duration: const Duration(seconds: 2), - ), - ); } @override @@ -190,7 +111,6 @@ class _MessageSigningScreenContentState @override Widget build(BuildContext context) { final theme = Theme.of(context); - final isSelectEnabled = pubkeys != null && pubkeys!.keys.length > 1; return Column( children: [ @@ -210,504 +130,63 @@ class _MessageSigningScreenContentState ], ), ), - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( - elevation: 2, - shape: RoundedRectangleBorder( - side: BorderSide( - color: theme.colorScheme.primary.withOpacity(0.2), - ), - borderRadius: BorderRadius.circular(16), - ), - color: theme.colorScheme.surface.withOpacity(0.95), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Signing Address', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - if (errorMessage != null && !isLoadingAddresses) - ErrorMessageWidget(errorMessage: errorMessage!) - else - AddressSelectInput( - addresses: pubkeys?.keys ?? [], - selectedAddress: selectedAddress, - onAddressSelected: !isSelectEnabled - ? null - : (address) { - setState(() { - selectedAddress = address; - signedMessage = null; - errorMessage = null; - }); - }, - assetName: widget.asset.id.name, - ), - const SizedBox(height: 20), - Text( - LocaleKeys.messageToSign.tr(), - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - EnhancedMessageInput( - controller: messageController, - hintText: LocaleKeys.enterMessage.tr(), - ), - const SizedBox(height: 24), - Center( - child: UiPrimaryButton( - text: LocaleKeys.signMessageButton.tr(), - onPressed: isLoading ? null : _signMessage, - width: double.infinity, - height: 56, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} - -class MessageSigningHeader extends StatelessWidget { - final VoidCallback? onBackButtonPressed; - final String title; - - const MessageSigningHeader({ - super.key, - required this.title, - this.onBackButtonPressed, - }); - - @override - Widget build(BuildContext context) { - return PageHeader( - title: title, - backText: LocaleKeys.backToWallet.tr(), - onBackButtonPressed: onBackButtonPressed, - ); - } -} - -// Enhanced Signed Message Card -class EnhancedSignedMessageCard extends StatelessWidget { - final PubkeyInfo selectedAddress; - final String message; - final String signedMessage; - final Function(String) onCopyToClipboard; - - const EnhancedSignedMessageCard({ - super.key, - required this.selectedAddress, - required this.message, - required this.signedMessage, - required this.onCopyToClipboard, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Card( - elevation: 8, - shadowColor: theme.colorScheme.shadow.withOpacity(0.3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - gradient: LinearGradient( - colors: [ - theme.colorScheme.surface, - theme.colorScheme.surfaceVariant.withOpacity(0.5), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - _buildSectionHeader(context, LocaleKeys.address.tr()), - _buildContentSection( - context, - selectedAddress.address, - icon: Icons.account_balance_wallet, - onCopy: () => onCopyToClipboard(selectedAddress.address), - ), - const SizedBox(height: 24), - _buildSectionHeader(context, LocaleKeys.message.tr()), - _buildContentSection( - context, - message, - icon: Icons.chat_bubble_outline, - onCopy: () => onCopyToClipboard(message), - ), - const SizedBox(height: 24), - _buildSectionHeader(context, LocaleKeys.signedMessage.tr()), - _buildContentSection( - context, - signedMessage, - icon: Icons.vpn_key_outlined, - onCopy: () => onCopyToClipboard(signedMessage), - isSignature: true, - ), - const SizedBox(height: 24), - _buildCopyAllButton(context), - ], - ), - ), - ), - ); - } - - Widget _buildSectionHeader(BuildContext context, String title) { - final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: Row( - children: [ - Container( - width: 4, - height: 20, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 8), - Text( - title, - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, - letterSpacing: 0.5, - ), - ), - ], - ), - ); - } - - Widget _buildContentSection( - BuildContext context, - String content, { - required IconData icon, - required VoidCallback onCopy, - bool isSignature = false, - }) { - final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: theme.colorScheme.surface.withOpacity(0.7), - border: Border.all( - color: theme.colorScheme.outline.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: theme.colorScheme.shadow.withOpacity(0.05), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: CopyableTextField( - content: content, - onCopy: onCopy, - icon: icon, - isSignature: isSignature, - ), - ); - } - - Widget _buildCopyAllButton(BuildContext context) { - return UiPrimaryButton.flexible( - child: Text(LocaleKeys.copyAllDetails.tr()), - onPressed: () => onCopyToClipboard( - 'Address:\n${selectedAddress.address}\n\n' - 'Message:\n$message\n\n' - 'Signature:\n$signedMessage', - ), - ); - } -} - -// Copyable Text Field Component -class CopyableTextField extends StatefulWidget { - final String content; - final VoidCallback onCopy; - final IconData icon; - final bool isSignature; - - const CopyableTextField({ - super.key, - required this.content, - required this.onCopy, - required this.icon, - this.isSignature = false, - }); - - @override - State createState() => _CopyableTextFieldState(); -} - -class _CopyableTextFieldState extends State - with SingleTickerProviderStateMixin { - bool _isCopied = false; - late AnimationController _controller; - late Animation _fadeAnimation; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 1500), - vsync: this, - ); - _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ), - ); - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() { - _isCopied = false; - }); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - void _copyWithAnimation() { - widget.onCopy(); - setState(() { - _isCopied = true; - }); - _controller.reset(); - _controller.forward(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return InkWell( - onTap: _copyWithAnimation, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - widget.icon, - size: 20, - color: theme.colorScheme.primary.withOpacity(0.7), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - widget.content, - style: theme.textTheme.bodyMedium?.copyWith( - letterSpacing: widget.isSignature ? 0 : 0.5, - fontFamily: widget.isSignature ? 'monospace' : null, - height: 1.5, - ), - ), - ], - ), - ), - AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Opacity( - opacity: _isCopied ? _fadeAnimation.value : 1.0, - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: _isCopied - ? theme.colorScheme.primary.withOpacity(0.1) - : null, - borderRadius: BorderRadius.circular(6), - ), - child: Center( - child: Icon( - _isCopied ? Icons.check : Icons.copy, - size: 16, - color: _isCopied - ? theme.colorScheme.primary - : theme.colorScheme.onPrimaryContainer, - ), - ), - ), + child: BlocBuilder( + builder: (context, state) { + final theme = Theme.of(context); + Widget content; + + if (state.signedMessage != null) { + final selected = state.selected ?? + (state.addresses.isNotEmpty ? state.addresses.first : null); + + content = selected != null + ? MessageSignedResult( + theme: theme, + selected: selected, + message: messageController.text, + signedMessage: state.signedMessage!, + ) + : const SizedBox(); + } else if (showConfirmation) { + content = MessageSigningConfirmationCard( + theme: theme, + message: messageController.text.trim(), + coinAbbr: widget.coin.abbr, + understood: understood, + onCancel: () { + setState(() { + showConfirmation = false; + understood = false; + }); + }, + onUnderstoodChanged: (val) { + setState(() => understood = val); + }, ); - }, - ), - ], - ), - ), - ); - } -} - -// Error Message Widget -class ErrorMessageWidget extends StatelessWidget { - final String errorMessage; - - const ErrorMessageWidget({ - super.key, - required this.errorMessage, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - errorMessage, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.error, - ), - ), - ); - } -} - -// Enhanced Address Selection Widget - -// Enhanced Message Input Widget -class EnhancedMessageInput extends StatefulWidget { - final TextEditingController controller; - final String hintText; - - const EnhancedMessageInput({ - super.key, - required this.controller, - required this.hintText, - }); - - @override - State createState() => _EnhancedMessageInputState(); -} - -class _EnhancedMessageInputState extends State { - late int charCount = 0; - - @override - void initState() { - super.initState(); - charCount = widget.controller.text.length; - widget.controller.addListener(_updateCharCount); - } - - void _updateCharCount() { - setState(() { - charCount = widget.controller.text.length; - }); - } - - @override - void dispose() { - widget.controller.removeListener(_updateCharCount); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: theme.colorScheme.shadow.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - controller: widget.controller, - decoration: InputDecoration( - hintText: widget.hintText, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: theme.colorScheme.outline.withOpacity(0.5), - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: theme.colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.all(16), - fillColor: theme.colorScheme.surfaceVariant.withOpacity(0.3), - filled: true, - ), - style: theme.textTheme.bodyMedium?.copyWith( - letterSpacing: 0.5, - height: 1.5, - ), - maxLines: 4, - cursorColor: theme.colorScheme.primary, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8, right: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - '$charCount characters', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withOpacity(0.6), + } else { + content = MessageSigningForm( + state: state, + theme: theme, + coin: widget.coin, + asset: widget.asset, + messageController: messageController, + onSignPressed: () => _handleSignMessage(context), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + content, + ], ), - ), - ], + ); + }), ), ), ], ); } -} +} \ No newline at end of file From 7fc2867b8237424d4b46bb46940a84a3ac323ff3 Mon Sep 17 00:00:00 2001 From: WesleyAButt Date: Mon, 19 May 2025 09:33:21 +0200 Subject: [PATCH 12/13] chore(message-signing): apply new form UI changes and localize missing strings - Applied new UI changes to message signing form - Localized text strings based of feedback --- assets/translations/en.json | 4 +- lib/generated/codegen_loader.g.dart | 2 + .../Widgets/message_signed_result.dart | 2 +- .../Widgets/message_signing_form.dart | 48 ++++++++++--------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 5159eb94a8..13bb50e4c7 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -681,5 +681,7 @@ "confirmMessageSigning" : "Confirm Message Signing", "messageSigningWarning" : "Only sign messages from trusted sources.", "messageSigningCheckboxText" : "I understand that signing proves ownership of this address.", - "messageSigned" : "Message signed" + "messageSigned" : "Message signed", + "addressLabel" : "Address - ", + "signingAddress" : "Signing address" } \ No newline at end of file diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index a3add72b9b..2a525bb2c0 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -679,5 +679,7 @@ abstract class LocaleKeys { static const messageSigningWarning = 'messageSigningWarning'; static const messageSigningCheckboxText = 'messageSigningCheckboxText'; static const messageSigned = 'messageSigned'; + static const addressLabel = 'addressLabel'; + static const signingAddress = 'signingAddress'; } diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart index f1b05a870d..2e820d2f70 100644 --- a/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signed_result.dart @@ -57,7 +57,7 @@ class MessageSignedResult extends StatelessWidget { ), Center( child: Text( - 'Address - ${selected.address}', + LocaleKeys.addressLabel.tr(args: [selected.address]), textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( fontFamily: 'monospace', diff --git a/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart index 99d153a205..21bdf12aff 100644 --- a/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart +++ b/lib/views/wallet/coin_details/message_signing/Widgets/message_signing_form.dart @@ -51,7 +51,7 @@ class MessageSigningForm extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Signing Address', + LocaleKeys.signingAddress.tr(), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), @@ -103,27 +103,32 @@ class MessageSigningForm extends StatelessWidget { messageController.text = data!.text!; } }, - trailingIcon: IconButton.filled( - icon: const Icon(Icons.qr_code_scanner, size: 16), - splashRadius: 18, - color: theme.textTheme.bodyMedium!.color, - onPressed: () async { - final result = await QrCodeReaderOverlay.show(context); - if (result != null) { - messageController.text = result; - } - }, + trailingIcon: UiPrimaryButton.flexible( + onPressed: isSubmitting + ? null + : () async { + final result = + await QrCodeReaderOverlay.show(context); + if (result != null) { + messageController.text = result; + } + }, + child: const Icon(Icons.qr_code_scanner, size: 16), ), ), ), ], ), const SizedBox(height: 24), - UiPrimaryButton( - text: LocaleKeys.signMessageButton.tr(), - onPressed: isSubmitting ? null : onSignPressed, + SizedBox( width: double.infinity, height: 56, + child: UiPrimaryButton.flexible( + text: LocaleKeys.signMessageButton.tr(), + onPressed: isSubmitting ? null : onSignPressed, + padding: + const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + ), ), ], ), @@ -232,8 +237,8 @@ class _EnhancedMessageInputState extends State { ), ), contentPadding: const EdgeInsets.all(16), - fillColor: - theme.colorScheme.surfaceVariant.withOpacity(0.3), + fillColor: theme.colorScheme.surfaceContainerHighest + .withOpacity(0.3), filled: true, ), style: theme.textTheme.bodyMedium?.copyWith( @@ -245,18 +250,17 @@ class _EnhancedMessageInputState extends State { ), ), const SizedBox(width: 8), - Container( + SizedBox( width: 48, child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (widget.trailingIcon != null) widget.trailingIcon!, if (widget.showCopyButton) - IconButton.filled( - icon: const Icon(Icons.content_paste, size: 16), - splashRadius: 18, - color: theme.textTheme.bodyMedium!.color, + UiPrimaryButton.flexible( + padding: const EdgeInsets.all(8), onPressed: widget.onCopyPressed, + child: const Icon(Icons.content_paste, size: 16), ), ], ), @@ -282,4 +286,4 @@ class _EnhancedMessageInputState extends State { ], ); } -} +} \ No newline at end of file From 03e303dbaec66533f64c58a23681a605b5886c43 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:25:08 +0200 Subject: [PATCH 13/13] fix: resolve analyzer errors from merge - alias mm2 BestOrder in grouped_list_view to disambiguate - pass required isMobile to CoinDetailsCommonButtons --- .../simple/form/tables/orders_table/grouped_list_view.dart | 4 ++-- .../coin_details/coin_details_info/coin_details_info.dart | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart index fb9f3052b4..adec86757a 100644 --- a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart +++ b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart @@ -9,7 +9,7 @@ import 'package:komodo_ui_kit/komodo_ui_kit.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:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart' as mm2; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/views/dex/simple/form/tables/coins_table/coins_table_item.dart'; @@ -136,7 +136,7 @@ class GroupedListView extends StatelessWidget { } else if (item is SelectItem) { return (coinsState.walletCoins[item.id] ?? coinsState.coins[item.id])!; } else { - final String coinId = (item as BestOrder).coin; + final String coinId = (item as mm2.BestOrder).coin; return (coinsState.walletCoins[coinId] ?? coinsState.coins[coinId])!; } } 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 2aecaf9605..f5f07e0cdf 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 @@ -262,6 +262,7 @@ class _DesktopCoinDetails extends StatelessWidget { padding: const EdgeInsets.fromLTRB(2, 28.0, 0, 0), width: double.infinity, child: CoinDetailsCommonButtons( + isMobile: false, selectWidget: setPageType, onClickSwapButton: context.watch().state is TradingEnabled @@ -350,6 +351,7 @@ class _CoinDetailsInfoHeader extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 12.0, bottom: 14.0), child: CoinDetailsCommonButtons( + isMobile: true, selectWidget: setPageType, onClickSwapButton: context.watch().state is TradingEnabled