Skip to content

Commit 8ce386f

Browse files
feat: implement settings for dont show again + fix reset memo on navigation back + different fixes
1 parent e606b34 commit 8ce386f

File tree

14 files changed

+181
-77
lines changed

14 files changed

+181
-77
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"ledger-live-desktop": minor
3+
"@ledgerhq/live-common": minor
4+
---
5+
6+
Feature/live 20724 [LLD][UI] Memo screen

apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -70,24 +70,6 @@ export function SendHeader() {
7070
const currencyName = state.account.currency?.ticker ?? "";
7171
const availableText = useAvailableBalance(state.account.account);
7272

73-
const handleBack = useCallback(() => {
74-
if (navigation.canGoBack()) {
75-
// Reset amount-related state when leaving Amount step (back navigation),
76-
// otherwise the transaction amount can persist while the UI remounts empty.
77-
if (currentStep === SEND_FLOW_STEP.AMOUNT) {
78-
transaction.updateTransaction(tx => ({
79-
...tx,
80-
amount: new BigNumber(0),
81-
useAllAmount: false,
82-
feesStrategy: null,
83-
}));
84-
}
85-
navigation.goToPreviousStep();
86-
} else {
87-
close();
88-
}
89-
}, [close, currentStep, navigation, transaction]);
90-
9173
const showBackButton = navigation.canGoBack();
9274
const showTitle = currentStepConfig?.showTitle !== false;
9375

@@ -96,25 +78,12 @@ export function SendHeader() {
9678
showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : "";
9779

9880
const showRecipientInput = currentStepConfig?.addressInput ?? false;
99-
const isRecipientStep = currentStep === SEND_FLOW_STEP.RECIPIENT;
10081
const isAmountStep = currentStep === SEND_FLOW_STEP.AMOUNT;
10182

10283
const addressInputValue = useMemo(() => {
103-
if (isRecipientStep) return recipientSearch.value;
10484
if (isAmountStep) return getRecipientDisplayValue(state.recipient);
10585
return recipientSearch.value;
106-
}, [isRecipientStep, isAmountStep, recipientSearch.value, state.recipient]);
107-
108-
const handleRecipientInputClick = useCallback(() => {
109-
if (!isAmountStep) return;
110-
111-
const prefillValue = getRecipientSearchPrefillValue(state.recipient);
112-
if (prefillValue) {
113-
recipientSearch.setValue(prefillValue);
114-
}
115-
116-
handleBack();
117-
}, [handleBack, isAmountStep, recipientSearch, state.recipient]);
86+
}, [isAmountStep, recipientSearch.value, state.recipient]);
11887

11988
const showMemoControls = Boolean(
12089
showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0,
@@ -139,7 +108,7 @@ export function SendHeader() {
139108
memoType,
140109
memoTypeOptions,
141110
onMemoChange: memo => {
142-
transaction.setRecipient({ memo });
111+
transaction.setRecipient({ ...state.recipient, memo });
143112
},
144113
onMemoSkip: () => {
145114
navigation.goToNextStep();
@@ -149,6 +118,37 @@ export function SendHeader() {
149118
}`,
150119
});
151120

121+
const handleBack = useCallback(() => {
122+
if (navigation.canGoBack()) {
123+
// Reset amount-related state when leaving Amount step (back navigation),
124+
// otherwise the transaction amount can persist while the UI remounts empty.
125+
if (currentStep === SEND_FLOW_STEP.AMOUNT) {
126+
transaction.updateTransaction(tx => ({
127+
...tx,
128+
amount: new BigNumber(0),
129+
useAllAmount: false,
130+
feesStrategy: null,
131+
}));
132+
133+
memoViewModel.resetViewState();
134+
}
135+
navigation.goToPreviousStep();
136+
} else {
137+
close();
138+
}
139+
}, [close, currentStep, memoViewModel, navigation, transaction]);
140+
141+
const handleRecipientInputClick = useCallback(() => {
142+
if (!isAmountStep) return;
143+
144+
const prefillValue = getRecipientSearchPrefillValue(state.recipient);
145+
if (prefillValue) {
146+
recipientSearch.setValue(prefillValue);
147+
}
148+
149+
handleBack();
150+
}, [handleBack, isAmountStep, recipientSearch, state.recipient]);
151+
152152
const transactionErrorName = state.transaction.status?.errors?.transaction?.name;
153153

154154
const recipientInputContent = useMemo(() => {
@@ -173,8 +173,8 @@ export function SendHeader() {
173173
return (
174174
<>
175175
<AddressInput
176-
className="-mt-12 mb-24 px-24"
177-
defaultValue={addressInputValue}
176+
className="-mt-12 mb-12 px-24"
177+
value={addressInputValue}
178178
onChange={e => recipientSearch.setValue(e.target.value)}
179179
onClear={recipientSearch.clear}
180180
placeholder={
@@ -228,13 +228,13 @@ export function SendHeader() {
228228
t,
229229
showMemoControls,
230230
currencyId,
231+
memoViewModel.showMemoValueInput,
232+
memoViewModel.showSkipMemo,
231233
memoViewModel.hasMemoTypeOptions,
232234
memoViewModel.memo.type,
233235
memoViewModel.memo.value,
234236
memoViewModel.onMemoTypeChange,
235-
memoViewModel.showMemoValueInput,
236237
memoViewModel.onMemoValueChange,
237-
memoViewModel.showSkipMemo,
238238
memoViewModel.skipMemoState,
239239
memoViewModel.onSkipMemoRequestConfirm,
240240
memoViewModel.onSkipMemoCancelConfirm,

apps/ledger-live-desktop/src/mvvm/features/Send/components/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type RecipientLike = Readonly<{
55
ensName?: string;
66
}>;
77

8-
export function getRecipientDisplayValue(recipient: RecipientLike | null): string {
8+
export function getRecipientDisplayValue(recipient: RecipientLike | null): string | undefined {
99
if (!recipient) return "";
1010

1111
const formattedAddress = formatAddress(recipient.address, {
@@ -20,7 +20,9 @@ export function getRecipientDisplayValue(recipient: RecipientLike | null): strin
2020
return formattedAddress;
2121
}
2222

23-
export function getRecipientSearchPrefillValue(recipient: RecipientLike | null): string {
23+
export function getRecipientSearchPrefillValue(
24+
recipient: RecipientLike | null,
25+
): string | undefined {
2426
if (!recipient) return "";
2527
return recipient.ensName?.trim() ? recipient.ensName : recipient.address;
2628
}

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function RecipientScreen() {
2121
const handleAddressSelected = useCallback(
2222
(address: string, ensName?: string, goToNextStep?: boolean) => {
2323
transaction.setRecipient({
24+
...state.recipient,
2425
address,
2526
ensName,
2627
});
@@ -29,7 +30,7 @@ export function RecipientScreen() {
2930
navigation.goToNextStep();
3031
}
3132
},
32-
[transaction, navigation],
33+
[transaction, state.recipient, navigation],
3334
);
3435

3536
if (!account || !currency) {

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function AddressMatchedSection({
5151
return `${ensName} (${formattedAddress})`;
5252
};
5353

54-
const getRecentDescription = (): string => {
54+
const getRecentDescription = (): string | undefined => {
5555
if (matchedRecentAddress) {
5656
return `Already used · ${formatRelativeDate(matchedRecentAddress.lastUsedAt)}`;
5757
}

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type SkipMemoSectionProps = Readonly<{
1111
state: SkipMemoState;
1212
onRequestConfirm: () => void;
1313
onCancelConfirm: () => void;
14-
onConfirm: () => void;
14+
onConfirm: (doNotAskAgain: boolean) => void;
1515
}>;
1616

1717
function SkipMemoSectionComponent({
@@ -36,14 +36,18 @@ function SkipMemoSectionComponent({
3636
setDoNotAskAgain(prev => !prev);
3737
}, []);
3838

39+
const handleOnSkipConfirmed = useCallback(() => {
40+
onConfirm(doNotAskAgain);
41+
}, [onConfirm, doNotAskAgain]);
42+
3943
if (state === "propose") {
4044
return (
4145
<div className="mt-16">
42-
<span style={{ fontSize: 14 }}>
46+
<span className="body-2 text-base">
4347
{t("newSendFlow.skipMemo.notRequired", { memoLabel })}
4448
&nbsp;
4549
</span>
46-
<Link underline appearance="accent" size="sm" onClick={onRequestConfirm}>
50+
<Link className="body-2" underline appearance="accent" size="sm" onClick={onRequestConfirm}>
4751
{t("common.skip")}
4852
</Link>
4953
</div>
@@ -60,7 +64,7 @@ function SkipMemoSectionComponent({
6064
onClose={onCancelConfirm}
6165
closeAriaLabel="Close banner"
6266
primaryAction={
63-
<Button appearance="transparent" size="sm" onClick={onConfirm}>
67+
<Button appearance="transparent" size="sm" onClick={handleOnSkipConfirmed}>
6468
{t("newSendFlow.skipMemo.confirm")}
6569
</Button>
6670
}
@@ -70,15 +74,11 @@ function SkipMemoSectionComponent({
7074
</Button>
7175
}
7276
/>
73-
<button
74-
type="button"
75-
className="mt-12 flex items-center gap-8 text-left"
76-
onClick={toggleDoNotAskAgain}
77-
>
78-
<div onClick={e => e.stopPropagation()}>
77+
<button type="button" className="mt-16 flex items-center gap-8" onClick={toggleDoNotAskAgain}>
78+
<div className="flex items-center" onClick={e => e.stopPropagation()}>
7979
<Checkbox checked={doNotAskAgain} onCheckedChange={setDoNotAskAgain} />
8080
</div>
81-
<span style={{ fontSize: 14 }}>{t("newSendFlow.skipMemo.neverAskAgain")}</span>
81+
<span className="body-2 text-base">{t("newSendFlow.skipMemo.neverAskAgain")}</span>
8282
</button>
8383
</div>
8484
);

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
2+
13
import { renderHook } from "@testing-library/react";
24
import { useRecipientAddressModalViewModel } from "../useRecipientAddressModalViewModel";
35
import { useSelector } from "LLD/hooks/redux";
@@ -11,6 +13,7 @@ import {
1113
import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor";
1214
import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors";
1315
import { createMockAccount } from "../../__integrations__/__fixtures__/accounts";
16+
import { SendFlowState } from "../../../../types";
1417

1518
jest.mock("LLD/hooks/redux");
1619
jest.mock("../useAddressValidation");
@@ -49,6 +52,14 @@ const mockRecipientSearch = {
4952
clear: jest.fn(),
5053
};
5154

55+
const DEFAULT_STATE = {
56+
transaction: {
57+
status: {
58+
errors: {},
59+
},
60+
},
61+
} as unknown as SendFlowState;
62+
5263
describe("useRecipientAddressModalViewModel", () => {
5364
beforeEach(() => {
5465
jest.clearAllMocks();
@@ -67,7 +78,7 @@ describe("useRecipientAddressModalViewModel", () => {
6778
mockedSendFeatures.getSelfTransferPolicy.mockReturnValue("impossible");
6879
mockedUseSendFlowData.mockReturnValue({
6980
recipientSearch: mockRecipientSearch,
70-
state: {} as never,
81+
state: DEFAULT_STATE,
7182
uiConfig: {} as never,
7283
});
7384
mockedUseAddressValidation.mockReturnValue({
@@ -108,7 +119,7 @@ describe("useRecipientAddressModalViewModel", () => {
108119
it("shows search results when search value is provided", () => {
109120
mockedUseSendFlowData.mockReturnValue({
110121
recipientSearch: { ...mockRecipientSearch, value: "some_address" },
111-
state: {} as never,
122+
state: DEFAULT_STATE,
112123
uiConfig: {} as never,
113124
});
114125

@@ -165,7 +176,7 @@ describe("useRecipientAddressModalViewModel", () => {
165176

166177
result.current.handleRecentAddressSelect(recentAddress);
167178

168-
expect(onAddressSelected).toHaveBeenCalledWith("recent_address", undefined);
179+
expect(onAddressSelected).toHaveBeenCalledWith("recent_address", undefined, true);
169180
});
170181

171182
it("calls onAddressSelected when handleAccountSelect is called", () => {
@@ -186,7 +197,7 @@ describe("useRecipientAddressModalViewModel", () => {
186197

187198
result.current.handleAccountSelect(selectedAccount);
188199

189-
expect(onAddressSelected).toHaveBeenCalledWith("selected_fresh_address");
200+
expect(onAddressSelected).toHaveBeenCalledWith("selected_fresh_address", undefined, true);
190201
});
191202

192203
it("calls onAddressSelected when handleAddressSelect is called", () => {
@@ -203,7 +214,7 @@ describe("useRecipientAddressModalViewModel", () => {
203214

204215
result.current.handleAddressSelect("new_address", "ens_name");
205216

206-
expect(onAddressSelected).toHaveBeenCalledWith("new_address", "ens_name");
217+
expect(onAddressSelected).toHaveBeenCalledWith("new_address", "ens_name", true);
207218
});
208219

209220
it("removes address from recent addresses when handleRemoveAddress is called", () => {
@@ -235,7 +246,7 @@ describe("useRecipientAddressModalViewModel", () => {
235246
it("shows sanctioned banner when address is sanctioned", () => {
236247
mockedUseSendFlowData.mockReturnValue({
237248
recipientSearch: { ...mockRecipientSearch, value: "sanctioned_address" },
238-
state: {} as never,
249+
state: DEFAULT_STATE,
239250
uiConfig: {} as never,
240251
});
241252

@@ -275,7 +286,7 @@ describe("useRecipientAddressModalViewModel", () => {
275286
it("shows address validation error for incorrect format", () => {
276287
mockedUseSendFlowData.mockReturnValue({
277288
recipientSearch: { ...mockRecipientSearch, value: "invalid_address" },
278-
state: {} as never,
289+
state: DEFAULT_STATE,
279290
uiConfig: {} as never,
280291
});
281292

@@ -315,7 +326,7 @@ describe("useRecipientAddressModalViewModel", () => {
315326
it("shows matched address when validation is valid", () => {
316327
mockedUseSendFlowData.mockReturnValue({
317328
recipientSearch: { ...mockRecipientSearch, value: "valid_address" },
318-
state: {} as never,
329+
state: DEFAULT_STATE,
319330
uiConfig: {} as never,
320331
});
321332

@@ -354,7 +365,7 @@ describe("useRecipientAddressModalViewModel", () => {
354365
it("identifies self-transfer error correctly", () => {
355366
mockedUseSendFlowData.mockReturnValue({
356367
recipientSearch: { ...mockRecipientSearch, value: "source_address" },
357-
state: {} as never,
368+
state: DEFAULT_STATE,
358369
uiConfig: {} as never,
359370
});
360371

@@ -396,7 +407,7 @@ describe("useRecipientAddressModalViewModel", () => {
396407
it("treats InvalidAddress as incorrect format for domain-like strings", () => {
397408
mockedUseSendFlowData.mockReturnValue({
398409
recipientSearch: { ...mockRecipientSearch, value: "invalid.eth" },
399-
state: {} as never,
410+
state: DEFAULT_STATE,
400411
uiConfig: {} as never,
401412
});
402413

@@ -436,7 +447,7 @@ describe("useRecipientAddressModalViewModel", () => {
436447
it("shows empty state when no matches and not complete", () => {
437448
mockedUseSendFlowData.mockReturnValue({
438449
recipientSearch: { ...mockRecipientSearch, value: "searching" },
439-
state: {} as never,
450+
state: DEFAULT_STATE,
440451
uiConfig: {} as never,
441452
});
442453

0 commit comments

Comments
 (0)