Linearize Send Flow#2764
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an initial “linearized” Send flow in the popup, aligning with the desired UX of selecting a token first (unless pre-selected via query params), then choosing destination, then entering amount, with step transition animations and updated tests.
Changes:
- Added a new initial Send step (
SELECT_SOURCE_ASSET) and step transition animations (enter from bottom/right/left + dismiss/exit overlay). - Updated Send screens to support the new step order, including “My Accounts” recipient picking and persisting/displaying a
recipientName. - Updated unit and e2e tests to reflect the new flow and added supporting selectors/testids.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/views/Send/styles.scss | Adds CSS keyframes/classes for step transition animations and dismiss overlay behavior. |
| extension/src/popup/views/Send/index.tsx | Implements the new step state machine (token picker first unless query param preselect) and animation wiring. |
| extension/src/popup/views/tests/SendPayment.test.tsx | Re-enables Send tests, adds initial-step assertions, and skips legacy tests pending rewrite. |
| extension/src/popup/locales/pt/translation.json | Adds new i18n keys used by the refreshed Send UI (currently with untranslated values). |
| extension/src/popup/locales/en/translation.json | Adds new i18n keys used by the refreshed Send UI (including a new “don’t” variant key). |
| extension/src/popup/ducks/transactionSubmission.ts | Persists recipientName in redux transaction data and exposes saveRecipientName. |
| extension/src/popup/constants/send-payment.ts | Adds the SELECT_SOURCE_ASSET step to the send flow enum. |
| extension/src/popup/components/send/styles.scss | Updates SendAmount styling for the new card layout, inline asset selector, and percentage buttons. |
| extension/src/popup/components/send/SendTo/index.tsx | Updates destination step header and adds “My Accounts” list that can set recipientName. |
| extension/src/popup/components/send/SendDestinationAsset/styles.scss | Adjusts destination-asset selector styling (collectibles heading/section). |
| extension/src/popup/components/send/SendDestinationAsset/index.tsx | Removes token/collectibles tabs and renders tokens + collectibles section in one view; adds returnToAmount. |
| extension/src/popup/components/send/SendAmount/index.tsx | Refactors amount screen layout (recipient at top, inline asset selector, pct buttons, settings button). |
| extension/src/popup/components/send/AddressTile/index.tsx | Allows displaying recipientName as primary text with address/federation as secondary. |
| extension/src/popup/components/InternalTransaction/TokenList/index.tsx | Adds data-testid="token-list" to support updated test assertions. |
| extension/src/popup/components/InternalTransaction/SubmitTransaction/index.tsx | Displays recipientName (if set) in the transaction summary instead of only truncated address. |
| extension/src/popup/components/InternalTransaction/SubmitTransaction/hooks/useSubmitTxData.tsx | Avoids adding self-owned destinations to recent addresses. |
| extension/e2e-tests/test-fixtures.ts | Adds optional viewport sizing to the Playwright context fixture. |
| extension/e2e-tests/sendPayment.test.ts | Updates existing e2e flows for the new step order and adds new flow-specific e2e cases. |
Comments suppressed due to low confidence (1)
extension/src/popup/views/tests/SendPayment.test.tsx:31
- The same module ("popup/helpers/blockaid") is imported twice under two different identifiers (
BlockaidHelpersandBlockAidHelpers). This is redundant and makes the test harder to follow. Consolidate to a single import name and update references accordingly.
import * as ApiInternal from "@shared/api/internal";
import * as UseNetworkFees from "popup/helpers/useNetworkFees";
import * as BlockaidHelpers from "popup/helpers/blockaid";
import * as UseGetCollectibles from "helpers/hooks/useGetCollectibles";
import * as ExtensionMessaging from "@shared/api/helpers/extensionMessaging";
import * as TokenList from "@shared/api/helpers/token-list";
import {
TESTNET_NETWORK_DETAILS,
DEFAULT_NETWORKS,
MAINNET_NETWORK_DETAILS,
} from "@shared/constants/stellar";
import { APPLICATION_STATE as ApplicationState } from "@shared/constants/applicationState";
import { ROUTES } from "popup/constants/routes";
import { Send } from "popup/views/Send";
import { initialState as transactionSubmissionInitialState } from "popup/ducks/transactionSubmission";
import * as CheckSuspiciousAsset from "popup/helpers/checkForSuspiciousAsset";
import * as tokenPaymentActions from "popup/ducks/token-payment";
import * as GetIconHelper from "@shared/api/helpers/getIconUrlFromIssuer";
import * as BlockAidHelpers from "popup/helpers/blockaid";
jest.mock("lodash/debounce", () => jest.fn((fn) => fn));
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (2)
extension/src/popup/views/Send/hooks/useSendQueryParams.ts:119
- When
?assetis missing, the hook still dispatchessaveIsToken(false)/saveIsCollectible(false)even iftransactionData.assetis already set to a token/collectible. That can desyncassetvsisTokenafter navigation. Consider only resetting these flags when you also set the default asset, or derive them from the existingtransactionData.asset.
} else {
// Set default asset to native if not already set
if (!transactionData.asset) {
dispatch(saveAsset("native"));
}
dispatch(saveIsCollectible(false));
dispatch(saveIsToken(false));
}
extension/src/popup/locales/pt/translation.json:728
- The pt locale value for the smart-apostrophe key "You don’t have enough {{asset}} in your account" is currently English. This string is still referenced elsewhere (e.g., SwapAmount), so users will see English error text in Portuguese UI. Translate this value (or consolidate keys) to keep locale coverage consistent.
"You can close this screen, your transaction should be complete in less than a minute.": "Você pode fechar esta tela, sua transação deve estar completa em menos de um minuto.",
"You can define your own assets lists in Settings.": "Você pode definir suas próprias listas de ativos nas Configurações.",
"You don't have enough {{asset}} in your account": "Você não tem {{asset}} suficiente em sua conta",
"You don’t have enough {{asset}} in your account": "You don’t have enough {{asset}} in your account",
"You have no assets added.": "Você não tem ativos adicionados.",
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
extension/src/popup/views/tests/SendPayment.test.tsx:781
- This new test is declared at the top level (outside the
describe("Send", ...)block), so it won’t inherit the suite’sbeforeEachmocks (e.g.BlockaidHelpers.useScanTx, etc.). That can cause it to run with real implementations and become flaky/fail. Move it into the existingdescribe("Send")suite (or add the necessary setup/mocks locally).
| const hasError = sendDataState.state === RequestState.ERROR; | ||
| const isLoading = | ||
| sendDataState.state === RequestState.IDLE || | ||
| sendDataState.state === RequestState.LOADING; |
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
| extensionId, | ||
| context, | ||
| }) => { | ||
| test.slow(); |
There was a problem hiding this comment.
I notice we're using a lot of test.slow(). Why is this necessary? This isn't necessarily wrong, but it does triple the time a test has to run (15s vs 45s). If a test fails, you'll have to wait 45s for it terminate.
It's best to avoid this unless it's absolutely necessary to make sure our tests run and fail quickly
| await expect(page.getByTestId("send-amount-fee-display")).toHaveText( | ||
| "0.00001 XLM", | ||
| "0.0093338 XLM", | ||
| { timeout: 10000 }, |
There was a problem hiding this comment.
10s is a pretty long timeout. Shouldn't this happen pretty quickly if the API calls are stubbed?
| await page.getByTestId("nav-link-swap").click(); | ||
| await expect(page.getByTestId("AppHeaderPageTitle")).toContainText("Swap"); | ||
| await expect(page.getByTestId("swap-src-asset-tile")).toBeVisible({ | ||
| timeout: 15000, |
There was a problem hiding this comment.
similar comment about the timeouts here
| ["75%", 75], | ||
| ] as const; | ||
|
|
||
| const normalizeNumericString = (value: string) => { |
There was a problem hiding this comment.
NIT: it might be nice to move this helper and the 2 below this to the formatters helpers file so all the formatting utils are in 1 place
|
Occasionally, I'm seeing a loader briefly under the destination address bar that pushes everything down. It's not clear what it's loading: Screen.Recording.2026-05-15.at.5.21.40.PM.mov |
|
The number input in the Swap amount input isn't quite displaying correctly Screen.Recording.2026-05-15.at.5.25.21.PM.mov |
|
If you click Swap from the Asset Detail, pressing back goes back to the Account screen But, if you click Send from the Asset Detail, pressing back goes back to the Asset Detail screen Screen.Recording.2026-05-15.at.5.27.28.PM.mov |

Closes #2686
This PR linearizes the extension Send flow so users move more consistently from recipient selection to amount entry to review, with cleaner state handoff between steps.
It improves recipient UX by keeping Recents and My Accounts visible while searching, making Suggestions pressable like other destination options, and debouncing validation/search feedback to avoid premature errors.
It also hardens amount handling by preserving the exact source value when switching between fiat and token views, improving numeric sanitization, and fixing edge cases like formatted thousands leading to invalid amount states.
Additionally, it includes reliability and polish updates across query-param handling, submit safeguards, collectible grouping/styling, locale cleanup, and Send flow test coverage.
initial.flow.and.navigation.mov
input-validation-and-preservation.mov
changing-address-and-token.mov
fullscreen-smoke-test.mov
sending-to-federated-p1.mov
sending-to-federated-p2-and-recents.mov
Adding back the memo and fee row that was missing from previous videos: