Skip to content

Refactor and unify the send & multisig-propose flow#541

Open
n13 wants to merge 9 commits into
mainfrom
refactor_send_flow
Open

Refactor and unify the send & multisig-propose flow#541
n13 wants to merge 9 commits into
mainfrom
refactor_send_flow

Conversation

@n13

@n13 n13 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

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 SendStrategy encapsulating everything that differs between them:

  • send_strategy.dartSendStrategy + 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).
  • Shared screens now take a strategy: select_recipient_screen.dart, input_amount_screen.dart, review_send_screen.dart, and a unified send_terminal_screen.dart (renamed from tx_submitted_screen.dart).
  • Deleted the duplicated multisig screens: propose_recipient_screen.dart, propose_amount_screen.dart, propose_review_screen.dart, propose_done_screen.dart.
  • Entry points updated: home_screen.dart, shared_address_action_sheet.dart.

Keystone signing seam: hardware signing is modeled as an orthogonal step via SendOutcome.NeedsHardwareSignature, threaded through keystone_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

  • The success screen now shows a "View in Explorer ↗" link built from the extrinsic hash returned by submission.
  • transaction_submission_service.dart now returns the extrinsic hash (it was already computed, just discarded); added an explorerImmediateTransactionUrl() helper.
  • Extracted a shared ExplorerLink widget 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:

  • labels the (still-disabled) button "Can't Self Transfer", and
  • shows a warning toaster once on entry.

Works for both flows (strategy.sourceAccountId resolves to the active account for sends, the multisig address for proposals).

4. Debug QR scanner button

  • Added a kDebugMode-only button to the shared QrScannerPage that returns a test address, so the send / swap / add-account flows can be exercised in the simulator where the camera is unavailable.
  • Shared 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 _setRecipient helper (the path paste already used). This screen was subsequently folded into the refactor above.

Test plan

  • Regular send — recipient (scan / paste / recent / type) → amount → review → submit → terminal shows "sent" with a working explorer link
  • Regular send via Keystone — review hands off to the QR sign flow → broadcasts → terminal + explorer link
  • Multisig propose — recipient → amount → review → propose → terminal; proposal shows as pending
  • Self-send — entering/scanning own address shows "Can't Self Transfer" + toaster, button stays disabled (both flows)
  • Explorer link — terminal, POS receipt, and tx/proposal detail sheets all open the correct explorer URL
  • Pay mode (payment-URL QR) — recipient + amount prefilled
  • Regular send flow visually matches the original Figma design

n13 added 5 commits June 25, 2026 16:52
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 n13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

  1. DRY: _estimateFeeAmount duplicated. The identical BigInt.from(1000) * NumberFormattingService.scaleFactorBigInt now lives in both regular_send_strategy.dart and multisig_propose_strategy.dart. Lift it to a shared location (a static on SendStrategy, or a fee constant).
  2. Regular send reuses a multisig-named l10n key. RegularSendStrategy.strings sets feeFetchFailedMessage: l10n.multisigProposeFeeFetchFailed. It renders fine, but a regular transfer surfacing a multisigPropose... string is a semantic mismatch and fragile if that copy changes. Consider a neutral sendFeeFetchFailed key.
  3. MultisigProposeStrategy finds the signer two ways. estimateFee uses a manual for loop; submit uses .firstWhere(... orElse: throw). Same lookup — collapse into one helper.
  4. Fail-early inconsistency. submit throws when the member account is missing, but estimateFee silently falls back to a zero-networkFee static breakdown when signer == null. If that's only meant to cover "accounts still loading", at least debugPrint to distinguish loading from genuinely-missing so a wrong/cheap estimate isn't shown silently.

Nits

  • review_send_screen.dart: mainAxisAlignment: MainAxisAlignment.spaceBetween is a no-op inside a SingleChildScrollView (carried over from the old _summarySection).
  • input_amount_screen._openReview: bails with sendInputAmountChecksumRequired even when the real cause is fee == 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; the shared_address_action_sheetInputAmountScreen deep-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 old if (_isFetchingFee) return;.
  • Returning the already-computed extrinsic hash from transaction_submission_service (instead of discarding it) to drive the explorer link.
  • ExplorerLink + explorerImmediateTransactionUrl removing three near-identical copies.
  • kDebugMode-gating the scanner debug button and sharing AppConstants.debugTestAddress (de-duping the hardcoded address).

Comment thread mobile-app/lib/l10n/app_en.arb Outdated
},

"componentQrScannerTitle": "Scan QR Code",
"componentQrScannerTitle": "X QR Code",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

X ???

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't really understand why we need this generation != _feeFetchGeneration.

Widget _successMark(AppColorsV2 colors) {
return Container(
width: containerSize,
height: containerSize,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah single value fine to hard code I guess

@dewabisma dewabisma left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants