Skip to content

Commit 6d1cc26

Browse files
committed
♻️ (llm) refactor useModularDrawerState
1 parent b8b0fa2 commit 6d1cc26

File tree

11 files changed

+592
-318
lines changed

11 files changed

+592
-318
lines changed

.changeset/rude-years-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"live-mobile": minor
3+
---
4+
5+
LLM - MAD - rework useModularDrawerState

apps/ledger-live-mobile/__tests__/handlers/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import marketHandlers from "./market";
22
import ledgerSyncHandlers from "./ledgerSync";
33
import cryptoIconsHandlers from "./crypto-icons";
4+
import supportedCvsHandlers from "./supportedCvs";
45

56
export const ALLOWED_UNHANDLED_REQUESTS = [
67
"ledger.statuspage.io",
@@ -10,4 +11,9 @@ export const ALLOWED_UNHANDLED_REQUESTS = [
1011
"https://crypto-assets-service.api.ledger.com/v1/partners?output=name,signature,public_key,public_key_curve&service_name=swap",
1112
];
1213

13-
export default [...marketHandlers, ...ledgerSyncHandlers, ...cryptoIconsHandlers];
14+
export default [
15+
...marketHandlers,
16+
...ledgerSyncHandlers,
17+
...cryptoIconsHandlers,
18+
...supportedCvsHandlers,
19+
];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { http, HttpResponse } from "msw";
2+
3+
const supportedCvsHandlers = [
4+
http.get("https://countervalues.live.ledger.com/v3/supported/crypto", () =>
5+
HttpResponse.json([]),
6+
),
7+
];
8+
9+
export default supportedCvsHandlers;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { renderHook, act } from "@tests/test-renderer";
2+
import { useDeviceNavigation } from "../useDeviceNavigation";
3+
import {
4+
arbitrumToken,
5+
mockBtcCryptoCurrency,
6+
} from "@ledgerhq/live-common/modularDrawer/__mocks__/currencies.mock";
7+
import { StepFlowManagerReturnType } from "../useModularDrawerFlowStepManager";
8+
import { NavigationProp } from "@react-navigation/native";
9+
import { ModularDrawerStep } from "../../types";
10+
11+
const mockNavigate = jest.fn();
12+
const mockNavigation: Partial<NavigationProp<Record<string, never>>> = {
13+
navigate: mockNavigate,
14+
};
15+
16+
jest.mock("@react-navigation/native", () => ({
17+
useNavigation: () => mockNavigation,
18+
NavigationContainer: ({ children }: { children: React.ReactNode }) => children,
19+
}));
20+
21+
describe("useDeviceNavigation", () => {
22+
beforeEach(() => {
23+
jest.clearAllMocks();
24+
});
25+
26+
const navigationStepManager: StepFlowManagerReturnType = {
27+
reset: jest.fn(),
28+
goToStep: jest.fn(),
29+
currentStep: ModularDrawerStep.Asset,
30+
currentStepIndex: 0,
31+
};
32+
33+
it("navigates to device with a crypto currency", () => {
34+
const onClose = jest.fn();
35+
const resetSelection = jest.fn();
36+
const { result } = renderHook(() =>
37+
useDeviceNavigation({ navigationStepManager, onClose, resetSelection }),
38+
);
39+
40+
const crypto = mockBtcCryptoCurrency;
41+
act(() => result.current.navigateToDeviceWithCurrency(crypto));
42+
43+
expect(onClose).toHaveBeenCalled();
44+
expect(resetSelection).toHaveBeenCalled();
45+
expect(navigationStepManager.reset).toHaveBeenCalled();
46+
expect(mockNavigate).toHaveBeenCalled();
47+
});
48+
49+
it("navigates to device with a token currency (uses parent)", () => {
50+
const onClose = jest.fn();
51+
const resetSelection = jest.fn();
52+
const { result } = renderHook(() =>
53+
useDeviceNavigation({ navigationStepManager, onClose, resetSelection }),
54+
);
55+
56+
const token = arbitrumToken;
57+
act(() => result.current.navigateToDeviceWithCurrency(token));
58+
59+
expect(mockNavigate).toHaveBeenCalled();
60+
});
61+
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { renderHook, act } from "@tests/test-renderer";
2+
import { useDrawerLifecycle } from "../useDrawerLifecycle";
3+
import { ModularDrawerStep } from "../../types";
4+
import type { StepFlowManagerReturnType } from "../useModularDrawerFlowStepManager";
5+
6+
jest.mock("../../analytics/useModularDrawerAnalytics", () => ({
7+
useModularDrawerAnalytics: () => ({
8+
trackModularDrawerEvent: jest.fn(),
9+
}),
10+
getCurrentPageName: () => "page",
11+
}));
12+
13+
describe("useDrawerLifecycle", () => {
14+
const navigationStepManager: StepFlowManagerReturnType = {
15+
currentStep: ModularDrawerStep.Account,
16+
currentStepIndex: 2,
17+
reset: jest.fn(),
18+
goToStep: jest.fn(),
19+
};
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
it("back button prefers network when available, else asset", () => {
26+
const backToAsset = jest.fn();
27+
const backToNetwork = jest.fn();
28+
const { result } = renderHook(() =>
29+
useDrawerLifecycle({
30+
flow: "test",
31+
navigationStepManager,
32+
canGoBackToAsset: true,
33+
canGoBackToNetwork: true,
34+
backToAsset,
35+
backToNetwork,
36+
resetSelection: jest.fn(),
37+
}),
38+
);
39+
40+
act(() => result.current.handleBackButton());
41+
expect(backToNetwork).toHaveBeenCalled();
42+
43+
const result2 = renderHook(() =>
44+
useDrawerLifecycle({
45+
flow: "test",
46+
navigationStepManager,
47+
canGoBackToAsset: true,
48+
canGoBackToNetwork: false,
49+
backToAsset,
50+
backToNetwork,
51+
resetSelection: jest.fn(),
52+
}),
53+
);
54+
act(() => result2.result.current.handleBackButton());
55+
expect(backToAsset).toHaveBeenCalled();
56+
});
57+
58+
it("close button resets and calls onClose", () => {
59+
const onClose = jest.fn();
60+
const resetSelection = jest.fn();
61+
const { result } = renderHook(() =>
62+
useDrawerLifecycle({
63+
flow: "test",
64+
navigationStepManager,
65+
canGoBackToAsset: false,
66+
canGoBackToNetwork: false,
67+
backToAsset: jest.fn(),
68+
backToNetwork: jest.fn(),
69+
onClose,
70+
resetSelection,
71+
}),
72+
);
73+
74+
act(() => result.current.handleCloseButton());
75+
expect(resetSelection).toHaveBeenCalled();
76+
expect(navigationStepManager.reset).toHaveBeenCalled();
77+
expect(onClose).toHaveBeenCalled();
78+
});
79+
});

apps/ledger-live-mobile/src/newArch/features/ModularDrawer/hooks/__tests__/useModularDrawerState.test.ts

Lines changed: 38 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
11
import { renderHook, act } from "@tests/test-renderer";
22
import { useModularDrawerState } from "../useModularDrawerState";
3-
import { ModularDrawerStep } from "../../types";
43
import {
54
mockBtcCryptoCurrency,
65
mockCurrenciesByProvider,
76
mockCurrencyIds,
8-
mockEthCryptoCurrency,
97
} from "@ledgerhq/live-common/modularDrawer/__mocks__/currencies.mock";
8+
import { NavigationProp } from "@react-navigation/native";
109

1110
const mockNavigate = jest.fn();
12-
const mockNavigation = {
11+
const mockNavigation: Partial<NavigationProp<Record<string, never>>> = {
1312
navigate: mockNavigate,
14-
goBack: jest.fn(),
15-
addListener: jest.fn(),
16-
removeListener: jest.fn(),
17-
setParams: jest.fn(),
18-
dispatch: jest.fn(),
19-
canGoBack: jest.fn(),
20-
isFocused: jest.fn(),
21-
getParent: jest.fn(),
22-
replace: jest.fn(),
23-
push: jest.fn(),
24-
pop: jest.fn(),
25-
popToTop: jest.fn(),
26-
reset: jest.fn(),
27-
getId: jest.fn(),
28-
getState: jest.fn(),
29-
setOptions: jest.fn(),
3013
};
3114

3215
jest.mock("@react-navigation/native", () => ({
@@ -37,13 +20,14 @@ jest.mock("@react-navigation/native", () => ({
3720
// Mock the useProviders hook
3821
const mockSetProviders = jest.fn();
3922
const mockGetNetworksFromProvider = jest.fn();
23+
const mockGetProvider: jest.Mock = jest.fn(() => null);
4024
jest.mock("../useProviders", () => ({
4125
useProviders: () => ({
4226
providers: null,
4327
setProviders: mockSetProviders,
4428
getNetworksFromProvider: mockGetNetworksFromProvider,
4529
}),
46-
getProvider: jest.fn(() => null),
30+
getProvider: (currency: unknown, providers: unknown) => mockGetProvider(currency, providers),
4731
}));
4832

4933
// Mock the modularDrawer utils
@@ -53,12 +37,14 @@ jest.mock("@ledgerhq/live-common/modularDrawer/utils/index", () => ({
5337
haveOneCommonProvider: jest.fn(() => false),
5438
}));
5539

56-
// Mock the useModularDrawerFlowStepManager to prevent infinite loops
40+
// Mock the useModularDrawerFlowStepManager to prevent navigation side effects
41+
const mockGoToStep = jest.fn();
42+
const mockResetStepManager = jest.fn();
5743
jest.mock("../useModularDrawerFlowStepManager", () => ({
5844
useModularDrawerFlowStepManager: () => ({
5945
currentStep: "asset",
60-
goToStep: jest.fn(),
61-
reset: jest.fn(),
46+
goToStep: mockGoToStep,
47+
reset: mockResetStepManager,
6248
}),
6349
}));
6450

@@ -67,6 +53,7 @@ jest.mock("../../analytics/useModularDrawerAnalytics", () => ({
6753
useModularDrawerAnalytics: () => ({
6854
trackModularDrawerEvent: jest.fn(),
6955
}),
56+
getCurrentPageName: () => "page",
7057
}));
7158

7259
describe("useModularDrawerState", () => {
@@ -89,22 +76,31 @@ describe("useModularDrawerState", () => {
8976
expect(result.current.availableNetworks).toEqual([]);
9077
});
9178

92-
it("should select an asset and go to correct step", () => {
79+
it("should handle asset selection and populate networks when multiple networks exist", () => {
80+
mockGetProvider.mockReturnValue({
81+
providerId: "provider1",
82+
currenciesByNetwork: [],
83+
});
84+
mockGetNetworksFromProvider.mockReturnValue(["ethereum", "bitcoin"]);
85+
9386
const { result } = renderHook(() =>
9487
useModularDrawerState({
95-
currencyIds: mockCurrencyIds,
88+
currencyIds: ["ethereum", "bitcoin"],
9689
currenciesByProvider: mockCurrenciesByProvider,
9790
flow: "test",
9891
}),
9992
);
93+
10094
act(() => {
101-
result.current.selectAsset(mockCurrency, [mockEthCryptoCurrency, mockBtcCryptoCurrency]);
95+
result.current.handleAsset(mockCurrency);
10296
});
97+
98+
expect(mockSetProviders).toHaveBeenCalled();
10399
expect(result.current.asset).toEqual(mockCurrency);
104-
expect(result.current.availableNetworks.length).toBeGreaterThan(0);
100+
expect(result.current.availableNetworks.length).toBeGreaterThan(1);
105101
});
106102

107-
it("should reset state", () => {
103+
it("should reset state on close", () => {
108104
const { result } = renderHook(() =>
109105
useModularDrawerState({
110106
currencyIds: mockCurrencyIds,
@@ -113,92 +109,45 @@ describe("useModularDrawerState", () => {
113109
}),
114110
);
115111
act(() => {
116-
result.current.selectAsset(mockCurrency, [mockEthCryptoCurrency]);
117-
result.current.selectNetwork(mockCurrency, mockEthCryptoCurrency);
118-
result.current.reset();
112+
result.current.handleAsset(mockCurrency);
119113
});
120-
expect(result.current.asset).toBeUndefined();
121-
expect(result.current.network).toBeUndefined();
122-
expect(result.current.availableNetworks).toEqual([]);
123-
});
124-
125-
it("should go back to asset step", () => {
126-
const { result } = renderHook(() =>
127-
useModularDrawerState({
128-
currencyIds: mockCurrencyIds,
129-
currenciesByProvider: mockCurrenciesByProvider,
130-
flow: "test",
131-
}),
132-
);
114+
expect(result.current.asset).toEqual(mockCurrency);
133115
act(() => {
134-
result.current.backToAsset();
116+
result.current.handleCloseButton();
135117
});
136-
// Test that the state is reset
137118
expect(result.current.asset).toBeUndefined();
138119
expect(result.current.network).toBeUndefined();
120+
expect(result.current.availableNetworks).toEqual([]);
139121
});
140122

141-
it("should go back to network step", () => {
123+
it("should expose back/close handlers", () => {
142124
const { result } = renderHook(() =>
143125
useModularDrawerState({
144126
currencyIds: mockCurrencyIds,
145127
currenciesByProvider: mockCurrenciesByProvider,
146128
flow: "test",
147129
}),
148130
);
149-
act(() => {
150-
result.current.backToNetwork();
151-
});
152-
// Test that network is cleared
153-
expect(result.current.network).toBeUndefined();
131+
expect(typeof result.current.handleBackButton).toBe("function");
132+
expect(typeof result.current.handleCloseButton).toBe("function");
154133
});
155134

156-
it("should handle back from Network step", () => {
135+
it("should compute hasOneCurrency as false with provided mocks", () => {
157136
const { result } = renderHook(() =>
158137
useModularDrawerState({
159-
currencyIds: mockCurrencyIds,
138+
currencyIds: ["bitcoin", "ethereum"],
160139
currenciesByProvider: mockCurrenciesByProvider,
161140
flow: "test",
162141
}),
163142
);
164-
act(() => {
165-
result.current.handleBack(ModularDrawerStep.Network);
166-
});
167-
// Test that the state is reset when going back from network
168-
expect(result.current.asset).toBeUndefined();
143+
expect(result.current.hasOneCurrency).toBe(false);
169144
});
170145

171-
it("should handle back from Account step with multiple networks", () => {
172-
const { result } = renderHook(() =>
173-
useModularDrawerState({
174-
currencyIds: mockCurrencyIds,
175-
currenciesByProvider: mockCurrenciesByProvider,
176-
flow: "test",
177-
}),
178-
);
179-
act(() => {
180-
result.current.selectAsset(mockCurrency, [mockEthCryptoCurrency, mockBtcCryptoCurrency]);
181-
result.current.handleBack(ModularDrawerStep.Account);
182-
});
183-
// Test that we go back to network step when multiple networks are available
184-
expect(result.current.network).toBeUndefined();
185-
});
146+
// Back/step navigation is tested at hook level in useStepNavigation.test.ts
186147

187-
it("should handle back from Account step with one network", () => {
188-
const { result } = renderHook(() =>
189-
useModularDrawerState({
190-
currencyIds: mockCurrencyIds,
191-
currenciesByProvider: mockCurrenciesByProvider,
192-
flow: "test",
193-
}),
194-
);
195-
act(() => {
196-
result.current.selectAsset(mockCurrency, [mockEthCryptoCurrency]);
197-
result.current.handleBack(ModularDrawerStep.Account);
198-
});
199-
// Test that we go back to asset step when only one network is available
200-
expect(result.current.asset).toBeUndefined();
201-
});
148+
// Covered in useStepNavigation tests
149+
150+
// Covered in useStepNavigation tests
202151

203152
it("should handle single currency flow", () => {
204153
const { result } = renderHook(() =>

0 commit comments

Comments
 (0)