Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5e7b094
add initial version of linearizing send flow
leofelix077 May 6, 2026
6a18093
update keyboard avoiding view behavior for android
leofelix077 May 7, 2026
ceaf935
adjust keyboard avoiding view behavior for android
leofelix077 May 7, 2026
18084bd
add recipient name passing to review flow
leofelix077 May 7, 2026
1c517e6
fix navigation, typigns and input handling from typing and pasting
leofelix077 May 8, 2026
134bf87
Merge branch 'main' into lf-linearize-send-flow
leofelix077 May 11, 2026
fc3ab77
fix typings and reset search state
leofelix077 May 11, 2026
8cd4860
Merge branch 'lf-linearize-send-flow' of github.com:stellar/freighter…
leofelix077 May 11, 2026
60dc585
add back caret and remove highlighted hidden input
leofelix077 May 11, 2026
af8e4cc
fix ui elements from figma design
leofelix077 May 11, 2026
293fb05
adjust CI for android
leofelix077 May 11, 2026
4573eae
test no-keyboard option for android CI
leofelix077 May 11, 2026
616aca2
revert android ci option
leofelix077 May 11, 2026
0dc713b
fix copilot comments for virtualization
leofelix077 May 11, 2026
077872a
remove limit 10 recent contacts and add virtualization back
leofelix077 May 12, 2026
e4abc7f
adjust UI for wallets and invalid message
leofelix077 May 12, 2026
05a868c
clean stale state on collectible close
leofelix077 May 12, 2026
2aa00f1
fix collectible send flow
leofelix077 May 12, 2026
a5f5181
fix send flows when coming from details screens
leofelix077 May 12, 2026
618e545
fix memoization for nav headers
leofelix077 May 12, 2026
a5fb723
replace navigate with push and add e2e test for send from details
leofelix077 May 18, 2026
c9357ce
fix merge conflicts and adapt code to handle federated address on new…
leofelix077 May 18, 2026
04446e0
adjust safeguards for address truncation and fix scroll for tokens in…
leofelix077 May 19, 2026
7b6f9a5
adjust ui tweaks from figma and keyboard dismiss
leofelix077 May 26, 2026
20b7838
Merge branch 'main' into lf-linearize-send-flow
leofelix077 May 26, 2026
ca57079
fix navigation, align ui with figma latest design
leofelix077 May 28, 2026
e1e542a
adjust logic to hide usd conversion and has no price
leofelix077 May 28, 2026
bc3d2a1
adjust logic to hide usd conversion and has no price
leofelix077 May 28, 2026
efafeca
Merge branch 'main' into lf-linearize-send-flow
leofelix077 May 29, 2026
7ec7a9d
adjust memo type clearing on transaction amount screen and remove unu…
leofelix077 May 29, 2026
887e27e
adjust comments on federation memo type tests
leofelix077 May 29, 2026
10803db
Merge branch 'lf-linearize-send-flow' of github.com:stellar/freighter…
leofelix077 May 29, 2026
9c51a3e
enhance transaction test for federation memo type
leofelix077 May 29, 2026
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
208 changes: 206 additions & 2 deletions __tests__/components/screens/SendScreen/SendSearchContacts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { NavigationContainer, RouteProp } from "@react-navigation/native";
Comment thread
leofelix077 marked this conversation as resolved.
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { screen, userEvent, waitFor } from "@testing-library/react-native";
import {
act,
fireEvent,
screen,
userEvent,
waitFor,
} from "@testing-library/react-native";
import { SendSearchContacts } from "components/screens/SendScreen";
import {
RootStackParamList,
SEND_PAYMENT_ROUTES,
SendPaymentStackParamList,
} from "config/routes";
import { Account } from "config/types";
import { useAuthenticationStore } from "ducks/auth";
import * as sendDuck from "ducks/sendRecipient";
import * as transactionSettingsDuck from "ducks/transactionSettings";
import { renderWithProviders } from "helpers/testUtils";
Expand Down Expand Up @@ -48,6 +56,7 @@ const mockLoadRecentAddresses = jest.fn();
const mockSearchAddress = jest.fn();
const mockAddRecentAddress = jest.fn();
const mockSetDestinationAddress = jest.fn();
const mockPrepareForSearch = jest.fn();
const mockReset = jest.fn();

// Create mock data
Expand Down Expand Up @@ -78,7 +87,12 @@ const getSendStoreMock = (overrides = {}) =>
searchAddress: mockSearchAddress,
addRecentAddress: mockAddRecentAddress,
setDestinationAddress: mockSetDestinationAddress,
prepareForSearch: mockPrepareForSearch,
resetSendRecipient: mockReset,
isSearching: false,
isValidDestination: false,
isDestinationFunded: null,
destinationAddress: "",
...overrides,
});

Expand All @@ -88,6 +102,7 @@ jest.mock("ducks/sendRecipient", () => ({

const getTransactionSettingsStoreMock = (overrides = {}) => ({
saveRecipientAddress: jest.fn(),
saveRecipientName: jest.fn(),
saveSelectedCollectibleDetails: jest.fn(),
selectedCollectibleDetails: { collectionAddress: "", tokenId: "" },
selectedTokenId: "",
Expand All @@ -97,6 +112,7 @@ const getTransactionSettingsStoreMock = (overrides = {}) => ({
jest.mock("ducks/transactionSettings", () => ({
useTransactionSettingsStore: jest.fn().mockReturnValue({
saveRecipientAddress: jest.fn(),
saveRecipientName: jest.fn(),
saveSelectedCollectibleDetails: jest.fn(),
selectedCollectibleDetails: { collectionAddress: "", tokenId: "" },
selectedTokenId: "",
Expand All @@ -107,6 +123,10 @@ jest.mock("ducks/qrData", () => ({
useQRDataStore: () => ({ clearQRData: jest.fn() }),
}));

jest.mock("ducks/auth", () => ({
useAuthenticationStore: jest.fn(),
}));

jest.mock("hooks/useInAppBrowser", () => ({
useInAppBrowser: () => ({ open: jest.fn() }),
}));
Expand Down Expand Up @@ -162,6 +182,7 @@ jest.mock("hooks/useAppTranslation", () => () => ({
"sendPaymentScreen.recents": "Recent",
"sendPaymentScreen.suggestions": "Suggestions",
"common.paste": "Paste",
"sendSearchContacts.myWallets": "My Wallets",
"sendSearchContacts.unfunded.title":
"The destination account doesn't exist",
"sendSearchContacts.unfunded.action":
Expand All @@ -172,6 +193,25 @@ jest.mock("hooks/useAppTranslation", () => () => ({
},
}));

const mockUseAuthenticationStore =
useAuthenticationStore as jest.MockedFunction<typeof useAuthenticationStore>;

const mockAccounts: Account[] = [
{
id: "wallet-1",
name: "My Second Wallet",
publicKey: "GBLS3IXAFSUWBSW3RXJMNXEGCHXEUL6VMBLFGVFPW47X2OL7BG7QQMUQ",
},
{
id: "wallet-2",
name: "Savings",
publicKey: "GACJYENHYW2LGHBNNGNZ4NCBGZYVTGTZM4CJLQIOQQ5IUZU3SYWOW5EK",
},
];

const activePublicKey =
"GDAS7BS4XKW27H2K5C25V6ZU46FCFGBTFQGFDZURAKVPA6QYQG4GTWBC";

describe("SendSearchContacts", () => {
beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -182,6 +222,10 @@ describe("SendSearchContacts", () => {
loadRecentAddresses: mockLoadRecentAddresses,
}),
);
mockUseAuthenticationStore.mockReturnValue({
allAccounts: mockAccounts,
account: { publicKey: activePublicKey } as any,
} as any);
});

it("renders correctly with the search input", async () => {
Expand Down Expand Up @@ -232,6 +276,8 @@ describe("SendSearchContacts", () => {
});

it("shows search suggestions when text is entered", async () => {
jest.useFakeTimers();

// Setup the mock to return search results for this specific test
jest.spyOn(sendDuck, "useSendRecipientStore").mockImplementation(
getSendStoreMock({
Expand All @@ -248,11 +294,68 @@ describe("SendSearchContacts", () => {
);

const input = await screen.findByPlaceholderText("Enter address");
await userEvent.type(input, "test");
fireEvent.changeText(input, "test");

expect(mockPrepareForSearch).toHaveBeenCalled();
expect(mockSearchAddress).not.toHaveBeenCalled();

act(() => {
jest.runAllTimers();
});

await waitFor(() => {
expect(mockSearchAddress).toHaveBeenCalledWith("test");
});

jest.useRealTimers();
});

it("keeps recents and wallets visible while typing an invalid address", async () => {
jest.spyOn(sendDuck, "useSendRecipientStore").mockImplementation(
getSendStoreMock({
searchResults: mockSearchResults,
recentAddresses: mockRecentAddresses,
loadRecentAddresses: mockLoadRecentAddresses,
isValidDestination: false,
}),
);

renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

const input = await screen.findByPlaceholderText("Enter address");
await userEvent.type(input, "GABC");

await waitFor(() => {
expect(screen.getByText("Recent Contact")).toBeTruthy();
expect(screen.getByText("My Second Wallet")).toBeTruthy();
});
});

it("hides recents and wallets once a valid destination is found", async () => {
jest.spyOn(sendDuck, "useSendRecipientStore").mockImplementation(
getSendStoreMock({
searchResults: mockSearchResults,
recentAddresses: mockRecentAddresses,
loadRecentAddresses: mockLoadRecentAddresses,
isValidDestination: true,
}),
);

renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.queryByText("Recent Contact")).toBeNull();
expect(screen.queryByText("My Second Wallet")).toBeNull();
expect(screen.getByText("Search Result")).toBeTruthy();
});
});

describe("unfunded destination notification", () => {
Expand Down Expand Up @@ -394,4 +497,105 @@ describe("SendSearchContacts", () => {
expect(screen.queryByText(unfundedTitle)).toBeNull();
});
});

describe("My Wallets section", () => {
beforeEach(() => {
jest.spyOn(sendDuck, "useSendRecipientStore").mockImplementation(
getSendStoreMock({
recentAddresses: [],
loadRecentAddresses: mockLoadRecentAddresses,
}),
);
mockUseAuthenticationStore.mockReturnValue({
allAccounts: mockAccounts,
account: { publicKey: activePublicKey } as any,
} as any);
});

it("renders rows for wallets other than active account", async () => {
renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.getByText("My Second Wallet")).toBeTruthy();
expect(screen.getByText("Savings")).toBeTruthy();
});
});

it("does not render active account row", async () => {
mockUseAuthenticationStore.mockReturnValue({
allAccounts: [
...mockAccounts,
{ id: "active", name: "Active Wallet", publicKey: activePublicKey },
],
account: { publicKey: activePublicKey } as any,
} as any);

renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.getByText("My Second Wallet")).toBeTruthy();
});
expect(screen.queryByText("Active Wallet")).toBeNull();
});

it("sets destination address when wallet row is tapped", async () => {
renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.getByText("My Second Wallet")).toBeTruthy();
});

await userEvent.press(screen.getByTestId("my-wallet-row-wallet-1"));

await waitFor(() => {
expect(mockSetDestinationAddress).toHaveBeenCalledWith(
"GBLS3IXAFSUWBSW3RXJMNXEGCHXEUL6VMBLFGVFPW47X2OL7BG7QQMUQ",
);
});
});

it("shows My Wallets header when other accounts exist", async () => {
renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.getByText("My Wallets")).toBeTruthy();
});
});

it("hides My Wallets section when there are no other accounts", async () => {
mockUseAuthenticationStore.mockReturnValue({
allAccounts: [
{ id: "active", name: "Active Wallet", publicKey: activePublicKey },
],
account: { publicKey: activePublicKey } as any,
} as any);

renderWithProviders(
<NavigationContainer>
<SendSearchContacts navigation={mockNavigation} route={mockRoute} />
</NavigationContainer>,
);

await waitFor(() => {
expect(screen.getByPlaceholderText("Enter address")).toBeTruthy();
});
expect(screen.queryByText("My Wallets")).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ jest.mock("hooks/useColors", () => ({
__esModule: true,
default: () => ({
themeColors: {
foreground: {
primary: "#000000",
text: {
Copy link
Copy Markdown
Collaborator Author

@leofelix077 leofelix077 May 12, 2026

Choose a reason for hiding this comment

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

Two findings from @sdfcharles.

Should be minor changes that will not affect the final code, but will adjust soon and publish another version

1 to adjust the spacing between the search bar and the recents / first header
and 1 to rework a bit the collectibles to have a horizontal rule and numbering on the right side, to have a more distinct view

Send image (1)

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.

Screen.Recording.2026-05-12.at.13.18.20.mov

secondary: "#000000",
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.

Also coming from discussion with @sdfcharles, to linearize the send collectible

Screen.Recording.2026-05-12.at.15.28.21.mov
Screen.Recording.2026-05-12.at.15.31.24.mov

},
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.

Also adjusting the flows when coming from the details screens

Screen.Recording.2026-05-12.at.16.22.56.mov

},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jest.mock("ducks/transactionSettings", () => ({
useTransactionSettingsStore: jest.fn(() => ({
recipientAddress:
"GA6SXIZIKLJHCZI2KEOBEUUOFMM4JUPPM2UTWX6STAWT25JWIEUFIMFF",
recipientName: "",
memo: "",
isMemoRequired: false,
transactionFee: "0.00001",
Expand Down Expand Up @@ -83,6 +84,15 @@ describe("SendReviewBottomSheet", () => {

expect(getByText("Test Account")).toBeTruthy();
});

it("displays recipient name above the truncated address when provided", () => {
const { getByText } = renderWithProviders(
<SendReviewBottomSheet {...defaultProps} recipientName="Account 2" />,
);

expect(getByText("Account 2")).toBeTruthy();
expect(getByText("GA6S...IMFF")).toBeTruthy();
});
});

describe("Security banner states", () => {
Expand Down
Loading
Loading