diff --git a/assets/translations/en.json b/assets/translations/en.json index 60ab6a2a91..8527347210 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -63,7 +63,7 @@ "transactionComplete": "Transaction complete!", "transactionDenied": "Denied", "coinDisableSpan1": "You can't disable {} while it has a swap in progress", - "confirmSending": "Confirm withdrawl", + "confirmSending": "Confirm withdrawal", "confirmSend": "Confirm send", "confirm": "Confirm", "confirmed": "Confirmed", @@ -148,7 +148,7 @@ "settingsMenuSecurity": "Security", "settingsMenuAbout": "About", "seedPhraseSettingControlsViewSeed": "View seed phrase", - "seedPhraseSettingControlsDownloadSeed": "Download seed phrase file", + "seedPhraseSettingControlsDownloadSeed": "Download as file", "debugSettingsResetActivatedCoins": "Reset activated coins", "debugSettingsDownloadButton": "Download logs", "or": "Or", @@ -320,6 +320,7 @@ "feedbackFormMatrix": "Matrix", "feedbackFormTelegram": "Telegram", "feedbackFormSelectContactMethod": "Select contact method", + "feedbackSelectTypeValidation": "Please select a feedback type", "feedbackFormDiscordHint": "Discord username (e.g., username123)", "feedbackFormMatrixHint": "Matrix ID (e.g., @user:matrix.org)", "feedbackFormTelegramHint": "Telegram username (e.g., @username)", @@ -381,6 +382,17 @@ "trezorSelectSubTitle": "Select a hardware wallet you'd like to use with Komodo Wallet", "trezorBrowserUnsupported": "Trezor is not supported on this browser.\nPlease use Chrome for Trezor functionality.", "trezorTransactionInProgressMessage": "Please confirm transaction on your Trezor device", + "trezorInitializingMessage": "Initializing Trezor device...", + "trezorWaitingForDeviceMessage": "Waiting for Trezor device connection...", + "trezorAwaitingConfirmationMessage": "Please follow instructions on your Trezor device", + "trezorPinRequiredMessage": "Please enter your Trezor PIN", + "trezorPassphraseRequiredMessage": "Please enter your Trezor passphrase", + "trezorAuthFailedMessage": "Trezor authentication failed", + "trezorAuthCancelledMessage": "Trezor authentication was cancelled", + "trezorInitializationFailed": "Trezor initialization failed", + "trezorNoTaskIdFound": "No task ID found", + "trezorProvidePinFailed": "Failed to provide PIN", + "trezorProvidePassphraseFailed": "Failed to provide passphrase", "mixedCaseError": "If you are using non mixed case address, please try to convert to mixed case one.", "addressConvertedToMixedCase": "Address automatically converted to mixed-case for checksum validation.", "invalidAddressChecksum": "Invalid address checksum", @@ -403,6 +415,7 @@ "notEnoughBalanceForGasError": "Not enough balance to pay gas.", "notEnoughFundsError": "Not enough funds to perform a trade", "dexErrorMessage": "Something went wrong!", + "dexUnableToStartSwap": "Unable to start swap. Refresh the quote and try again.", "seedConfirmInitialText": "Enter the seed phrase", "seedConfirmIncorrectText": "Incorrect seed phrase", "mnemonicInvalidWordError": "Your seed phrase contains an unknown word. Please check spelling and try again.", @@ -515,6 +528,7 @@ "unableToActiveCoin": "Unable to activate {}", "coinIsNotActive": "{} is not active", "feedback": "Feedback", + "feedbackDefaultType": "User Feedback", "feedbackViewTitle": "Send us your feedback", "feedbackPageDescription": "Help us improve by sharing your suggestions, reporting bugs, or giving general feedback.", "feedbackThankyou": "Thank you for your feedback!", @@ -525,6 +539,9 @@ "totalFees": "Total fees", "selectProtocol": "Select protocol", "showSwapData": "Show swap data", + "exportSwapData": "Export swap data", + "orderUuid": "Order UUID: {}", + "swapUuid": "Swap UUID: {}", "importSwaps": "Import Swaps", "changeTheme": "Change theme", "available": "Available", diff --git a/lib/app_config/app_config.dart b/lib/app_config/app_config.dart index bfafab3fb2..95eaeca03e 100644 --- a/lib/app_config/app_config.dart +++ b/lib/app_config/app_config.dart @@ -104,6 +104,20 @@ Map priorityCoinsAbbrMap = { // All other coins get default priority (0) }; +/// Priority ticker symbols for unauthenticated users' asset list. +/// These coins will appear first in the order specified here, before other coins. +/// Order matters: coins are displayed in the order they appear in this list. +const List unauthenticatedUserPriorityTickers = [ + 'BTC', + 'KMD', + 'ETH', + 'BNB', + 'LTC', + 'DASH', + 'ZEC', + 'DOGE', +]; + /// List of coins that are excluded from the list of coins displayed on the /// coin lists (e.g. wallet page, coin selection dropdowns, etc.) /// TODO: remove this list once zhltc and NFTs are fully supported in the SDK diff --git a/lib/bloc/auth_bloc/auth_bloc.dart b/lib/bloc/auth_bloc/auth_bloc.dart index a36b777ae7..12e6a92fe9 100644 --- a/lib/bloc/auth_bloc/auth_bloc.dart +++ b/lib/bloc/auth_bloc/auth_bloc.dart @@ -14,6 +14,8 @@ import 'package:web_dex/blocs/wallets_repository.dart'; import 'package:web_dex/model/authorize_mode.dart'; import 'package:web_dex/model/kdf_auth_metadata_extension.dart'; import 'package:web_dex/model/wallet.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; part 'auth_bloc_event.dart'; part 'auth_bloc_state.dart'; diff --git a/lib/bloc/auth_bloc/trezor_auth_mixin.dart b/lib/bloc/auth_bloc/trezor_auth_mixin.dart index e516353eb0..f65a16ca6f 100644 --- a/lib/bloc/auth_bloc/trezor_auth_mixin.dart +++ b/lib/bloc/auth_bloc/trezor_auth_mixin.dart @@ -65,30 +65,30 @@ mixin TrezorAuthMixin on Bloc { switch (authState.status) { case AuthenticationStatus.initializing: return AuthBlocState.trezorInitializing( - message: authState.message ?? 'Initializing Trezor device...', + message: authState.message ?? LocaleKeys.trezorInitializingMessage.tr(), taskId: authState.taskId, ); case AuthenticationStatus.waitingForDevice: return AuthBlocState.trezorInitializing( message: - authState.message ?? 'Waiting for Trezor device connection...', + authState.message ?? LocaleKeys.trezorWaitingForDeviceMessage.tr(), taskId: authState.taskId, ); case AuthenticationStatus.waitingForDeviceConfirmation: return AuthBlocState.trezorAwaitingConfirmation( message: authState.message ?? - 'Please follow instructions on your Trezor device', + LocaleKeys.trezorAwaitingConfirmationMessage.tr(), taskId: authState.taskId, ); case AuthenticationStatus.pinRequired: return AuthBlocState.trezorPinRequired( - message: authState.message ?? 'Please enter your Trezor PIN', + message: authState.message ?? LocaleKeys.trezorPinRequiredMessage.tr(), taskId: authState.taskId!, ); case AuthenticationStatus.passphraseRequired: return AuthBlocState.trezorPassphraseRequired( - message: authState.message ?? 'Please enter your Trezor passphrase', + message: authState.message ?? LocaleKeys.trezorPassphraseRequiredMessage.tr(), taskId: authState.taskId!, ); case AuthenticationStatus.authenticating: @@ -98,14 +98,14 @@ mixin TrezorAuthMixin on Bloc { case AuthenticationStatus.error: return AuthBlocState.error( AuthException( - authState.error ?? 'Trezor authentication failed', + authState.error ?? LocaleKeys.trezorAuthFailedMessage.tr(), type: AuthExceptionType.generalAuthError, ), ); case AuthenticationStatus.cancelled: return AuthBlocState.error( AuthException( - 'Trezor authentication was cancelled', + LocaleKeys.trezorAuthCancelledMessage.tr(), type: AuthExceptionType.generalAuthError, ), ); @@ -123,7 +123,7 @@ mixin TrezorAuthMixin on Bloc { if (authState.user == null) { return AuthBlocState.error( AuthException( - 'Trezor initialization failed', + LocaleKeys.trezorInitializationFailed.tr(), type: AuthExceptionType.generalAuthError, ), ); @@ -155,7 +155,7 @@ mixin TrezorAuthMixin on Bloc { emit( AuthBlocState.error( AuthException( - 'No task ID found', + LocaleKeys.trezorNoTaskIdFound.tr(), type: AuthExceptionType.generalAuthError, ), ), @@ -169,7 +169,7 @@ mixin TrezorAuthMixin on Bloc { emit( AuthBlocState.error( AuthException( - 'Failed to provide PIN', + LocaleKeys.trezorProvidePinFailed.tr(), type: AuthExceptionType.generalAuthError, ), ), @@ -187,7 +187,7 @@ mixin TrezorAuthMixin on Bloc { emit( AuthBlocState.error( AuthException( - 'No task ID found', + LocaleKeys.trezorNoTaskIdFound.tr(), type: AuthExceptionType.generalAuthError, ), ), @@ -201,7 +201,7 @@ mixin TrezorAuthMixin on Bloc { emit( AuthBlocState.error( AuthException( - 'Failed to provide passphrase', + LocaleKeys.trezorProvidePassphraseFailed.tr(), type: AuthExceptionType.generalAuthError, ), ), diff --git a/lib/bloc/coins_bloc/asset_coin_extension.dart b/lib/bloc/coins_bloc/asset_coin_extension.dart index 2c073af8f0..9fbfedadd4 100644 --- a/lib/bloc/coins_bloc/asset_coin_extension.dart +++ b/lib/bloc/coins_bloc/asset_coin_extension.dart @@ -62,6 +62,9 @@ extension AssetCoinExtension on Asset { protocol.config.valueOrNull('protocol', 'protocol_data', 'platform'); } +/// Legacy mapping between SDK [CoinSubClass] and the app-level [CoinType]. +/// +/// New code should prefer using [CoinSubClass] directly for protocol logic. extension CoinTypeExtension on CoinSubClass { CoinType toCoinType() { switch (this) { @@ -71,6 +74,8 @@ extension CoinTypeExtension on CoinSubClass { return CoinType.ftm20; case CoinSubClass.arbitrum: return CoinType.arb20; + case CoinSubClass.sia: + return CoinType.sia; case CoinSubClass.slp: return CoinType.slp; case CoinSubClass.qrc20: @@ -107,7 +112,12 @@ extension CoinTypeExtension on CoinSubClass { return CoinType.krc20; case CoinSubClass.zhtlc: return CoinType.zhtlc; - default: + case CoinSubClass.moonbeam: + case CoinSubClass.ewt: + case CoinSubClass.rskSmartBitcoin: + case CoinSubClass.unknown: + // These subclasses are not modeled in the legacy [CoinType] enum. + // New code should avoid going through CoinType entirely. return CoinType.utxo; } } @@ -138,6 +148,10 @@ extension CoinTypeExtension on CoinSubClass { } } +/// Legacy mapping from app-level [CoinType] back to SDK [CoinSubClass]. +/// +/// This is used by older flows that still think in terms of `CoinType` +/// (e.g. some NFT / custom-token paths). Prefer [CoinSubClass] in new code. extension CoinSubClassExtension on CoinType { CoinSubClass toCoinSubClass() { switch (this) { @@ -147,6 +161,8 @@ extension CoinSubClassExtension on CoinType { return CoinSubClass.ftm20; case CoinType.arb20: return CoinSubClass.arbitrum; + case CoinType.sia: + return CoinSubClass.sia; case CoinType.slp: return CoinSubClass.slp; case CoinType.qrc20: diff --git a/lib/bloc/coins_bloc/coins_bloc.dart b/lib/bloc/coins_bloc/coins_bloc.dart index 9b0fc9824c..5653c61c90 100644 --- a/lib/bloc/coins_bloc/coins_bloc.dart +++ b/lib/bloc/coins_bloc/coins_bloc.dart @@ -156,15 +156,16 @@ class CoinsBloc extends Bloc { await emit.forEach( coinUpdateStream, onData: (Coin coin) { - if (!state.walletCoins.containsKey(coin.abbr)) { + final key = coin.id.id; + if (!state.walletCoins.containsKey(key)) { _log.warning( 'Coin ${coin.abbr} not found in wallet coins, skipping update', ); return state; } return state.copyWith( - walletCoins: {...state.walletCoins, coin.id.id: coin}, - coins: {...state.coins, coin.id.id: coin}, + walletCoins: {...state.walletCoins, key: coin}, + coins: {...state.coins, key: coin}, ); }, ); diff --git a/lib/bloc/coins_bloc/coins_repo.dart b/lib/bloc/coins_bloc/coins_repo.dart index 9907caacca..f0fd2830da 100644 --- a/lib/bloc/coins_bloc/coins_repo.dart +++ b/lib/bloc/coins_bloc/coins_repo.dart @@ -775,10 +775,16 @@ class CoinsRepo { spendable: newSpendable, ); + final updatedCoin = coin.copyWith(sendableBalance: newSpendable); + + // Broadcast the updated balance so non-streaming assets still emit + // real-time change events through the same path as streaming assets. + _broadcastBalanceChange(updatedCoin); + // Yield updated coin with new balance // We still set both the deprecated fields and rely on the SDK // for future access to maintain backward compatibility - yield coin.copyWith(sendableBalance: newSpendable); + yield updatedCoin; } } catch (e, s) { _log.warning('Failed to update balance for ${coin.id}', e, s); diff --git a/lib/bloc/coins_manager/coins_manager_bloc.dart b/lib/bloc/coins_manager/coins_manager_bloc.dart index 3a464025a6..2b0da74164 100644 --- a/lib/bloc/coins_manager/coins_manager_bloc.dart +++ b/lib/bloc/coins_manager/coins_manager_bloc.dart @@ -14,7 +14,6 @@ import 'package:web_dex/bloc/coins_manager/coins_manager_sort.dart'; import 'package:web_dex/bloc/settings/settings_repository.dart'; import 'package:web_dex/blocs/trading_entities_bloc.dart'; import 'package:web_dex/model/coin.dart'; -import 'package:web_dex/model/coin_type.dart'; import 'package:web_dex/model/coin_utils.dart'; import 'package:web_dex/shared/utils/extensions/kdf_user_extensions.dart'; import 'package:web_dex/router/state/wallet_state.dart'; @@ -184,8 +183,11 @@ class CoinsManagerBloc extends Bloc { CoinsManagerCoinTypeSelect event, Emitter emit, ) { - final List newTypes = state.selectedCoinTypes.contains(event.type) - ? state.selectedCoinTypes.where((type) => type != event.type).toList() + final List newTypes = + state.selectedCoinTypes.contains(event.type) + ? state.selectedCoinTypes + .where((type) => type != event.type) + .toList() : [...state.selectedCoinTypes, event.type]; emit(state.copyWith(selectedCoinTypes: newTypes)); @@ -356,7 +358,9 @@ class CoinsManagerBloc extends Bloc { List _filterByType(List coins) { return coins - .where((coin) => state.selectedCoinTypes.contains(coin.type)) + .where( + (coin) => state.selectedCoinTypes.contains(coin.id.subClass), + ) .toList(); } diff --git a/lib/bloc/coins_manager/coins_manager_event.dart b/lib/bloc/coins_manager/coins_manager_event.dart index e499fe2d0f..ba12036196 100644 --- a/lib/bloc/coins_manager/coins_manager_event.dart +++ b/lib/bloc/coins_manager/coins_manager_event.dart @@ -16,7 +16,9 @@ class CoinsManagerCoinsUpdate extends CoinsManagerEvent { class CoinsManagerCoinTypeSelect extends CoinsManagerEvent { const CoinsManagerCoinTypeSelect({required this.type}); - final CoinType type; + + /// Selected protocol filter, represented by SDK coin subclass. + final CoinSubClass type; } class CoinsManagerCoinsSwitch extends CoinsManagerEvent { diff --git a/lib/bloc/coins_manager/coins_manager_state.dart b/lib/bloc/coins_manager/coins_manager_state.dart index 396bf788df..27cda1a393 100644 --- a/lib/bloc/coins_manager/coins_manager_state.dart +++ b/lib/bloc/coins_manager/coins_manager_state.dart @@ -14,7 +14,11 @@ class CoinsManagerState extends Equatable { }); final CoinsManagerAction action; final String searchPhrase; - final List selectedCoinTypes; + /// Selected protocol filters, represented by SDK coin subclasses. + /// + /// Using [CoinSubClass] here keeps the UI in sync with SDK protocols + /// without requiring manual updates when new coin types are added. + final List selectedCoinTypes; final List coins; final List selectedCoins; final CoinsManagerSortData sortData; @@ -45,7 +49,7 @@ class CoinsManagerState extends Equatable { CoinsManagerState copyWith({ CoinsManagerAction? action, String? searchPhrase, - List? selectedCoinTypes, + List? selectedCoinTypes, List? coins, List? selectedCoins, CoinsManagerSortData? sortData, diff --git a/lib/bloc/taker_form/taker_bloc.dart b/lib/bloc/taker_form/taker_bloc.dart index 44e2c3e911..8f1a679ed1 100644 --- a/lib/bloc/taker_form/taker_bloc.dart +++ b/lib/bloc/taker_form/taker_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; @@ -16,6 +17,7 @@ import 'package:web_dex/bloc/taker_form/taker_event.dart'; import 'package:web_dex/bloc/taker_form/taker_state.dart'; import 'package:web_dex/bloc/taker_form/taker_validator.dart'; import 'package:web_dex/bloc/transformers.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.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_request.dart'; @@ -107,15 +109,33 @@ class TakerBloc extends Bloc { TakerStartSwap event, Emitter emit, ) async { + final sellCoin = state.sellCoin; + final selectedOrder = state.selectedOrder; + final sellAmount = state.sellAmount; + + if (sellCoin == null || selectedOrder == null || sellAmount == null) { + _log.warning( + 'Attempted to start swap with incomplete state. sellCoin: ' + '$sellCoin, selectedOrder: $selectedOrder, sellAmount: $sellAmount', + ); + emit(state.copyWith(inProgress: () => false)); + add( + TakerAddError( + DexFormError(error: LocaleKeys.dexUnableToStartSwap.tr()), + ), + ); + return; + } + emit(state.copyWith(inProgress: () => true)); final int callStart = DateTime.now().millisecondsSinceEpoch; final SellResponse response = await _dexRepo.sell( SellRequest( - base: state.sellCoin!.abbr, - rel: state.selectedOrder!.coin, - volume: state.sellAmount!, - price: state.selectedOrder!.price, + base: sellCoin.abbr, + rel: selectedOrder.coin, + volume: sellAmount, + price: selectedOrder.price, orderType: SellBuyOrderType.fillOrKill, ), ); @@ -129,12 +149,11 @@ class TakerBloc extends Bloc { (await _sdk.auth.currentUser)?.wallet.config.type.name ?? 'unknown'; _analyticsBloc.logEvent( SwapFailedEventData( - asset: state.sellCoin!.abbr, - secondaryAsset: state.selectedOrder!.coin, - network: state.sellCoin!.protocolType, + asset: sellCoin.abbr, + secondaryAsset: selectedOrder.coin, + network: sellCoin.protocolType, secondaryNetwork: - _coinsRepo.getCoin(state.selectedOrder!.coin)?.protocolType ?? - 'unknown', + _coinsRepo.getCoin(selectedOrder.coin)?.protocolType ?? 'unknown', failureStage: 'order_submission', hdType: walletType, durationMs: durationMs, diff --git a/lib/bloc/transaction_history/transaction_history_bloc.dart b/lib/bloc/transaction_history/transaction_history_bloc.dart index f3e2b84be9..d5663dc004 100644 --- a/lib/bloc/transaction_history/transaction_history_bloc.dart +++ b/lib/bloc/transaction_history/transaction_history_bloc.dart @@ -112,6 +112,9 @@ class TransactionHistoryBloc blockHeight: sanitized.blockHeight, fee: sanitized.fee ?? existing.fee, memo: sanitized.memo ?? existing.memo, + // Update the timestamp to change date from "Now" once tx + // is confirmed on the blockchain + timestamp: sanitized.timestamp, ); } @@ -192,6 +195,9 @@ class TransactionHistoryBloc blockHeight: sanitized.blockHeight, fee: sanitized.fee ?? existing.fee, memo: sanitized.memo ?? existing.memo, + // Update the timestamp to change date from "Now" once tx + // is confirmed on the blockchain + timestamp: sanitized.timestamp, ); } @@ -242,12 +248,6 @@ class TransactionHistoryBloc emit(state.copyWith(loading: false, error: event.error)); } - DateTime _sortTime(Transaction tx) { - if (tx.timestamp.millisecondsSinceEpoch != 0) return tx.timestamp; - final firstSeen = _firstSeenAtById[tx.internalId]; - return firstSeen ?? DateTime.fromMillisecondsSinceEpoch(0); - } - int _compareTransactions(Transaction left, Transaction right) { final unconfirmedTimestamp = DateTime.fromMillisecondsSinceEpoch(0); if (right.timestamp == unconfirmedTimestamp) { diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index d9c3c4002c..a9d1877fdd 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -6,7 +6,6 @@ import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; -import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/services/fd_monitor_service.dart'; @@ -23,7 +22,6 @@ import 'package:decimal/decimal.dart'; class WithdrawFormBloc extends Bloc { final KomodoDefiSdk _sdk; final WalletType? _walletType; - final Mm2Api _mm2Api; WithdrawFormBloc({ required Asset asset, @@ -31,7 +29,6 @@ class WithdrawFormBloc extends Bloc { required Mm2Api mm2Api, WalletType? walletType, }) : _sdk = sdk, - _mm2Api = mm2Api, _walletType = walletType, super( WithdrawFormState( @@ -485,27 +482,35 @@ class WithdrawFormBloc extends Bloc { throw Exception('Missing withdrawal preview'); } - final response = await _mm2Api.sendRawTransaction( - SendRawTransactionRequest( - coin: preview.coin, - txHex: preview.txHex, - ), - ); - - if (response.txHash == null) { - throw Exception(response.error?.message ?? 'Broadcast failed'); + // Execute the previewed withdrawal: the transaction was already signed during preview, + // so executeWithdrawal() will NOT sign again. It simply broadcasts the pre-signed transaction, + // preserving the key behavior from the previous implementation. + WithdrawalResult? result; + await for (final progress in _sdk.withdrawals.executeWithdrawal( + preview, + state.asset.id.id, + )) { + if (progress.status == WithdrawalStatus.complete) { + result = progress.withdrawalResult; + break; + } else if (progress.status == WithdrawalStatus.error) { + throw Exception(progress.errorMessage ?? 'Broadcast failed'); + } + // Continue for in-progress states } - final result = WithdrawalResult( - txHash: response.txHash!, - balanceChanges: preview.balanceChanges, - coin: preview.coin, - toAddress: preview.to.first, - fee: preview.fee, - kmdRewardsEligible: - preview.kmdRewards != null && - Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, - ); + if (result == null) { + emit( + state.copyWith( + isSending: false, + transactionError: () => TextError( + error: 'Withdrawal did not complete: no result received.' + ), + isAwaitingTrezorConfirmation: false, + ), + ); + return; + } emit( state.copyWith( diff --git a/lib/blocs/kmd_rewards_bloc.dart b/lib/blocs/kmd_rewards_bloc.dart index ec31b5d469..b0b8b2b2c0 100644 --- a/lib/blocs/kmd_rewards_bloc.dart +++ b/lib/blocs/kmd_rewards_bloc.dart @@ -41,6 +41,13 @@ class KmdRewardsBloc implements BlocBase { ); } + if (withdrawDetails.txHex == null) { + _claimInProgress = false; + return BlocResponse( + error: TextError(error: LocaleKeys.somethingWrong.tr()), + ); + } + final tx = await _mm2Api.sendRawTransaction(SendRawTransactionRequest( coin: 'KMD', txHex: withdrawDetails.txHex, diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index ceb3c15a21..305d5a33bd 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const plsActivateKmd = 'plsActivateKmd'; static const rewardClaiming = 'rewardClaiming'; static const noKmdAddress = 'noKmdAddress'; @@ -106,28 +106,36 @@ abstract class LocaleKeys { static const seedPhrase = 'seedPhrase'; static const assetNumber = 'assetNumber'; static const clipBoard = 'clipBoard'; - static const walletsManagerCreateWalletButton = 'walletsManagerCreateWalletButton'; - static const walletsManagerImportWalletButton = 'walletsManagerImportWalletButton'; - static const walletsManagerStepBuilderCreationWalletError = 'walletsManagerStepBuilderCreationWalletError'; + static const walletsManagerCreateWalletButton = + 'walletsManagerCreateWalletButton'; + static const walletsManagerImportWalletButton = + 'walletsManagerImportWalletButton'; + static const walletsManagerStepBuilderCreationWalletError = + 'walletsManagerStepBuilderCreationWalletError'; static const walletCreationTitle = 'walletCreationTitle'; static const walletImportTitle = 'walletImportTitle'; static const walletImportByFileTitle = 'walletImportByFileTitle'; static const invalidWalletNameError = 'invalidWalletNameError'; static const invalidWalletFileNameError = 'invalidWalletFileNameError'; - static const walletImportCreatePasswordTitle = 'walletImportCreatePasswordTitle'; + static const walletImportCreatePasswordTitle = + 'walletImportCreatePasswordTitle'; static const walletImportByFileDescription = 'walletImportByFileDescription'; static const walletLogInTitle = 'walletLogInTitle'; static const walletCreationNameHint = 'walletCreationNameHint'; static const walletCreationPasswordHint = 'walletCreationPasswordHint'; - static const walletCreationConfirmPasswordHint = 'walletCreationConfirmPasswordHint'; + static const walletCreationConfirmPasswordHint = + 'walletCreationConfirmPasswordHint'; static const walletCreationConfirmPassword = 'walletCreationConfirmPassword'; static const walletCreationUploadFile = 'walletCreationUploadFile'; static const walletCreationEmptySeedError = 'walletCreationEmptySeedError'; static const walletCreationExistNameError = 'walletCreationExistNameError'; static const walletCreationNameLengthError = 'walletCreationNameLengthError'; - static const walletCreationFormatPasswordError = 'walletCreationFormatPasswordError'; - static const walletCreationConfirmPasswordError = 'walletCreationConfirmPasswordError'; - static const walletCreationNameCharactersError = 'walletCreationNameCharactersError'; + static const walletCreationFormatPasswordError = + 'walletCreationFormatPasswordError'; + static const walletCreationConfirmPasswordError = + 'walletCreationConfirmPasswordError'; + static const walletCreationNameCharactersError = + 'walletCreationNameCharactersError'; static const renameWalletDescription = 'renameWalletDescription'; static const renameWalletConfirm = 'renameWalletConfirm'; static const incorrectPassword = 'incorrectPassword'; @@ -138,15 +146,19 @@ abstract class LocaleKeys { static const passphraseCheckingTitle = 'passphraseCheckingTitle'; static const passphraseCheckingDescription = 'passphraseCheckingDescription'; static const passphraseCheckingEnterWord = 'passphraseCheckingEnterWord'; - static const passphraseCheckingEnterWordHint = 'passphraseCheckingEnterWordHint'; + static const passphraseCheckingEnterWordHint = + 'passphraseCheckingEnterWordHint'; static const back = 'back'; static const settingsMenuGeneral = 'settingsMenuGeneral'; static const settingsMenuLanguage = 'settingsMenuLanguage'; static const settingsMenuSecurity = 'settingsMenuSecurity'; static const settingsMenuAbout = 'settingsMenuAbout'; - static const seedPhraseSettingControlsViewSeed = 'seedPhraseSettingControlsViewSeed'; - static const seedPhraseSettingControlsDownloadSeed = 'seedPhraseSettingControlsDownloadSeed'; - static const debugSettingsResetActivatedCoins = 'debugSettingsResetActivatedCoins'; + static const seedPhraseSettingControlsViewSeed = + 'seedPhraseSettingControlsViewSeed'; + static const seedPhraseSettingControlsDownloadSeed = + 'seedPhraseSettingControlsDownloadSeed'; + static const debugSettingsResetActivatedCoins = + 'debugSettingsResetActivatedCoins'; static const debugSettingsDownloadButton = 'debugSettingsDownloadButton'; static const or = 'or'; static const passwordTitle = 'passwordTitle'; @@ -156,16 +168,19 @@ abstract class LocaleKeys { static const changePasswordSpan1 = 'changePasswordSpan1'; static const updatePassword = 'updatePassword'; static const passwordHasChanged = 'passwordHasChanged'; - static const confirmationForShowingSeedPhraseTitle = 'confirmationForShowingSeedPhraseTitle'; + static const confirmationForShowingSeedPhraseTitle = + 'confirmationForShowingSeedPhraseTitle'; static const saveAndRemember = 'saveAndRemember'; static const seedPhraseShowingTitle = 'seedPhraseShowingTitle'; static const seedPhraseShowingWarning = 'seedPhraseShowingWarning'; static const seedPhraseShowingShowPhrase = 'seedPhraseShowingShowPhrase'; static const seedPhraseShowingCopySeed = 'seedPhraseShowingCopySeed'; - static const seedPhraseShowingSavedPhraseButton = 'seedPhraseShowingSavedPhraseButton'; + static const seedPhraseShowingSavedPhraseButton = + 'seedPhraseShowingSavedPhraseButton'; static const seedAccessSpan1 = 'seedAccessSpan1'; static const backupSeedNotificationTitle = 'backupSeedNotificationTitle'; - static const backupSeedNotificationDescription = 'backupSeedNotificationDescription'; + static const backupSeedNotificationDescription = + 'backupSeedNotificationDescription'; static const backupSeedNotificationButton = 'backupSeedNotificationButton'; static const swapConfirmationTitle = 'swapConfirmationTitle'; static const swapConfirmationYouReceive = 'swapConfirmationYouReceive'; @@ -173,41 +188,54 @@ abstract class LocaleKeys { static const tradingDetailsTitleFailed = 'tradingDetailsTitleFailed'; static const tradingDetailsTitleCompleted = 'tradingDetailsTitleCompleted'; static const tradingDetailsTitleInProgress = 'tradingDetailsTitleInProgress'; - static const tradingDetailsTitleOrderMatching = 'tradingDetailsTitleOrderMatching'; + static const tradingDetailsTitleOrderMatching = + 'tradingDetailsTitleOrderMatching'; static const tradingDetailsTotalSpentTime = 'tradingDetailsTotalSpentTime'; - static const tradingDetailsTotalSpentTimeWithHours = 'tradingDetailsTotalSpentTimeWithHours'; + static const tradingDetailsTotalSpentTimeWithHours = + 'tradingDetailsTotalSpentTimeWithHours'; static const swapRecoverButtonTitle = 'swapRecoverButtonTitle'; static const swapRecoverButtonText = 'swapRecoverButtonText'; static const swapRecoverButtonErrorMessage = 'swapRecoverButtonErrorMessage'; - static const swapRecoverButtonSuccessMessage = 'swapRecoverButtonSuccessMessage'; + static const swapRecoverButtonSuccessMessage = + 'swapRecoverButtonSuccessMessage'; static const swapProgressStatusFailed = 'swapProgressStatusFailed'; static const swapDetailsStepStatusFailed = 'swapDetailsStepStatusFailed'; static const disclaimerAcceptEulaCheckbox = 'disclaimerAcceptEulaCheckbox'; - static const disclaimerAcceptTermsAndConditionsCheckbox = 'disclaimerAcceptTermsAndConditionsCheckbox'; + static const disclaimerAcceptTermsAndConditionsCheckbox = + 'disclaimerAcceptTermsAndConditionsCheckbox'; static const disclaimerAcceptDescription = 'disclaimerAcceptDescription'; - static const swapDetailsStepStatusInProcess = 'swapDetailsStepStatusInProcess'; - static const swapDetailsStepStatusTimeSpent = 'swapDetailsStepStatusTimeSpent'; + static const swapDetailsStepStatusInProcess = + 'swapDetailsStepStatusInProcess'; + static const swapDetailsStepStatusTimeSpent = + 'swapDetailsStepStatusTimeSpent'; static const milliseconds = 'milliseconds'; static const seconds = 'seconds'; static const minutes = 'minutes'; static const hours = 'hours'; - static const coinAddressDetailsNotificationTitle = 'coinAddressDetailsNotificationTitle'; - static const coinAddressDetailsNotificationDescription = 'coinAddressDetailsNotificationDescription'; + static const coinAddressDetailsNotificationTitle = + 'coinAddressDetailsNotificationTitle'; + static const coinAddressDetailsNotificationDescription = + 'coinAddressDetailsNotificationDescription'; static const swapFeeDetailsPaidFromBalance = 'swapFeeDetailsPaidFromBalance'; static const swapFeeDetailsSendCoinTxFee = 'swapFeeDetailsSendCoinTxFee'; - static const swapFeeDetailsReceiveCoinTxFee = 'swapFeeDetailsReceiveCoinTxFee'; + static const swapFeeDetailsReceiveCoinTxFee = + 'swapFeeDetailsReceiveCoinTxFee'; static const swapFeeDetailsTradingFee = 'swapFeeDetailsTradingFee'; - static const swapFeeDetailsSendTradingFeeTxFee = 'swapFeeDetailsSendTradingFeeTxFee'; + static const swapFeeDetailsSendTradingFeeTxFee = + 'swapFeeDetailsSendTradingFeeTxFee'; static const swapFeeDetailsNone = 'swapFeeDetailsNone'; - static const swapFeeDetailsPaidFromReceivedVolume = 'swapFeeDetailsPaidFromReceivedVolume'; + static const swapFeeDetailsPaidFromReceivedVolume = + 'swapFeeDetailsPaidFromReceivedVolume'; static const logoutPopupTitle = 'logoutPopupTitle'; - static const logoutPopupDescriptionWalletOnly = 'logoutPopupDescriptionWalletOnly'; + static const logoutPopupDescriptionWalletOnly = + 'logoutPopupDescriptionWalletOnly'; static const logoutPopupDescription = 'logoutPopupDescription'; static const transactionDetailsTitle = 'transactionDetailsTitle'; static const customSeedWarningText = 'customSeedWarningText'; static const customSeedIUnderstand = 'customSeedIUnderstand'; static const walletCreationBip39SeedError = 'walletCreationBip39SeedError'; - static const walletCreationHdBip39SeedError = 'walletCreationHdBip39SeedError'; + static const walletCreationHdBip39SeedError = + 'walletCreationHdBip39SeedError'; static const walletPageNoSuchAsset = 'walletPageNoSuchAsset'; static const swap = 'swap'; static const swapAddress = 'swapAddress'; @@ -283,7 +311,8 @@ abstract class LocaleKeys { static const sellCryptoDescription = 'sellCryptoDescription'; static const buy = 'buy'; static const changingWalletPassword = 'changingWalletPassword'; - static const changingWalletPasswordDescription = 'changingWalletPasswordDescription'; + static const changingWalletPasswordDescription = + 'changingWalletPasswordDescription'; static const dark = 'dark'; static const darkMode = 'darkMode'; static const light = 'light'; @@ -315,7 +344,9 @@ abstract class LocaleKeys { static const feedbackFormDiscord = 'feedbackFormDiscord'; static const feedbackFormMatrix = 'feedbackFormMatrix'; static const feedbackFormTelegram = 'feedbackFormTelegram'; - static const feedbackFormSelectContactMethod = 'feedbackFormSelectContactMethod'; + static const feedbackFormSelectContactMethod = + 'feedbackFormSelectContactMethod'; + static const feedbackSelectTypeValidation = 'feedbackSelectTypeValidation'; static const feedbackFormDiscordHint = 'feedbackFormDiscordHint'; static const feedbackFormMatrixHint = 'feedbackFormMatrixHint'; static const feedbackFormTelegramHint = 'feedbackFormTelegramHint'; @@ -327,7 +358,8 @@ abstract class LocaleKeys { static const contactRequiredError = 'contactRequiredError'; static const contactDetailsMaxLengthError = 'contactDetailsMaxLengthError'; static const discordUsernameValidatorError = 'discordUsernameValidatorError'; - static const telegramUsernameValidatorError = 'telegramUsernameValidatorError'; + static const telegramUsernameValidatorError = + 'telegramUsernameValidatorError'; static const matrixIdValidatorError = 'matrixIdValidatorError'; static const myCoinsMissing = 'myCoinsMissing'; static const myCoinsMissingReassurance = 'myCoinsMissingReassurance'; @@ -337,7 +369,8 @@ abstract class LocaleKeys { static const myCoinsMissingHelp = 'myCoinsMissingHelp'; static const myCoinsMissingSignIn = 'myCoinsMissingSignIn'; static const feedbackValidatorEmptyError = 'feedbackValidatorEmptyError'; - static const feedbackValidatorMaxLengthError = 'feedbackValidatorMaxLengthError'; + static const feedbackValidatorMaxLengthError = + 'feedbackValidatorMaxLengthError'; static const yourFeedback = 'yourFeedback'; static const sendFeedback = 'sendFeedback'; static const sendFeedbackError = 'sendFeedbackError'; @@ -376,7 +409,21 @@ abstract class LocaleKeys { static const trezorSelectTitle = 'trezorSelectTitle'; static const trezorSelectSubTitle = 'trezorSelectSubTitle'; static const trezorBrowserUnsupported = 'trezorBrowserUnsupported'; - static const trezorTransactionInProgressMessage = 'trezorTransactionInProgressMessage'; + static const trezorTransactionInProgressMessage = + 'trezorTransactionInProgressMessage'; + static const trezorInitializingMessage = 'trezorInitializingMessage'; + static const trezorWaitingForDeviceMessage = 'trezorWaitingForDeviceMessage'; + static const trezorAwaitingConfirmationMessage = + 'trezorAwaitingConfirmationMessage'; + static const trezorPinRequiredMessage = 'trezorPinRequiredMessage'; + static const trezorPassphraseRequiredMessage = + 'trezorPassphraseRequiredMessage'; + static const trezorAuthFailedMessage = 'trezorAuthFailedMessage'; + static const trezorAuthCancelledMessage = 'trezorAuthCancelledMessage'; + static const trezorInitializationFailed = 'trezorInitializationFailed'; + static const trezorNoTaskIdFound = 'trezorNoTaskIdFound'; + static const trezorProvidePinFailed = 'trezorProvidePinFailed'; + static const trezorProvidePassphraseFailed = 'trezorProvidePassphraseFailed'; static const mixedCaseError = 'mixedCaseError'; static const addressConvertedToMixedCase = 'addressConvertedToMixedCase'; static const invalidAddressChecksum = 'invalidAddressChecksum'; @@ -386,7 +433,8 @@ abstract class LocaleKeys { static const noSenderAddress = 'noSenderAddress'; static const confirmOnTrezor = 'confirmOnTrezor'; static const alphaVersionWarningTitle = 'alphaVersionWarningTitle'; - static const alphaVersionWarningDescription = 'alphaVersionWarningDescription'; + static const alphaVersionWarningDescription = + 'alphaVersionWarningDescription'; static const sendToAnalytics = 'sendToAnalytics'; static const backToWallet = 'backToWallet'; static const backToDex = 'backToDex'; @@ -399,6 +447,7 @@ abstract class LocaleKeys { static const notEnoughBalanceForGasError = 'notEnoughBalanceForGasError'; static const notEnoughFundsError = 'notEnoughFundsError'; static const dexErrorMessage = 'dexErrorMessage'; + static const dexUnableToStartSwap = 'dexUnableToStartSwap'; static const seedConfirmInitialText = 'seedConfirmInitialText'; static const seedConfirmIncorrectText = 'seedConfirmIncorrectText'; static const mnemonicInvalidWordError = 'mnemonicInvalidWordError'; @@ -411,12 +460,14 @@ abstract class LocaleKeys { static const currentPassword = 'currentPassword'; static const walletNotFound = 'walletNotFound'; static const passwordIsEmpty = 'passwordIsEmpty'; - static const passwordContainsTheWordPassword = 'passwordContainsTheWordPassword'; + static const passwordContainsTheWordPassword = + 'passwordContainsTheWordPassword'; static const passwordTooShort = 'passwordTooShort'; static const passwordMissingDigit = 'passwordMissingDigit'; static const passwordMissingLowercase = 'passwordMissingLowercase'; static const passwordMissingUppercase = 'passwordMissingUppercase'; - static const passwordMissingSpecialCharacter = 'passwordMissingSpecialCharacter'; + static const passwordMissingSpecialCharacter = + 'passwordMissingSpecialCharacter'; static const passwordConsecutiveCharacters = 'passwordConsecutiveCharacters'; static const passwordSecurity = 'passwordSecurity'; static const allowWeakPassword = 'allowWeakPassword'; @@ -445,13 +496,16 @@ abstract class LocaleKeys { static const bridgeMaxSendAmountError = 'bridgeMaxSendAmountError'; static const bridgeMinOrderAmountError = 'bridgeMinOrderAmountError'; static const bridgeMaxOrderAmountError = 'bridgeMaxOrderAmountError'; - static const bridgeInsufficientBalanceError = 'bridgeInsufficientBalanceError'; + static const bridgeInsufficientBalanceError = + 'bridgeInsufficientBalanceError'; static const lowTradeVolumeError = 'lowTradeVolumeError'; static const bridgeSelectReceiveCoinError = 'bridgeSelectReceiveCoinError'; static const withdrawNoParentCoinError = 'withdrawNoParentCoinError'; static const withdrawTopUpBalanceError = 'withdrawTopUpBalanceError'; - static const withdrawNotEnoughBalanceForGasError = 'withdrawNotEnoughBalanceForGasError'; - static const withdrawNotSufficientBalanceError = 'withdrawNotSufficientBalanceError'; + static const withdrawNotEnoughBalanceForGasError = + 'withdrawNotEnoughBalanceForGasError'; + static const withdrawNotSufficientBalanceError = + 'withdrawNotSufficientBalanceError'; static const withdrawZeroBalanceError = 'withdrawZeroBalanceError'; static const withdrawAmountTooLowError = 'withdrawAmountTooLowError'; static const withdrawNoSuchCoinError = 'withdrawNoSuchCoinError'; @@ -511,6 +565,7 @@ abstract class LocaleKeys { static const unableToActiveCoin = 'unableToActiveCoin'; static const coinIsNotActive = 'coinIsNotActive'; static const feedback = 'feedback'; + static const feedbackDefaultType = 'feedbackDefaultType'; static const feedbackViewTitle = 'feedbackViewTitle'; static const feedbackPageDescription = 'feedbackPageDescription'; static const feedbackThankyou = 'feedbackThankyou'; @@ -521,14 +576,19 @@ abstract class LocaleKeys { static const totalFees = 'totalFees'; static const selectProtocol = 'selectProtocol'; static const showSwapData = 'showSwapData'; + static const exportSwapData = 'exportSwapData'; + static const orderUuid = 'orderUuid'; + static const swapUuid = 'swapUuid'; static const importSwaps = 'importSwaps'; static const changeTheme = 'changeTheme'; static const available = 'available'; static const availableForSwaps = 'availableForSwaps'; static const swapNow = 'swapNow'; static const passphrase = 'passphrase'; - static const enterPassphraseHiddenWalletTitle = 'enterPassphraseHiddenWalletTitle'; - static const enterPassphraseHiddenWalletDescription = 'enterPassphraseHiddenWalletDescription'; + static const enterPassphraseHiddenWalletTitle = + 'enterPassphraseHiddenWalletTitle'; + static const enterPassphraseHiddenWalletDescription = + 'enterPassphraseHiddenWalletDescription'; static const skip = 'skip'; static const activateToSeeFunds = 'activateToSeeFunds'; static const useCustomSeedOrWif = 'useCustomSeedOrWif'; @@ -555,13 +615,15 @@ abstract class LocaleKeys { static const downloadAllKeys = 'downloadAllKeys'; static const shareAllKeys = 'shareAllKeys'; static const confirmPrivateKeyBackup = 'confirmPrivateKeyBackup'; - static const confirmPrivateKeyBackupDescription = 'confirmPrivateKeyBackupDescription'; + static const confirmPrivateKeyBackupDescription = + 'confirmPrivateKeyBackupDescription'; static const importantSecurityNotice = 'importantSecurityNotice'; static const privateKeySecurityWarning = 'privateKeySecurityWarning'; static const privateKeyBackupConfirmation = 'privateKeyBackupConfirmation'; static const confirmBackupComplete = 'confirmBackupComplete'; static const privateKeyExportSuccessTitle = 'privateKeyExportSuccessTitle'; - static const privateKeyExportSuccessDescription = 'privateKeyExportSuccessDescription'; + static const privateKeyExportSuccessDescription = + 'privateKeyExportSuccessDescription'; static const iHaveSavedMyPrivateKeys = 'iHaveSavedMyPrivateKeys'; static const copyWarning = 'copyWarning'; static const seedConfirmTitle = 'seedConfirmTitle'; @@ -609,8 +671,10 @@ abstract class LocaleKeys { static const collectibles = 'collectibles'; static const sendingProcess = 'sendingProcess'; static const ercStandardDisclaimer = 'ercStandardDisclaimer'; - static const nftReceiveNonSwapAddressWarning = 'nftReceiveNonSwapAddressWarning'; - static const nftReceiveNonSwapWalletDetails = 'nftReceiveNonSwapWalletDetails'; + static const nftReceiveNonSwapAddressWarning = + 'nftReceiveNonSwapAddressWarning'; + static const nftReceiveNonSwapWalletDetails = + 'nftReceiveNonSwapWalletDetails'; static const nftMainLoggedOut = 'nftMainLoggedOut'; static const confirmLogoutOnAnotherTab = 'confirmLogoutOnAnotherTab'; static const refreshList = 'refreshList'; @@ -624,8 +688,10 @@ abstract class LocaleKeys { static const noWalletsAvailable = 'noWalletsAvailable'; static const selectWalletToReset = 'selectWalletToReset'; static const qrScannerTitle = 'qrScannerTitle'; - static const qrScannerErrorControllerUninitialized = 'qrScannerErrorControllerUninitialized'; - static const qrScannerErrorPermissionDenied = 'qrScannerErrorPermissionDenied'; + static const qrScannerErrorControllerUninitialized = + 'qrScannerErrorControllerUninitialized'; + static const qrScannerErrorPermissionDenied = + 'qrScannerErrorPermissionDenied'; static const qrScannerErrorGenericError = 'qrScannerErrorGenericError'; static const qrScannerErrorTitle = 'qrScannerErrorTitle'; static const spend = 'spend'; @@ -670,7 +736,8 @@ abstract class LocaleKeys { static const fiatPaymentInProgressMessage = 'fiatPaymentInProgressMessage'; static const pleaseWait = 'pleaseWait'; static const bitrefillPaymentSuccessfull = 'bitrefillPaymentSuccessfull'; - static const bitrefillPaymentSuccessfullInstruction = 'bitrefillPaymentSuccessfullInstruction'; + static const bitrefillPaymentSuccessfullInstruction = + 'bitrefillPaymentSuccessfullInstruction'; static const tradingBot = 'tradingBot'; static const margin = 'margin'; static const updateInterval = 'updateInterval'; @@ -763,7 +830,8 @@ abstract class LocaleKeys { static const securitySettings = 'securitySettings'; static const zhtlcConfigureTitle = 'zhtlcConfigureTitle'; static const zhtlcZcashParamsPathLabel = 'zhtlcZcashParamsPathLabel'; - static const zhtlcPathAutomaticallyDetected = 'zhtlcPathAutomaticallyDetected'; + static const zhtlcPathAutomaticallyDetected = + 'zhtlcPathAutomaticallyDetected'; static const zhtlcSaplingParamsFolder = 'zhtlcSaplingParamsFolder'; static const zhtlcBlocksPerIterationLabel = 'zhtlcBlocksPerIterationLabel'; static const zhtlcScanIntervalLabel = 'zhtlcScanIntervalLabel'; @@ -783,7 +851,7 @@ abstract class LocaleKeys { static const zhtlcActivating = 'zhtlcActivating'; static const zhtlcActivationWarning = 'zhtlcActivationWarning'; static const zhtlcAdvancedConfiguration = 'zhtlcAdvancedConfiguration'; - static const zhtlcAdvancedConfigurationHint = 'zhtlcAdvancedConfigurationHint'; + static const zhtlcAdvancedConfigurationHint = + 'zhtlcAdvancedConfigurationHint'; static const zhtlcConfigButton = 'zhtlcConfigButton'; - } diff --git a/lib/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart b/lib/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart index d3e2573753..d32d9f474a 100644 --- a/lib/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart +++ b/lib/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart @@ -3,13 +3,18 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; class SendRawTransactionRequest implements BaseRequest { SendRawTransactionRequest({ required this.coin, - required this.txHex, - }); + this.txHex, + this.txJson, + }) : assert( + txHex != null || txJson != null, + 'Either txHex or txJson must be provided', + ); factory SendRawTransactionRequest.fromJson(Map json) { return SendRawTransactionRequest( coin: json['coin'], txHex: json['tx_hex'], + txJson: json['tx_json'], ); } @@ -17,7 +22,8 @@ class SendRawTransactionRequest implements BaseRequest { final String method = 'send_raw_transaction'; String coin; - String txHex; + String? txHex; + Map? txJson; @override late String userpass; @@ -27,7 +33,8 @@ class SendRawTransactionRequest implements BaseRequest { return { 'method': method, 'coin': coin, - 'tx_hex': txHex, + if (txHex != null) 'tx_hex': txHex, + if (txJson != null) 'tx_json': txJson, 'userpass': userpass, }; } diff --git a/lib/model/coin_type.dart b/lib/model/coin_type.dart index 402da3b250..4c94bd4c8c 100644 --- a/lib/model/coin_type.dart +++ b/lib/model/coin_type.dart @@ -1,3 +1,9 @@ +/// Legacy coin "type" used as a compatibility layer while migrating the app +/// to the SDK's `CoinSubClass`/`AssetId.subClass` protocol model. +/// +/// New features and UI should avoid depending on [CoinType] directly and +/// instead use SDK metadata. This enum exists only to keep older code paths +/// working until they can be refactored. // anchor: protocols support enum CoinType { utxo, @@ -21,4 +27,7 @@ enum CoinType { tendermint, slp, zhtlc, + + /// Legacy glue for the Sia protocol (`CoinSubClass.sia`). + sia, } diff --git a/lib/model/coin_utils.dart b/lib/model/coin_utils.dart index 1d89f21284..c98b00fcde 100644 --- a/lib/model/coin_utils.dart +++ b/lib/model/coin_utils.dart @@ -143,6 +143,8 @@ String getCoinTypeName(CoinType type, [String? symbol]) { return 'Native'; case CoinType.smartChain: return 'Smart Chain'; + case CoinType.sia: + return 'Sia'; case CoinType.ftm20: return 'FTM-20'; case CoinType.arb20: diff --git a/lib/model/withdraw_details/withdraw_details.dart b/lib/model/withdraw_details/withdraw_details.dart index 313732a28d..3f34cb10da 100644 --- a/lib/model/withdraw_details/withdraw_details.dart +++ b/lib/model/withdraw_details/withdraw_details.dart @@ -2,7 +2,8 @@ import 'package:web_dex/model/withdraw_details/fee_details.dart'; class WithdrawDetails { WithdrawDetails({ - required this.txHex, + this.txHex, + this.txJson, required this.txHash, required this.from, required this.to, @@ -15,7 +16,11 @@ class WithdrawDetails { required this.feeDetails, required this.coin, required this.internalId, - }); + }) : assert( + txHex != null || txJson != null, + 'Either txHex or txJson must be provided', + ); + factory WithdrawDetails.fromJson(Map json) { final String totalAmount = json['total_amount'].toString(); final String spentByMe = json['spent_by_me'].toString(); @@ -24,6 +29,7 @@ class WithdrawDetails { return WithdrawDetails( txHex: json['tx_hex'], + txJson: json['tx_json'], txHash: json['tx_hash'], from: List.from(json['from']), to: List.from(json['to']), @@ -55,7 +61,8 @@ class WithdrawDetails { internalId: '', ); - final String txHex; + final String? txHex; + final Map? txJson; final String txHash; final List from; final List to; @@ -83,6 +90,7 @@ class WithdrawDetails { static WithdrawDetails fromTrezorJson(Map json) { return WithdrawDetails( txHex: json['tx_hex'], + txJson: json['tx_json'], txHash: json['tx_hash'], totalAmount: json['total_amount'].toString(), coin: json['coin'], diff --git a/lib/sdk/widgets/window_close_handler.dart b/lib/sdk/widgets/window_close_handler.dart index 8623ae57bb..10b96594b9 100644 --- a/lib/sdk/widgets/window_close_handler.dart +++ b/lib/sdk/widgets/window_close_handler.dart @@ -1,3 +1,6 @@ +import 'dart:io' show exit + if (dart.library.html) 'window_close_handler_exit_stub.dart' show exit; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -15,6 +18,7 @@ import 'package:web_dex/shared/utils/window/window.dart'; /// /// This widget uses different strategies based on the platform: /// - Desktop (Windows, macOS, Linux): Uses flutter_window_close for native window close handling +/// On Linux, native code uses workaround to bypass GTK cleanup to prevent crashes /// - Web: Uses showMessageBeforeUnload for browser beforeunload event /// - Mobile (iOS, Android): Uses WidgetsBindingObserver for lifecycle management /// and PopScope for exit confirmation @@ -47,9 +51,25 @@ class _WindowCloseHandlerState extends State /// Sets up the appropriate close handler based on the platform. void _setupCloseHandler() { if (PlatformTuner.isNativeDesktop) { - // Desktop platforms: Use flutter_window_close + // Desktop platforms: Use flutter_window_close for all platforms + // On Linux, we use flutter_window_close for dialog, but return false to prevent + // standard window closing, then manually trigger exit via SystemNavigator FlutterWindowClose.setWindowShouldCloseHandler(() async { - return await _handleWindowClose(); + final shouldClose = await _handleWindowClose(); + + // On Linux, if user confirmed, we need to manually exit instead of letting + // flutter_window_close handle it, to avoid GTK cleanup issues + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.linux && shouldClose) { + // Hide window immediately + // Then exit after a short delay to allow any final cleanup + Future.delayed(const Duration(milliseconds: 200), () { + exit(0); + }); + // Return false to prevent flutter_window_close from closing the window + return false; + } + + return shouldClose; }); } else if (kIsWeb) { // Web platform: Use beforeunload event @@ -66,7 +86,8 @@ class _WindowCloseHandlerState extends State void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - // Dispose SDK when app is terminated or detached from UI (mobile platforms) + // Dispose SDK when app is terminated or detached from UI + // This applies to mobile platforms if (state == AppLifecycleState.detached) { _disposeSDKIfNeeded(); } @@ -151,6 +172,7 @@ class _WindowCloseHandlerState extends State void dispose() { // Clean up based on platform if (PlatformTuner.isNativeDesktop) { + // Desktop platforms: Remove flutter_window_close handler FlutterWindowClose.setWindowShouldCloseHandler(null); } else if (!kIsWeb) { // Mobile platforms: Remove lifecycle observer diff --git a/lib/sdk/widgets/window_close_handler_exit_stub.dart b/lib/sdk/widgets/window_close_handler_exit_stub.dart new file mode 100644 index 0000000000..abdc980178 --- /dev/null +++ b/lib/sdk/widgets/window_close_handler_exit_stub.dart @@ -0,0 +1,7 @@ +// Stub file for web platform - exit is not available on web +void exit(int code) { + // On web, we can't exit the process + // This should never be called as we check kIsWeb before using exit + throw UnsupportedError('exit() is not available on web platform'); +} + diff --git a/lib/services/feedback/custom_feedback_form.dart b/lib/services/feedback/custom_feedback_form.dart index 537260d9a9..91f28c038e 100644 --- a/lib/services/feedback/custom_feedback_form.dart +++ b/lib/services/feedback/custom_feedback_form.dart @@ -174,7 +174,7 @@ class _FeedbackTypeDropdown extends StatelessWidget { border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), validator: (value) => - value == null ? 'Please select a feedback type' : null, + value == null ? LocaleKeys.feedbackSelectTypeValidation.tr() : null, items: FeedbackType.values .map( (type) => DropdownMenuItem( diff --git a/lib/services/feedback/feedback_service.dart b/lib/services/feedback/feedback_service.dart index 8d2a7f1461..bd955a25d1 100644 --- a/lib/services/feedback/feedback_service.dart +++ b/lib/services/feedback/feedback_service.dart @@ -4,6 +4,8 @@ import 'package:get_it/get_it.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/services/feedback/feedback_provider.dart'; import 'package:web_dex/services/feedback/providers/cloudflare_feedback_provider.dart'; @@ -86,7 +88,7 @@ class FeedbackService { await provider.submitFeedback( description: feedback.text, screenshot: feedback.screenshot, - type: feedbackType ?? 'User Feedback', + type: feedbackType ?? LocaleKeys.feedbackDefaultType.tr(), metadata: metadata, ); return true; diff --git a/lib/services/file_loader/file_loader_native_desktop.dart b/lib/services/file_loader/file_loader_native_desktop.dart index 7f271c06cb..d626b2720c 100644 --- a/lib/services/file_loader/file_loader_native_desktop.dart +++ b/lib/services/file_loader/file_loader_native_desktop.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart' as p; import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/shared/utils/zip.dart'; @@ -15,7 +17,11 @@ class FileLoaderNativeDesktop implements FileLoader { }) async { switch (type) { case LoadFileType.text: - await _saveAsTextFile(fileName, data); + if (p.extension(fileName).toLowerCase() == '.json') { + await _saveAsJsonFile(fileName, data); + } else { + await _saveAsTextFile(fileName, data); + } case LoadFileType.compressed: await _saveAsCompressedFile(fileName, data); } @@ -46,13 +52,34 @@ class FileLoaderNativeDesktop implements FileLoader { } Future _saveAsTextFile(String fileName, String data) async { + final String suggestedName = + p.extension(fileName).isEmpty ? '$fileName.txt' : fileName; final String? fileFullPath = - await FilePicker.platform.saveFile(fileName: '$fileName.txt'); + await FilePicker.platform.saveFile(fileName: suggestedName); if (fileFullPath == null) return; final File file = File(fileFullPath)..createSync(recursive: true); await file.writeAsString(data); } + Future _saveAsJsonFile(String fileName, String data) async { + final String suggestedName = + p.extension(fileName).isEmpty ? '$fileName.json' : fileName; + final String? fileFullPath = + await FilePicker.platform.saveFile(fileName: suggestedName); + if (fileFullPath == null) return; + + String prettyData = data; + try { + final dynamic decoded = json.decode(data); + prettyData = const JsonEncoder.withIndent(' ').convert(decoded); + } catch (_) { + // If not valid JSON, keep original content + } + + final File file = File(fileFullPath)..createSync(recursive: true); + await file.writeAsString(prettyData); + } + Future _saveAsCompressedFile( String fileName, String data, diff --git a/lib/services/file_loader/file_loader_web.dart b/lib/services/file_loader/file_loader_web.dart index 65ed4a874c..361e215438 100644 --- a/lib/services/file_loader/file_loader_web.dart +++ b/lib/services/file_loader/file_loader_web.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:js_interop'; import 'package:web/web.dart' as web; @@ -17,7 +18,11 @@ class FileLoaderWeb implements FileLoader { }) async { switch (type) { case LoadFileType.text: - await _saveAsTextFile(filename: fileName, data: data); + if (fileName.toLowerCase().endsWith('.json')) { + await _saveAsJsonFile(filename: fileName, data: data); + } else { + await _saveAsTextFile(filename: fileName, data: data); + } case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); } @@ -51,6 +56,38 @@ class FileLoaderWeb implements FileLoader { } } + Future _saveAsJsonFile({ + required String filename, + required String data, + }) async { + String prettyData = data; + try { + final dynamic decoded = json.decode(data); + prettyData = const JsonEncoder.withIndent(' ').convert(decoded); + } catch (_) {} + + final dataArray = web.TextEncoder().encode(prettyData); + final blob = web.Blob( + [dataArray].toJS, + web.BlobPropertyBag(type: 'application/json'), + ); + + final url = web.URL.createObjectURL(blob); + + try { + final anchor = web.HTMLAnchorElement() + ..href = url + ..download = filename + ..style.display = 'none'; + web.document.body?.append(anchor); + anchor + ..click() + ..remove(); + } finally { + web.URL.revokeObjectURL(url); + } + } + Future _saveAsCompressedFile({ required String fileName, required String data, diff --git a/lib/services/file_loader/mobile/file_loader_native_android.dart b/lib/services/file_loader/mobile/file_loader_native_android.dart index c3b0c74d14..4c41c3d5b1 100644 --- a/lib/services/file_loader/mobile/file_loader_native_android.dart +++ b/lib/services/file_loader/mobile/file_loader_native_android.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart' as p; import 'package:web_dex/services/file_loader/file_loader.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/shared/utils/zip.dart'; @@ -18,7 +19,11 @@ class FileLoaderNativeAndroid implements FileLoader { }) async { switch (type) { case LoadFileType.text: - await _saveAsTextFile(fileName: fileName, data: data); + if (p.extension(fileName).toLowerCase() == '.json') { + await _saveAsJsonFile(fileName: fileName, data: data); + } else { + await _saveAsTextFile(fileName: fileName, data: data); + } case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); } @@ -31,8 +36,10 @@ class FileLoaderNativeAndroid implements FileLoader { // On mobile, the file bytes are used to create the file to be saved. // On desktop a file is created first, then a file is saved. final Uint8List fileBytes = utf8.encode(data); + final String suggestedName = + p.extension(fileName).isEmpty ? '$fileName.txt' : fileName; final String? fileFullPath = await FilePicker.platform.saveFile( - fileName: '$fileName.txt', + fileName: suggestedName, bytes: fileBytes, ); if (fileFullPath == null || fileFullPath.isEmpty == true) { @@ -40,6 +47,29 @@ class FileLoaderNativeAndroid implements FileLoader { } } + Future _saveAsJsonFile({ + required String fileName, + required String data, + }) async { + String prettyData = data; + try { + final dynamic decoded = json.decode(data); + prettyData = const JsonEncoder.withIndent(' ').convert(decoded); + } catch (_) {} + + final Uint8List fileBytes = utf8.encode(prettyData); + final String suggestedName = + p.extension(fileName).isEmpty ? '$fileName.json' : fileName; + final String? fileFullPath = await FilePicker.platform.saveFile( + fileName: suggestedName, + bytes: fileBytes, + ); + + if (fileFullPath == null || fileFullPath.isEmpty == true) { + log('error: output filepath for $fileName is empty'); + } + } + Future _saveAsCompressedFile({ required String fileName, required String data, diff --git a/lib/services/file_loader/mobile/file_loader_native_ios.dart b/lib/services/file_loader/mobile/file_loader_native_ios.dart index b5f0bdf224..616f870eef 100644 --- a/lib/services/file_loader/mobile/file_loader_native_ios.dart +++ b/lib/services/file_loader/mobile/file_loader_native_ios.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -30,7 +31,11 @@ class FileLoaderNativeIOS implements FileLoader { }) async { switch (type) { case LoadFileType.text: - await _saveAsTextFile(fileName: fileName, data: data); + if (path.extension(fileName).toLowerCase() == '.json') { + await _saveAsJsonFile(fileName: fileName, data: data); + } else { + await _saveAsTextFile(fileName: fileName, data: data); + } break; case LoadFileType.compressed: await _saveAsCompressedFile(fileName: fileName, data: data); @@ -43,7 +48,9 @@ class FileLoaderNativeIOS implements FileLoader { required String data, }) async { final directory = await getApplicationDocumentsDirectory(); - final filePath = path.join(directory.path, '$fileName.txt'); + final String suggestedName = + path.extension(fileName).isEmpty ? '$fileName.txt' : fileName; + final filePath = path.join(directory.path, suggestedName); final File file = File(filePath); await file.writeAsString(data); @@ -61,6 +68,27 @@ class FileLoaderNativeIOS implements FileLoader { ); } + Future _saveAsJsonFile({ + required String fileName, + required String data, + }) async { + final directory = await getApplicationDocumentsDirectory(); + final String suggestedName = + path.extension(fileName).isEmpty ? '$fileName.json' : fileName; + final filePath = path.join(directory.path, suggestedName); + + String prettyData = data; + try { + final dynamic decoded = json.decode(data); + prettyData = const JsonEncoder.withIndent(' ').convert(decoded); + } catch (_) {} + + final File file = File(filePath); + await file.writeAsString(prettyData); + + await Share.shareXFiles([XFile(file.path)]); + } + Future _saveAsCompressedFile({ required String fileName, required String data, diff --git a/lib/shared/utils/utils.dart b/lib/shared/utils/utils.dart index 8f8aa691ae..0044f665ec 100644 --- a/lib/shared/utils/utils.dart +++ b/lib/shared/utils/utils.dart @@ -44,7 +44,8 @@ Future copyToClipBoard( await Clipboard.setData(ClipboardData(text: payload)); if (!context.mounted) return; - final scaffoldMessenger = ScaffoldMessenger.maybeOf(context) ?? + final scaffoldMessenger = + ScaffoldMessenger.maybeOf(context) ?? ScaffoldMessenger.of(scaffoldKey.currentContext!); scaffoldMessenger.showSnackBar( SnackBar( @@ -57,9 +58,7 @@ Future copyToClipBoard( color: themeData.colorScheme.onPrimaryContainer, ), const SizedBox(width: 12.0), - Text( - message ?? LocaleKeys.clipBoard.tr(), - ), + Text(message ?? LocaleKeys.clipBoard.tr()), ], ), duration: const Duration(seconds: 2), @@ -141,10 +140,7 @@ Rational? fract2rat(Map? fract, [bool willLog = true]) { try { final String numerStr = fract['numer'].toString(); final String denomStr = fract['denom'].toString(); - final rat = Rational( - BigInt.parse(numerStr), - BigInt.parse(denomStr), - ); + final rat = Rational(BigInt.parse(numerStr), BigInt.parse(denomStr)); return rat; } catch (e) { if (willLog) { @@ -198,8 +194,8 @@ String getTxExplorerUrl(Coin coin, String txHash) { final hash = coin.type == CoinType.tendermint || coin.type == CoinType.tendermintToken - ? txHash.toUpperCase() - : txHash; + ? txHash.toUpperCase() + : txHash; return coin.need0xPrefixForTxHash && !hash.startsWith('0x') ? '$explorerUrl${explorerTxUrl}0x$hash' @@ -247,8 +243,8 @@ Future openUrl(Uri uri, {bool? inSeparateTab}) async { mode: inSeparateTab == null ? LaunchMode.platformDefault : inSeparateTab == true - ? LaunchMode.externalApplication - : LaunchMode.inAppWebView, + ? LaunchMode.externalApplication + : LaunchMode.inAppWebView, ); } @@ -261,8 +257,8 @@ Future launchURLString(String url, {bool? inSeparateTab}) async { mode: inSeparateTab == null ? LaunchMode.platformDefault : inSeparateTab == true - ? LaunchMode.externalApplication - : LaunchMode.inAppWebView, + ? LaunchMode.externalApplication + : LaunchMode.inAppWebView, ); } else { throw 'Could not launch $url'; @@ -396,6 +392,9 @@ Color getProtocolColor(CoinType type) { case CoinType.zhtlc: case CoinType.utxo: return const Color.fromRGBO(233, 152, 60, 1); + case CoinType.sia: + // Match SDK Sia subclass color: 0xFF29F06F + return const Color(0xFF29F06F); case CoinType.erc20: return const Color.fromRGBO(108, 147, 237, 1); case CoinType.smartChain: @@ -447,6 +446,7 @@ bool hasTxHistorySupport(Coin coin) { case CoinType.tendermint: case CoinType.tendermintToken: case CoinType.utxo: + case CoinType.sia: case CoinType.erc20: case CoinType.smartChain: case CoinType.bep20: @@ -477,6 +477,8 @@ String getNativeExplorerUrlByCoin(Coin coin, String? address) { case CoinType.tendermintToken: return '${coin.explorerUrl}account/$coinAddress'; + // Coins listed below are handled via legacy explorer URLs only. New + // protocols should prefer SDK explorer helpers instead of extending this. case CoinType.zhtlc: case CoinType.utxo: case CoinType.smartChain: @@ -495,6 +497,7 @@ String getNativeExplorerUrlByCoin(Coin coin, String? address) { case CoinType.ubiq: case CoinType.krc20: case CoinType.slp: + case CoinType.sia: return '${coin.explorerUrl}address/$coinAddress'; } } @@ -571,10 +574,7 @@ Future pauseWhile( } } -enum HashExplorerType { - address, - tx, -} +enum HashExplorerType { address, tx } Future confirmParentCoinDisable( BuildContext context, { @@ -615,8 +615,9 @@ Future confirmCoinDisableWithOrders( builder: (context) => AlertDialog( title: Text(LocaleKeys.disable.tr()), content: Text( - LocaleKeys.coinDisableOpenOrdersWarning - .tr(args: [ordersCount.toString(), coin]), + LocaleKeys.coinDisableOpenOrdersWarning.tr( + args: [ordersCount.toString(), coin], + ), ), actions: [ TextButton( @@ -638,27 +639,28 @@ void confirmBeforeDisablingCoin( BuildContext context, { void Function()? onConfirm, }) { - final tradingEntitiesBloc = - RepositoryProvider.of(context); + final tradingEntitiesBloc = RepositoryProvider.of( + context, + ); final bloc = context.read(); final childCoins = bloc.state.walletCoins.values .where((c) => c.parentCoin?.abbr == coin.abbr) .toList(); - final hasSwap = tradingEntitiesBloc.hasActiveSwap(coin.abbr) || + final hasSwap = + tradingEntitiesBloc.hasActiveSwap(coin.abbr) || childCoins.any((c) => tradingEntitiesBloc.hasActiveSwap(c.abbr)); if (hasSwap) { - InformationPopup( - context: context, - ) + InformationPopup(context: context) ..text = LocaleKeys.coinDisableSpan1.tr(args: [coin.abbr]) ..show(); return; } - final int openOrders = tradingEntitiesBloc.openOrdersCount(coin.abbr) + + final int openOrders = + tradingEntitiesBloc.openOrdersCount(coin.abbr) + childCoins.fold( 0, (sum, c) => sum + tradingEntitiesBloc.openOrdersCount(c.abbr), diff --git a/lib/shared/widgets/password_visibility_control.dart b/lib/shared/widgets/password_visibility_control.dart index 3145ebe572..4a1d33b66c 100644 --- a/lib/shared/widgets/password_visibility_control.dart +++ b/lib/shared/widgets/password_visibility_control.dart @@ -1,6 +1,3 @@ -import 'dart:async'; -import 'dart:math'; - import 'package:flutter/material.dart'; /// #644: We want the password to be obscured most of the time @@ -19,77 +16,33 @@ class PasswordVisibilityControl extends StatefulWidget { class _PasswordVisibilityControlState extends State { bool _isObscured = true; - Offset _tapStartPosition = const Offset(0, 0); - Timer? _timer; void _setObscureTo(bool isObscured) { if (!mounted) { return; } - _timer?.cancel(); setState(() { _isObscured = isObscured; }); widget.onVisibilityChange(_isObscured); } - bool _wasLongPressMoved(Offset position) { - final double distance = sqrt(pow(_tapStartPosition.dx - position.dx, 2) + - pow(_tapStartPosition.dy - position.dy, 2)); - return distance > 20; - } - @override Widget build(BuildContext context) { - return GestureDetector( - // NB: Both the long press and the tap start with `onTabDown`. - onTapDown: (TapDownDetails details) { - _tapStartPosition = details.globalPosition; - _setObscureTo(!_isObscured); - }, - // #644: Most users expect the eye to react to the taps (behaving as a toggle) - // whereas long press handling starts too late to produce any visible reaction. - // Flashing the password for a few seconds in order not to befuddle the users. - onTapUp: (TapUpDetails details) { - _timer = Timer(const Duration(seconds: 2), () { - _setObscureTo(true); - }); - }, - onLongPressStart: (LongPressStartDetails details) { - _timer?.cancel(); - }, - onLongPressEnd: (LongPressEndDetails details) { - _setObscureTo(true); - }, - - // #644: Fires when we press on the eye and *a few seconds later* drag the finger off screen. - onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { - if (_wasLongPressMoved(details.globalPosition)) { - _setObscureTo(true); - } - }, - // #644: Fires when we press on the eye and *immediately* drag the finger off screen. - onVerticalDragStart: (DragStartDetails details) { - _setObscureTo(true); - }, - onHorizontalDragStart: (DragStartDetails details) { - _setObscureTo(true); - }, - - child: InkWell( - mouseCursor: SystemMouseCursors.click, - child: SizedBox( - width: 60, - child: Icon( - _isObscured - ? Icons.visibility_off_outlined - : Icons.visibility_outlined, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withValues(alpha: 0.7)), - ), + return InkWell( + mouseCursor: SystemMouseCursors.click, + onTap: () => _setObscureTo(!_isObscured), + child: SizedBox( + width: 60, + child: Icon( + _isObscured + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.7)), ), ); } diff --git a/lib/views/dex/entity_details/swap/swap_details.dart b/lib/views/dex/entity_details/swap/swap_details.dart index f225a32783..16abd25683 100644 --- a/lib/views/dex/entity_details/swap/swap_details.dart +++ b/lib/views/dex/entity_details/swap/swap_details.dart @@ -12,11 +12,12 @@ import 'package:web_dex/shared/widgets/copied_text.dart'; /// button so users can easily copy it. This version uses static strings /// instead of translation keys for the “Swap UUID” label. class SwapDetails extends StatelessWidget { - const SwapDetails({Key? key, required this.swapStatus, required this.isFailed}) + const SwapDetails({Key? key, required this.swapStatus, required this.isFailed, this.belowUuid}) : super(key: key); final Swap swapStatus; final bool isFailed; + final Widget? belowUuid; @override Widget build(BuildContext context) { @@ -47,25 +48,7 @@ class SwapDetails extends StatelessWidget { ? swapStatus.makerAmount : swapStatus.takerAmount, swapId: swapStatus.uuid, - ), - // Swap UUID row - Padding( - padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Swap UUID'), - CopiedText( - copiedValue: swapStatus.uuid, - text: swapStatus.uuid, - isCopiedValueShown: false, - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - fontSize: 11, - iconSize: 14, - ), - ], - ), + belowUuid: belowUuid, ), const SizedBox(height: 20), Column( diff --git a/lib/views/dex/entity_details/swap/swap_details_page.dart b/lib/views/dex/entity_details/swap/swap_details_page.dart index 457c1c4149..dbe2a50a35 100644 --- a/lib/views/dex/entity_details/swap/swap_details_page.dart +++ b/lib/views/dex/entity_details/swap/swap_details_page.dart @@ -52,24 +52,27 @@ class _SwapDetailsPageState extends State { children: [ TradingDetailsHeader(title: _headerText), SwapProgressStatus(progress: _progress, isFailed: _isFailed), - SwapDetails(swapStatus: widget.swapStatus, isFailed: _isFailed), - const SizedBox(height: 20), - UiBorderButton( - width: 160, - height: 32, - borderWidth: 1, - borderColor: theme.custom.specificButtonBorderColor, - backgroundColor: theme.custom.specificButtonBackgroundColor, - fontWeight: FontWeight.w500, - text: 'Export swap data', - icon: _isExporting - ? const UiSpinner() - : Icon( - Icons.file_download, - color: Theme.of(context).textTheme.bodyMedium?.color, - size: 18, - ), - onPressed: _isExporting ? null : _exportSwapData, + SwapDetails( + swapStatus: widget.swapStatus, + isFailed: _isFailed, + belowUuid: UiBorderButton( + width: 160, + height: 32, + borderWidth: 0, + borderColor: theme.custom.subCardBackgroundColor, + backgroundColor: theme.custom.subCardBackgroundColor, + fontWeight: FontWeight.w500, + fontSize: 11, + text: LocaleKeys.exportSwapData.tr(), + icon: _isExporting + ? const UiSpinner() + : Icon( + Icons.file_download, + color: Theme.of(context).textTheme.bodyMedium?.color, + size: 18, + ), + onPressed: _isExporting ? null : _exportSwapData, + ), ), ], ); diff --git a/lib/views/dex/entity_details/trading_details_coin_pair.dart b/lib/views/dex/entity_details/trading_details_coin_pair.dart index 09d8c037ef..743bca12ad 100644 --- a/lib/views/dex/entity_details/trading_details_coin_pair.dart +++ b/lib/views/dex/entity_details/trading_details_coin_pair.dart @@ -3,8 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:rational/rational.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/coins_bloc/coins_repo.dart'; +import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/model/coin.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item.dart'; import 'package:web_dex/shared/widgets/coin_item/coin_item_size.dart'; @@ -19,6 +21,7 @@ class TradingDetailsCoinPair extends StatelessWidget { required this.relAmount, this.swapId, this.isOrder = false, + this.belowUuid, }) : super(key: key); final String baseCoin; final Rational baseAmount; @@ -26,6 +29,7 @@ class TradingDetailsCoinPair extends StatelessWidget { final Rational relAmount; final String? swapId; final bool isOrder; + final Widget? belowUuid; @override Widget build(BuildContext context) { @@ -81,7 +85,9 @@ class TradingDetailsCoinPair extends StatelessWidget { Flexible( child: CopiedText( key: Key('uuid-${swapId}'), - text: isOrder ? 'Order UUID: ${swapId}' : 'Swap UUID: ${swapId}', + text: isOrder + ? LocaleKeys.orderUuid.tr(args: [swapId]) + : LocaleKeys.swapUuid.tr(args: [swapId]), copiedValue: swapId, isCopiedValueShown: false, padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), @@ -92,6 +98,15 @@ class TradingDetailsCoinPair extends StatelessWidget { ), ], ), + if (swapId != null && belowUuid != null) ...[ + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + belowUuid!, + ], + ), + ], ], ), ); diff --git a/lib/views/dex/simple/confirm/taker_order_confirmation.dart b/lib/views/dex/simple/confirm/taker_order_confirmation.dart index fc00d471ac..95a2bba921 100644 --- a/lib/views/dex/simple/confirm/taker_order_confirmation.dart +++ b/lib/views/dex/simple/confirm/taker_order_confirmation.dart @@ -320,10 +320,23 @@ class _TakerOrderConfirmationState extends State { final walletType = authBloc.state.currentUser?.type ?? ''; final takerBloc = context.read(); final coinsRepo = RepositoryProvider.of(context); - final sellCoinObj = takerBloc.state.sellCoin!; - final buyCoinObj = coinsRepo.getCoin(takerBloc.state.selectedOrder!.coin); + final sellCoinObj = takerBloc.state.sellCoin; + final selectedOrder = takerBloc.state.selectedOrder; + final sellAmount = takerBloc.state.sellAmount; + + if (sellCoinObj == null || selectedOrder == null || sellAmount == null) { + takerBloc.add( + TakerAddError( + DexFormError(error: LocaleKeys.dexUnableToStartSwap.tr()), + ), + ); + takerBloc.add(TakerSetInProgress(false)); + return; + } + + final buyCoinObj = coinsRepo.getCoin(selectedOrder.coin); final sellCoin = sellCoinObj.abbr; - final buyCoin = buyCoinObj?.abbr ?? takerBloc.state.selectedOrder!.coin; + final buyCoin = buyCoinObj?.abbr ?? selectedOrder.coin; context.read().logEvent( SwapInitiatedEventData( asset: sellCoin, diff --git a/lib/views/settings/widgets/general_settings/show_swap_data.dart b/lib/views/settings/widgets/general_settings/show_swap_data.dart index ec65188ae8..4b44b5b874 100644 --- a/lib/views/settings/widgets/general_settings/show_swap_data.dart +++ b/lib/views/settings/widgets/general_settings/show_swap_data.dart @@ -103,8 +103,7 @@ class _ShowSwapDataState extends State { borderColor: theme.custom.specificButtonBorderColor, backgroundColor: theme.custom.specificButtonBackgroundColor, fontWeight: FontWeight.w500, - // Use a static label since no translation key exists for export. - text: 'Export swap data', + text: LocaleKeys.exportSwapData.tr(), icon: _isDownloading ? const UiSpinner() : Icon( diff --git a/lib/views/settings/widgets/security_settings/security_settings_page.dart b/lib/views/settings/widgets/security_settings/security_settings_page.dart index 14f8b06599..8991aaa723 100644 --- a/lib/views/settings/widgets/security_settings/security_settings_page.dart +++ b/lib/views/settings/widgets/security_settings/security_settings_page.dart @@ -91,10 +91,11 @@ class _SecuritySettingsPageState extends State { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => SecuritySettingsBloc( - SecuritySettingsState.initialState(), - kdfSdk: RepositoryProvider.of(context), - ), + create: + (context) => SecuritySettingsBloc( + SecuritySettingsState.initialState(), + kdfSdk: RepositoryProvider.of(context), + ), child: MultiBlocListener( listeners: [ // Listen for step changes to manage sensitive data cleanup @@ -111,8 +112,8 @@ class _SecuritySettingsPageState extends State { if (isMobile) { return _SecuritySettingsPageMobile( content: content, - onBackButtonPressed: () => - _handleBackButton(context, state.step), + onBackButtonPressed: + () => _handleBackButton(context, state.step), ); } return content; @@ -202,8 +203,8 @@ class _SecuritySettingsPageState extends State { /// This maintains backward compatibility with the existing seed phrase /// backup flow while the private key flow uses the new hybrid approach. Future onViewSeedPressed(BuildContext context) async { - final SecuritySettingsBloc securitySettingsBloc = context - .read(); + final SecuritySettingsBloc securitySettingsBloc = + context.read(); final String? pass = await walletPasswordDialog(context); if (pass == null) return; @@ -220,7 +221,7 @@ class _SecuritySettingsPageState extends State { _privKeys.clear(); final parentCoins = coinsBloc.state.walletCoins.values.where( - (coin) => !coin.id.isChildAsset, + (coin) => !coin.id.isChildAsset && coin.id.subClass != CoinSubClass.sia, ); for (final coin in parentCoins) { final result = await mm2Api.showPrivKey( @@ -252,10 +253,10 @@ class _SecuritySettingsPageState extends State { // _sdkPrivateKeys AFTER the dialog closes, we ensure the state is preserved when the widget rebuilds. Map>? fetchedKeys; bool isEmptyKeys = false; - + // Store SDK reference before async operations to avoid BuildContext usage across async gaps final sdk = context.sdk; - + final bool success = await walletPasswordDialogWithLoading( context, onPasswordValidated: (String password) async { @@ -305,16 +306,17 @@ class _SecuritySettingsPageState extends State { // Note: fetchedKeys is a reference to the same Map object, so we only set it to null // to remove the extra reference, not clear() which would clear the data used by _sdkPrivateKeys fetchedKeys = null; - + // Private keys are ready, show the private keys screen // ignore: use_build_context_synchronously context.read().add(const ShowPrivateKeysEvent()); } else { // Show error to user // Check if failure was due to empty private keys - final errorMessage = isEmptyKeys - ? LocaleKeys.privateKeysEmptyError.tr() - : LocaleKeys.privateKeyRetrievalFailed.tr(); + final errorMessage = + isEmptyKeys + ? LocaleKeys.privateKeysEmptyError.tr() + : LocaleKeys.privateKeyRetrievalFailed.tr(); // ignore: use_build_context_synchronously _showPrivateKeyError(context, errorMessage); } diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index d893ca6ca0..6e8d1cf110 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -25,6 +25,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_for import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/trezor_withdraw_progress_dialog.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; +import 'package:decimal/decimal.dart'; import 'package:web_dex/views/wallet/coin_details/transactions/transaction_details.dart'; bool _isMemoSupportedProtocol(Asset asset) { @@ -51,6 +52,7 @@ class WithdrawForm extends StatefulWidget { class _WithdrawFormState extends State { late final WithdrawFormBloc _formBloc; late final _sdk = context.read(); + bool _suppressPreviewError = false; late final _mm2Api = context.read(); @override @@ -78,6 +80,62 @@ class _WithdrawFormState extends State { value: _formBloc, child: MultiBlocListener( listeners: [ + BlocListener( + listenWhen: (prev, curr) => prev.previewError != curr.previewError && curr.previewError != null, + listener: (context, state) async { + // If a preview failed and the user entered essentially their entire + // spendable balance (but didn't select Max), offer to deduct the fee + // by switching to max withdrawal. + if (state.isMaxAmount) return; + + final spendable = state.selectedSourceAddress?.balance.spendable; + Decimal? entered; + try { + entered = Decimal.parse(state.amount); + } catch (_) { + entered = null; + } + + bool amountsMatchWithTolerance(Decimal a, Decimal b) { + // Use a tiny epsilon to account for formatting/rounding differences + const epsStr = '0.000000000000000001'; + final epsilon = Decimal.parse(epsStr); + final diff = (a - b).abs(); + return diff <= epsilon; + } + + if (spendable != null && entered != null && amountsMatchWithTolerance(entered, spendable)) { + if (mounted) setState(() { _suppressPreviewError = true; }); + final bloc = context.read(); + final agreed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(LocaleKeys.userActionRequired.tr()), + content: const Text( + 'Since you\'re sending your full amount, the network fee will be deducted from the amount. Do you agree?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(LocaleKeys.cancel.tr()), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(LocaleKeys.ok.tr()), + ), + ], + ), + ); + + if (mounted) setState(() { _suppressPreviewError = false; }); + + if (agreed == true) { + bloc.add(const WithdrawFormMaxAmountEnabled(true)); + bloc.add(const WithdrawFormPreviewSubmitted()); + } + } + }, + ), BlocListener( listenWhen: (prev, curr) => prev.step != curr.step && curr.step == WithdrawFormStep.success, @@ -141,6 +199,7 @@ class _WithdrawFormState extends State { ], child: WithdrawFormContent( onBackButtonPressed: widget.onBackButtonPressed, + suppressPreviewError: _suppressPreviewError, onSuccess: widget.onSuccess, ), ), @@ -150,9 +209,15 @@ class _WithdrawFormState extends State { class WithdrawFormContent extends StatelessWidget { final VoidCallback? onBackButtonPressed; + final bool suppressPreviewError; final VoidCallback onSuccess; - const WithdrawFormContent({required this.onSuccess, this.onBackButtonPressed, super.key}); + const WithdrawFormContent({ + required this.onSuccess, + required this.suppressPreviewError, + this.onBackButtonPressed, + super.key, + }); @override Widget build(BuildContext context) { @@ -187,7 +252,7 @@ class WithdrawFormContent extends StatelessWidget { Widget _buildStep(WithdrawFormStep step) { switch (step) { case WithdrawFormStep.fill: - return const WithdrawFormFillSection(); + return WithdrawFormFillSection(suppressPreviewError: suppressPreviewError); case WithdrawFormStep.confirm: return const WithdrawFormConfirmSection(); case WithdrawFormStep.success: @@ -502,7 +567,9 @@ class WithdrawResultDetails extends StatelessWidget { } class WithdrawFormFillSection extends StatelessWidget { - const WithdrawFormFillSection({super.key}); + final bool suppressPreviewError; + + const WithdrawFormFillSection({required this.suppressPreviewError, super.key}); @override Widget build(BuildContext context) { @@ -619,7 +686,7 @@ class WithdrawFormFillSection extends StatelessWidget { const SizedBox(height: 24), // TODO! Refactor to use Formz and replace with the appropriate // error state value. - if (state.hasPreviewError) + if (state.hasPreviewError && !suppressPreviewError) ErrorDisplay( message: LocaleKeys.withdrawPreviewError.tr(), detailedMessage: state.previewError!.message, diff --git a/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart b/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart index f09ebc4c6e..53fec7405c 100644 --- a/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart +++ b/lib/views/wallet/coins_manager/coins_manager_filters_dropdown.dart @@ -1,17 +1,16 @@ import 'package:app_theme/app_theme.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; import 'package:web_dex/app_config/app_config.dart'; import 'package:web_dex/bloc/auth_bloc/auth_bloc.dart'; -import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; import 'package:web_dex/bloc/coins_bloc/coins_bloc.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin_type.dart'; +import 'package:web_dex/model/coin.dart'; import 'package:web_dex/model/wallet.dart'; class CoinsManagerFiltersDropdown extends StatefulWidget { @@ -99,11 +98,12 @@ class _Dropdown extends StatelessWidget { return BlocBuilder( bloc: bloc, builder: (context, state) { - final List selectedCoinTypes = bloc.state.selectedCoinTypes; - final List listTypes = CoinType.values - .where((CoinType type) => _filterTypes(context, type)) - .toList(); - onTap(CoinType type) => + final List selectedCoinTypes = + bloc.state.selectedCoinTypes; + + final List listTypes = _buildFilterSubClasses(context); + + onTap(CoinSubClass type) => bloc.add(CoinsManagerCoinTypeSelect(type: type)); final bool isLongListTypes = listTypes.length > 2; @@ -139,26 +139,37 @@ class _Dropdown extends StatelessWidget { ); } - bool _filterTypes(BuildContext context, CoinType type) { + /// Builds the list of available protocol filters dynamically from the + /// underlying coins, so that new SDK coin types automatically appear + /// without UI changes. + List _buildFilterSubClasses(BuildContext context) { final coinsBloc = context.read(); final currentWallet = context.read().state.currentUser?.wallet; + Iterable coinsSource; + switch (currentWallet?.config.type) { case WalletType.iguana: case WalletType.hdwallet: - return coinsBloc.state.coins.values - .firstWhereOrNull((coin) => coin.type == type) != - null; + coinsSource = coinsBloc.state.coins.values; + break; case WalletType.trezor: - // In Trezor mode, hide filter options that have no coins in the - // currently visible list (after search/test-coin filters). - return bloc.state.coins - .firstWhereOrNull((coin) => coin.type == type) != - null; + // In Trezor mode, derive available filters from the currently + // visible list (after search/test-coin filters). + coinsSource = bloc.state.coins; + break; case WalletType.metamask: case WalletType.keplr: case null: - return false; + return const []; } + + final subclasses = coinsSource + .map((coin) => coin.id.subClass) + .toSet() + .toList() + ..sort((a, b) => a.formatted.compareTo(b.formatted)); + + return subclasses; } } @@ -170,11 +181,11 @@ class _DropdownItem extends StatelessWidget { required this.isWide, required this.onTap, }); - final CoinType type; + final CoinSubClass type; final bool isSelected; final bool isFirst; final bool isWide; - final Function(CoinType) onTap; + final Function(CoinSubClass) onTap; @override Widget build(BuildContext context) { @@ -210,7 +221,7 @@ class _DropdownItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - type.toCoinSubClass().formatted, + type.formatted, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, diff --git a/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart b/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart index a10cb84bb0..e793b627eb 100644 --- a/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart +++ b/lib/views/wallet/coins_manager/coins_manager_selected_types_list.dart @@ -3,12 +3,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:komodo_ui_kit/komodo_ui_kit.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/coins_manager/coins_manager_bloc.dart'; import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; -import 'package:web_dex/model/coin_type.dart'; -import 'package:web_dex/model/coin_utils.dart'; -import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/coins_manager/coins_manager_filter_type_label.dart'; class CoinsManagerSelectedTypesList extends StatelessWidget { @@ -18,7 +16,8 @@ class CoinsManagerSelectedTypesList extends StatelessWidget { Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); - return BlocSelector>( + return BlocSelector>( selector: (state) { return state.selectedCoinTypes; }, @@ -38,7 +37,8 @@ class CoinsManagerSelectedTypesList extends StatelessWidget { itemCount: types.length, itemBuilder: (BuildContext context, int index) { final type = types[index]; - final Color protocolColor = getProtocolColor(type); + final Color protocolColor = + type.color ?? themeData.colorScheme.primary; if (index == 0) { return Row( mainAxisSize: MainAxisSize.min, @@ -64,10 +64,10 @@ class CoinsManagerSelectedTypesList extends StatelessWidget { const SizedBox(width: 8), Flexible( child: CoinsManagerFilterTypeLabel( - text: getCoinTypeName(type), + text: type.formatted, backgroundColor: protocolColor, border: Border.all( - color: type == CoinType.smartChain + color: type == CoinSubClass.smartChain ? theme.custom.smartchainLabelBorderColor : protocolColor, ), @@ -85,10 +85,10 @@ class CoinsManagerSelectedTypesList extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), child: CoinsManagerFilterTypeLabel( - text: getCoinTypeName(type), + text: type.formatted, backgroundColor: protocolColor, border: Border.all( - color: type == CoinType.smartChain + color: type == CoinSubClass.smartChain ? theme.custom.smartchainLabelBorderColor : protocolColor, ), diff --git a/lib/views/wallet/wallet_page/common/assets_list.dart b/lib/views/wallet/wallet_page/common/assets_list.dart index dc91e49852..a0110ec301 100644 --- a/lib/views/wallet/wallet_page/common/assets_list.dart +++ b/lib/views/wallet/wallet_page/common/assets_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:web_dex/app_config/app_config.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'; @@ -95,9 +96,32 @@ class AssetsList extends StatelessWidget { groupedAssets.putIfAbsent(symbol, () => []).add(asset); } - return Map.fromEntries( - groupedAssets.entries.toList()..sort((a, b) => a.key.compareTo(b.key)), - ); + // Sort groups: priority tickers first (in order from list), then others (alphabetically) + final groups = groupedAssets.entries.toList(); + groups.sort((a, b) { + final String tickerA = a.key; + final String tickerB = b.key; + + final int indexA = unauthenticatedUserPriorityTickers.indexOf(tickerA); + final int indexB = unauthenticatedUserPriorityTickers.indexOf(tickerB); + + final bool aIsPriority = indexA != -1; + final bool bIsPriority = indexB != -1; + + // Priority tickers come first + if (aIsPriority && !bIsPriority) return -1; + if (!aIsPriority && bIsPriority) return 1; + + // If both are priority, sort by their order in the priority list + if (aIsPriority && bIsPriority) { + return indexA.compareTo(indexB); + } + + // If both are not priority, sort alphabetically + return tickerA.compareTo(tickerB); + }); + + return Map.fromEntries(groups); } /// Filters assets based on search phrase 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 3d48937738..57ae92d646 100644 --- a/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart +++ b/lib/views/wallet/wallet_page/wallet_main/wallet_main.dart @@ -423,6 +423,7 @@ class CoinListView extends StatelessWidget { ); case AuthorizeMode.hiddenLogin: case AuthorizeMode.noLogin: + // Assets are sorted by priority at the group level in AssetsList._groupAssetsByTicker() return AssetsList( useGroupedView: true, assets: context diff --git a/linux/my_application.cc b/linux/my_application.cc index be94af558c..b572d3c713 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -1,5 +1,6 @@ #include "my_application.h" +#include #include #ifdef GDK_WINDOWING_X11 #include @@ -27,6 +28,23 @@ static void on_window_destroy(GtkWidget* widget, gpointer user_data) { self->main_window = nullptr; } +// Workaround for Flutter Linux shutdown issue (#132404) +// This handler intercepts delete-event early to prevent GTK from destroying the window +// and triggering FlutterEngineRemoveView. When flutter_window_close confirms closing, +// it will return false to prevent standard closing, and we'll handle exit manually. +// This handler acts as a safety net in case delete-event is still triggered. +static gboolean on_window_delete_event(GtkWidget* widget, GdkEvent* event, gpointer user_data) { + (void)widget; // Unused + (void)event; // Unused + (void)user_data; // Unused + + // On Linux, we want flutter_window_close to handle the dialog first + // So we return FALSE to let the event propagate to flutter_window_close + // If flutter_window_close returns false (user cancelled), nothing happens + // If flutter_window_close returns true, it will be intercepted and we'll exit manually + return FALSE; +} + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -89,7 +107,12 @@ static void my_application_activate(GApplication* application) { gtk_widget_grab_focus(GTK_WIDGET(view)); self->main_window = window; + + // Connect destroy signal g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), self); + // Connect delete-event signal for graceful shutdown workaround + // This prevents the crash when closing the window on Linux + g_signal_connect(window, "delete-event", G_CALLBACK(on_window_delete_event), self); } // Implements GApplication::local_command_line. diff --git a/sdk b/sdk index 8fcf2079d8..8e18a50a58 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 8fcf2079d82dae7bd92f00652c430553ec8adfef +Subproject commit 8e18a50a582d341694501f452e695e32d406de91