Refactor and unify the send & multisig-propose flow#541
Conversation
Scanning an address left the send and multisig-propose recipient buttons stuck on "Enter address". Scan assigned the controller text inside setState, firing the change listener's nested setState mid-update, so validation never enabled the button. Route scan, paste, and recent through a shared _setRecipient helper that assigns the controller text outside setState (the same path paste already used), letting the listener drive validation. Also removes the duplicated assignment across the three input methods in both screens.
align with design, delete duplicated screens from msig
Revert an accidental debug edit that changed componentQrScannerTitle to "X QR Code" across the arb and generated localization files.
n13
left a comment
There was a problem hiding this comment.
Review
Solid, well-motivated refactor. Collapsing the duplicated send / multisig-propose screens behind a SendStrategy is the right call: the sealed SendOutcome / SendFee types are clean, and modeling the Keystone hand-off as SendNeedsHardwareSignature keeps the shared screens free of flow branching. Net −800 lines with three explorer-link copies and four screens removed is a real DRY win. CI (Analyze) passes.
Blocking (now fixed)
Accidental localization corruption — componentQrScannerTitle → "X QR Code". This looked like a stray debug edit that landed in app_en.arb, app_localizations.dart, and app_localizations_en.dart, shipping a broken QR-scanner title to users. I've pushed a fix restoring "Scan QR Code" (commit 22a4d369).
Non-blocking suggestions
- DRY:
_estimateFeeAmountduplicated. The identicalBigInt.from(1000) * NumberFormattingService.scaleFactorBigIntnow lives in bothregular_send_strategy.dartandmultisig_propose_strategy.dart. Lift it to a shared location (a static onSendStrategy, or a fee constant). - Regular send reuses a multisig-named l10n key.
RegularSendStrategy.stringssetsfeeFetchFailedMessage: l10n.multisigProposeFeeFetchFailed. It renders fine, but a regular transfer surfacing amultisigPropose...string is a semantic mismatch and fragile if that copy changes. Consider a neutralsendFeeFetchFailedkey. MultisigProposeStrategyfinds the signer two ways.estimateFeeuses a manualforloop;submituses.firstWhere(... orElse: throw). Same lookup — collapse into one helper.- Fail-early inconsistency.
submitthrows when the member account is missing, butestimateFeesilently falls back to a zero-networkFeestatic breakdown whensigner == null. If that's only meant to cover "accounts still loading", at leastdebugPrintto distinguish loading from genuinely-missing so a wrong/cheap estimate isn't shown silently.
Nits
review_send_screen.dart:mainAxisAlignment: MainAxisAlignment.spaceBetweenis a no-op inside aSingleChildScrollView(carried over from the old_summarySection).input_amount_screen._openReview: bails withsendInputAmountChecksumRequiredeven when the real cause isfee == null(button is disabled in that state, so not reachable, but the message would be wrong if it were).- Self-send guard only runs on
SelectRecipientScreen; theshared_address_action_sheet→InputAmountScreendeep-link path skips it. Likely out of scope, just flagging.
Nicely done
- Generation-counter (
_feeFetchGeneration) fix for stale/dropped fee responses is a real improvement over the oldif (_isFetchingFee) return;. - Returning the already-computed extrinsic hash from
transaction_submission_service(instead of discarding it) to drive the explorer link. ExplorerLink+explorerImmediateTransactionUrlremoving three near-identical copies.kDebugMode-gating the scanner debug button and sharingAppConstants.debugTestAddress(de-duping the hardcoded address).
| }, | ||
|
|
||
| "componentQrScannerTitle": "Scan QR Code", | ||
| "componentQrScannerTitle": "X QR Code", |
There was a problem hiding this comment.
its already fixed ;) was just a test
| final feeData = await balancesService.getBalanceTransferFee(displayAccount.account, toAddress, amount); | ||
| if (!mounted) return; | ||
| final fee = await widget.strategy.estimateFee(ref, recipient: widget.recipientAddress.trim(), amount: _amount); | ||
| if (!mounted || generation != _feeFetchGeneration) return; |
There was a problem hiding this comment.
I don't really understand why we need this generation != _feeFetchGeneration.
| Widget _successMark(AppColorsV2 colors) { | ||
| return Container( | ||
| width: containerSize, | ||
| height: containerSize, |
There was a problem hiding this comment.
Not sure why delete this var for repeated value?
| ), | ||
| alignment: Alignment.center, | ||
| child: Icon(Icons.check, size: iconSize, color: colors.success), | ||
| child: Icon(Icons.check, size: 32, color: colors.success), |
There was a problem hiding this comment.
Yeah single value fine to hard code I guess
dewabisma
left a comment
There was a problem hiding this comment.
Added some comments. Minor clarifications.
Is there any UI change for the send in multisig? Or it just merged into one flow now which mean the view shared the same. Because previously multisig has it's own distinct proposal details and success submit screen.
Summary
This branch started as a one-line fix for the scan→continue button bug, but the scope grew into a full refactor of the send experience. The regular send and multisig propose flows were near 1:1 duplicated screens; they are now unified behind a single set of screens driven by a
SendStrategy, and several related fixes/features came along with it.Net ~−800 lines (four duplicated multisig screens deleted), no new localization keys, and the regular send flow is visually unchanged from the original Figma design.
1. Unified send & multisig-propose flows (Strategy pattern)
The two flows (recipient → amount → review → done) were copy-pasted. They now share one implementation, with a
SendStrategyencapsulating everything that differs between them:send_strategy.dart—SendStrategy+ value types:SendStrings(per-flow labels),SendFee(RegularFee/ProposeFee),SendOutcome(Submitted/NeedsHardwareSignature/Failed),SendTerminalContent.regular_send_strategy.dart— single-signer transfer from the active account (local signing, or Keystone hand-off).multisig_propose_strategy.dart— multisig proposal (member affordability gating, propose-fee breakdown, proposal submission + provider invalidation).select_recipient_screen.dart,input_amount_screen.dart,review_send_screen.dart, and a unifiedsend_terminal_screen.dart(renamed fromtx_submitted_screen.dart).propose_recipient_screen.dart,propose_amount_screen.dart,propose_review_screen.dart,propose_done_screen.dart.home_screen.dart,shared_address_action_sheet.dart.Keystone signing seam: hardware signing is modeled as an orthogonal step via
SendOutcome.NeedsHardwareSignature, threaded throughkeystone_sign_screen.dart/keystone_scan_signature_screen.dart. This leaves room for multisig + Keystone later without adding another branch.2. "View in Explorer" on the terminal screen
transaction_submission_service.dartnow returns the extrinsic hash (it was already computed, just discarded); added anexplorerImmediateTransactionUrl()helper.ExplorerLinkwidget and reused it across the terminal, the POS receipt (pos_qr_screen.dart), and the transaction/proposal detail sheets — removing three near-duplicate copies.3. Self-send handling on the recipient screen
Entering or scanning your own address previously left the button silently disabled on "Enter address". Now it:
Works for both flows (
strategy.sourceAccountIdresolves to the active account for sends, the multisig address for proposals).4. Debug QR scanner button
kDebugMode-only button to the sharedQrScannerPagethat returns a test address, so the send / swap / add-account flows can be exercised in the simulator where the camera is unavailable.AppConstants.debugTestAddress, reused by the existing hardware-account debug fill.5. Original fix (scan → continue button)
Scan and paste took different code paths; scan assigned the controller text inside
setState, so the validation listener never enabled the button. Unified scan/paste/recent through a single_setRecipienthelper (the path paste already used). This screen was subsequently folded into the refactor above.Test plan