Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"transactionComplete": "Transaction complete!",
"transactionDenied": "Denied",
"coinDisableSpan1": "You can't disable {} while it has a swap in progress",
"confirmSending": "Confirm sending",
"confirmSending": "Confirm withdrawl",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix spelling in confirmation label.

“Confirm withdrawl” has a typo; please change it to “Confirm withdrawal” so the UI copy reads correctly.

-  "confirmSending": "Confirm withdrawl",
+  "confirmSending": "Confirm withdrawal",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"confirmSending": "Confirm withdrawl",
"confirmSending": "Confirm withdrawal",
🤖 Prompt for AI Agents
In assets/translations/en.json at line 66, the confirmation label has a typo
"Confirm withdrawl"; update the string value to "Confirm withdrawal" so the UI
copy reads correctly (i.e., replace withdrawl with withdrawal).

"confirmSend": "Confirm send",
"confirm": "Confirm",
"confirmed": "Confirmed",
Expand Down
61 changes: 40 additions & 21 deletions lib/bloc/withdraw_form/withdraw_form_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:komodo_defi_types/komodo_defi_types.dart';
import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart';
import 'package:web_dex/generated/codegen_loader.g.dart';
import 'package:web_dex/mm2/mm2_api/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';
Expand All @@ -21,12 +23,15 @@ import 'package:decimal/decimal.dart';
class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
final KomodoDefiSdk _sdk;
final WalletType? _walletType;
final Mm2Api _mm2Api;

WithdrawFormBloc({
required Asset asset,
required KomodoDefiSdk sdk,
required Mm2Api mm2Api,
WalletType? walletType,
}) : _sdk = sdk,
_mm2Api = mm2Api,
_walletType = walletType,
super(
WithdrawFormState(
Expand Down Expand Up @@ -471,34 +476,48 @@ class WithdrawFormBloc extends Bloc<WithdrawFormEvent, WithdrawFormState> {
state.copyWith(
isSending: true,
transactionError: () => null,
// No second device interaction is needed on confirm
isAwaitingTrezorConfirmation: false,
),
);

// Show Trezor progress message for hardware wallets
if (_walletType == WalletType.trezor) {
emit(state.copyWith(isAwaitingTrezorConfirmation: true));
final preview = state.preview;
if (preview == null) {
throw Exception('Missing withdrawal preview');
}

await for (final progress in _sdk.withdrawals.withdraw(
state.toWithdrawParameters(),
)) {
if (progress.status == WithdrawalStatus.complete) {
emit(
state.copyWith(
step: WithdrawFormStep.success,
result: () => progress.withdrawalResult,
isSending: false,
isAwaitingTrezorConfirmation: false,
),
);
return;
}
final response = await _mm2Api.sendRawTransaction(
SendRawTransactionRequest(
coin: preview.coin,
txHex: preview.txHex,
),
);

if (progress.status == WithdrawalStatus.error) {
throw Exception(progress.errorMessage);
}
if (response.txHash == null) {
throw Exception(response.error?.message ?? 'Broadcast failed');
}

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,
);

emit(
state.copyWith(
step: WithdrawFormStep.success,
result: () => result,
// Clear cached preview after successful broadcast
preview: () => null,
isSending: false,
isAwaitingTrezorConfirmation: false,
),
);
return;
} catch (e) {
// Capture FD snapshot when KDF withdrawal submission fails
if (PlatformTuner.isIOS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ class CoinsTableItem<T> extends StatelessWidget {
required this.coin,
this.isGroupHeader = false,
this.subtitleText,
this.trailing,
});

final T? data;
final Coin coin;
final Function(T) onSelect;
final bool isGroupHeader;
final String? subtitleText;
final Widget? trailing;

@override
Widget build(BuildContext context) {
Expand All @@ -35,7 +37,10 @@ class CoinsTableItem<T> extends StatelessWidget {
showNetworkLogo: !isGroupHeader,
),
const SizedBox(width: 8),
if (coin.isActive) CoinBalance(coin: coin, isVertical: true),
if (trailing != null)
trailing!
else if (coin.isActive)
CoinBalance(coin: coin, isVertical: true),
],
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:web_dex/common/screen.dart';
import 'package:web_dex/generated/codegen_loader.g.dart';
import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart';
import 'package:web_dex/model/coin.dart';
import 'package:web_dex/shared/utils/formatters.dart';
import 'package:web_dex/shared/utils/utils.dart';
import 'package:web_dex/views/dex/simple/form/tables/coins_table/coins_table_item.dart';

class GroupedListView<T> extends StatelessWidget {
Expand Down Expand Up @@ -64,6 +66,11 @@ class GroupedListView<T> extends StatelessWidget {
subtitleText: LocaleKeys.nNetworks.tr(
args: [group.value.length.toString()],
),
trailing: _GroupedCoinBalance(
coins: group.value
.map((item) => getCoin(context, item))
.toList(),
Comment on lines +69 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deduplicate coins before summing grouped balances

The header balance for grouped coins is built from group.value.map((item) => getCoin(context, item)).toList() and then summed in _GroupedCoinBalance. group.value contains every order in the group, so the same Coin object is inserted once per order. When a coin has multiple orders, its spendable and USD amounts are added repeatedly, inflating the totals in the group header. The list should be deduplicated by coin id before summing so each variant contributes its balance only once.

Useful? React with 👍 / 👎.

),
),
children: group.value
.map((item) => buildItem(context, item, onSelect))
Expand Down Expand Up @@ -128,3 +135,62 @@ class GroupedListView<T> extends StatelessWidget {
}
}
}

class _GroupedCoinBalance extends StatelessWidget {
const _GroupedCoinBalance({required this.coins});

final List<Coin> coins;

@override
Widget build(BuildContext context) {
final baseFont = Theme.of(context).textTheme.bodySmall;
final balanceStyle = baseFont?.copyWith(fontWeight: FontWeight.w500);

// Sum on-chain spendable balances for all variants
double totalSpendable = 0.0;
for (final coin in coins) {
totalSpendable +=
context.sdk.balances.lastKnown(coin.id)?.spendable.toDouble() ?? 0.0;
}

// Sum USD balances (uses cached lastKnown data)
double totalUsd = 0.0;
for (final coin in coins) {
totalUsd += coin.lastKnownUsdBalance(context.sdk) ?? 0.0;
}

// Show base ticker without protocol/segwit suffixes in group header
final abbr = abbr2Ticker(coins.first.abbr);

final children = [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: AutoScrollText(
text: doubleToString(totalSpendable),
style: balanceStyle,
textAlign: TextAlign.right,
),
),
Text(' $abbr', style: balanceStyle),
],
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 100),
child: Text(
totalUsd > 0 ? ' (${formatUsdValue(totalUsd)})' : '',
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
];

return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
}
65 changes: 37 additions & 28 deletions lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart';
import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart';
import 'package:web_dex/generated/codegen_loader.g.dart';
import 'package:web_dex/mm2/mm2_api/rpc/base.dart';
import 'package:web_dex/mm2/mm2_api/mm2_api.dart';
import 'package:web_dex/model/text_error.dart';
import 'package:web_dex/model/wallet.dart';
import 'package:web_dex/shared/utils/extensions/kdf_user_extensions.dart';
Expand All @@ -24,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:web_dex/views/wallet/coin_details/transactions/transaction_details.dart';

bool _isMemoSupportedProtocol(Asset asset) {
final protocol = asset.protocol;
Expand All @@ -49,6 +51,7 @@ class WithdrawForm extends StatefulWidget {
class _WithdrawFormState extends State<WithdrawForm> {
late final WithdrawFormBloc _formBloc;
late final _sdk = context.read<KomodoDefiSdk>();
late final _mm2Api = context.read<Mm2Api>();

@override
void initState() {
Expand All @@ -58,6 +61,7 @@ class _WithdrawFormState extends State<WithdrawForm> {
_formBloc = WithdrawFormBloc(
asset: widget.asset,
sdk: _sdk,
mm2Api: _mm2Api,
walletType: walletType,
);
}
Expand All @@ -77,7 +81,7 @@ class _WithdrawFormState extends State<WithdrawForm> {
BlocListener<WithdrawFormBloc, WithdrawFormState>(
listenWhen: (prev, curr) =>
prev.step != curr.step && curr.step == WithdrawFormStep.success,
listener: (context, state) {
listener: (context, state) async {
final authBloc = context.read<AuthBloc>();
final walletType = authBloc.state.currentUser?.type ?? '';
context.read<AnalyticsBloc>().logEvent(
Expand All @@ -88,7 +92,6 @@ class _WithdrawFormState extends State<WithdrawForm> {
hdType: walletType,
),
);
widget.onSuccess();
},
),
BlocListener<WithdrawFormBloc, WithdrawFormState>(
Expand Down Expand Up @@ -138,6 +141,7 @@ class _WithdrawFormState extends State<WithdrawForm> {
],
child: WithdrawFormContent(
onBackButtonPressed: widget.onBackButtonPressed,
onSuccess: widget.onSuccess,
),
),
);
Expand All @@ -146,8 +150,9 @@ class _WithdrawFormState extends State<WithdrawForm> {

class WithdrawFormContent extends StatelessWidget {
final VoidCallback? onBackButtonPressed;
final VoidCallback onSuccess;

const WithdrawFormContent({this.onBackButtonPressed, super.key});
const WithdrawFormContent({required this.onSuccess, this.onBackButtonPressed, super.key});

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -186,7 +191,7 @@ class WithdrawFormContent extends StatelessWidget {
case WithdrawFormStep.confirm:
return const WithdrawFormConfirmSection();
case WithdrawFormStep.success:
return const WithdrawFormSuccessSection();
return WithdrawFormSuccessSection(onDone: onSuccess);
case WithdrawFormStep.failed:
return const WithdrawFormFailedSection();
}
Expand Down Expand Up @@ -696,7 +701,7 @@ class WithdrawFormConfirmSection extends StatelessWidget {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(LocaleKeys.confirm.tr()),
: Text(LocaleKeys.send.tr()),
),
),
],
Expand All @@ -709,34 +714,38 @@ class WithdrawFormConfirmSection extends StatelessWidget {
}

class WithdrawFormSuccessSection extends StatelessWidget {
const WithdrawFormSuccessSection({super.key});
final VoidCallback onDone;

const WithdrawFormSuccessSection({required this.onDone, super.key});

@override
Widget build(BuildContext context) {
return BlocBuilder<WithdrawFormBloc, WithdrawFormState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.check_circle_outline,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
LocaleKeys.transactionSuccessful.tr(),
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
WithdrawResultDetails(result: state.result!),
const SizedBox(height: 24),
FilledButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(LocaleKeys.done.tr()),
),
],
// Build a temporary Transaction model matching history view expectations
final result = state.result!;
final tx = Transaction(
id: result.txHash,
internalId: result.txHash,
assetId: state.asset.id,
balanceChanges: result.balanceChanges,
// Show as unconfirmed initially
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
confirmations: 0,
blockHeight: 0,
from: state.selectedSourceAddress != null
? [state.selectedSourceAddress!.address]
: <String>[],
to: [result.toAddress],
txHash: result.txHash,
fee: result.fee,
memo: state.memo,
);

return TransactionDetails(
transaction: tx,
coin: state.asset.toCoin(),
onClose: onDone,
);
Comment on lines +728 to 749
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid showing a 1970 timestamp in the success details.

Setting timestamp to DateTime.fromMillisecondsSinceEpoch(0) makes the TransactionDetails panel render “Jan 1, 1970” for a brand-new withdrawal, which is misleading for users. Feed a real timestamp (e.g., DateTime.now()) or adjust the widget to treat missing timestamps specially so the success view reflects the actual submission time.

-          timestamp: DateTime.fromMillisecondsSinceEpoch(0),
+          timestamp: DateTime.now(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: result.txHash,
internalId: result.txHash,
assetId: state.asset.id,
balanceChanges: result.balanceChanges,
// Show as unconfirmed initially
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
confirmations: 0,
blockHeight: 0,
from: state.selectedSourceAddress != null
? [state.selectedSourceAddress!.address]
: <String>[],
to: [result.toAddress],
txHash: result.txHash,
fee: result.fee,
memo: state.memo,
);
return TransactionDetails(
transaction: tx,
coin: state.asset.toCoin(),
onClose: onDone,
);
id: result.txHash,
internalId: result.txHash,
assetId: state.asset.id,
balanceChanges: result.balanceChanges,
// Show as unconfirmed initially
timestamp: DateTime.now(),
confirmations: 0,
blockHeight: 0,
from: state.selectedSourceAddress != null
? [state.selectedSourceAddress!.address]
: <String>[],
to: [result.toAddress],
txHash: result.txHash,
fee: result.fee,
memo: state.memo,
);
return TransactionDetails(
transaction: tx,
coin: state.asset.toCoin(),
onClose: onDone,
);
🤖 Prompt for AI Agents
In lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart around lines
728 to 749, the code creates a Transaction with timestamp set to
DateTime.fromMillisecondsSinceEpoch(0) which displays Jan 1, 1970; change the
timestamp to a real submission time (e.g., DateTime.now()) or supply null if
Transaction.timestamp is nullable and let TransactionDetails handle missing
timestamps; update the Transaction construction to pass DateTime.now() (or null)
and ensure any downstream types accept the change.

},
);
Expand Down
Loading