Skip to content

Commit 2fa9d7d

Browse files
committed
feat: refacto / fix conflicts / update banner
1 parent 573b802 commit 2fa9d7d

31 files changed

+662
-1657
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function SendFlowLayout({ stepRegistry, isOpen, onClose }: SendFlowLayout
3838

3939
return (
4040
<Dialog height={dialogHeight} open={isOpen} onOpenChange={handleDialogOpenChange}>
41-
<DialogContent>
41+
<DialogContent className="text-base">
4242
{shouldShowStatusGradient && (
4343
<div
4444
className={cn("pointer-events-none absolute inset-x-0 top-0 h-full", {
@@ -48,7 +48,7 @@ export function SendFlowLayout({ stepRegistry, isOpen, onClose }: SendFlowLayout
4848
/>
4949
)}
5050
<SendHeader />
51-
<DialogBody className="-mb-24 gap-32 px-24 py-16 text-base [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
51+
<DialogBody className="-mb-24 gap-32 px-24 py-16">
5252
{StepComponent && (
5353
<div key={currentStep} className="animate-fade-in">
5454
<StepComponent />

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

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react";
66
import type { AccountLike } from "@ledgerhq/types-live";
77
import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index";
88
import { getAccountCurrency } from "@ledgerhq/live-common/account/index";
9+
import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor";
910
import { useCalculate } from "@ledgerhq/live-countervalues-react";
1011
import { useMaybeAccountUnit } from "~/renderer/hooks/useAccountUnit";
1112
import { counterValueCurrencySelector, localeSelector } from "~/renderer/reducers/settings";
@@ -14,6 +15,10 @@ import {
1415
useSendFlowData,
1516
useSendFlowActions,
1617
} from "../context/SendFlowContext";
18+
import { useRecipientMemo } from "../screens/Recipient/hooks/useRecipientMemo";
19+
import { MemoTypeSelect } from "../screens/Recipient/components/Memo/MemoTypeSelect";
20+
import { MemoValueInput } from "../screens/Recipient/components/Memo/MemoValueInput";
21+
import { SkipMemoSection } from "../screens/Recipient/components/Memo/SkipMemoSection";
1722

1823
function useAvailableBalance(account?: AccountLike | null) {
1924
const locale = useSelector(localeSelector);
@@ -57,7 +62,7 @@ function useAvailableBalance(account?: AccountLike | null) {
5762
export function SendHeader() {
5863
const { navigation, currentStepConfig } = useSendFlowNavigation();
5964
const { state, uiConfig, recipientSearch } = useSendFlowData();
60-
const { close } = useSendFlowActions();
65+
const { close, transaction } = useSendFlowActions();
6166
const { t } = useTranslation();
6267

6368
const currencyName = state.account.currency?.ticker ?? "";
@@ -79,9 +84,41 @@ export function SendHeader() {
7984
showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : "";
8085

8186
const showRecipientInput = currentStepConfig?.addressInput ?? false;
87+
const showMemoControls = Boolean(
88+
showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0,
89+
);
90+
91+
const currencyId = state.account.currency?.id;
92+
const memoDefaultOption = useMemo(() => {
93+
return state.account.currency
94+
? sendFeatures.getMemoDefaultOption(state.account.currency)
95+
: undefined;
96+
}, [state.account.currency]);
97+
98+
const memoTypeOptions = uiConfig.memoOptions ?? [];
99+
const memoType = uiConfig.memoType;
100+
const memoMaxLength = uiConfig.memoMaxLength;
101+
102+
const memoViewModel = useRecipientMemo({
103+
hasMemo: uiConfig.hasMemo,
104+
memoDefaultOption,
105+
memoType,
106+
memoTypeOptions,
107+
onMemoChange: memo => {
108+
transaction.setRecipient({ memo });
109+
},
110+
onMemoSkip: () => {
111+
navigation.goToNextStep();
112+
},
113+
resetKey: `${state.account.account?.id ?? ""}|${currencyId ?? ""}|${
114+
recipientSearch.value.length === 0 ? "empty" : "filled"
115+
}`,
116+
});
117+
118+
const transactionErrorName = state.transaction.status?.errors?.transaction?.name;
82119

83120
return (
84-
<div className="-mb-12 flex flex-col">
121+
<div className="flex flex-col">
85122
<DialogHeader
86123
appearance="compact"
87124
title={title}
@@ -90,17 +127,54 @@ export function SendHeader() {
90127
onClose={close}
91128
/>
92129
{showRecipientInput ? (
93-
<AddressInput
94-
className="mb-24 px-24"
95-
value={recipientSearch.value}
96-
onChange={e => recipientSearch.setValue(e.target.value)}
97-
onClear={recipientSearch.clear}
98-
placeholder={
99-
uiConfig.recipientSupportsDomain
100-
? t("newSendFlow.placeholder")
101-
: t("newSendFlow.placeholderNoENS")
102-
}
103-
/>
130+
<>
131+
<AddressInput
132+
className="mb-12 px-24"
133+
value={recipientSearch.value}
134+
onChange={e => recipientSearch.setValue(e.target.value)}
135+
onClear={recipientSearch.clear}
136+
placeholder={
137+
uiConfig.recipientSupportsDomain
138+
? t("newSendFlow.placeholder")
139+
: t("newSendFlow.placeholderNoENS")
140+
}
141+
/>
142+
143+
{showMemoControls && currencyId ? (
144+
<div className="mb-24 px-24">
145+
<div className="flex flex-col gap-12">
146+
{memoViewModel.hasMemoTypeOptions ? (
147+
<MemoTypeSelect
148+
currencyId={currencyId}
149+
options={memoTypeOptions}
150+
value={memoViewModel.memo.type}
151+
onChange={memoViewModel.onMemoTypeChange}
152+
/>
153+
) : null}
154+
155+
{memoViewModel.showMemoValueInput ? (
156+
<MemoValueInput
157+
currencyId={currencyId}
158+
value={memoViewModel.memo.value}
159+
maxLength={memoMaxLength}
160+
transactionErrorName={transactionErrorName}
161+
onChange={memoViewModel.onMemoValueChange}
162+
/>
163+
) : null}
164+
</div>
165+
166+
{memoViewModel.showSkipMemo ? (
167+
<SkipMemoSection
168+
currencyId={currencyId}
169+
state={memoViewModel.skipMemoState}
170+
onRequestConfirm={memoViewModel.onSkipMemoRequestConfirm}
171+
onCancelConfirm={memoViewModel.onSkipMemoCancelConfirm}
172+
onConfirm={memoViewModel.onSkipMemoConfirm}
173+
/>
174+
) : null}
175+
</div>
176+
) : null}
177+
</>
104178
) : null}
105179
</div>
106180
);

apps/ledger-live-desktop/src/mvvm/features/Send/hooks/__tests__/useSendFlowTransaction.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe("useSendFlowTransaction", () => {
7676
act(() => {
7777
result.current.actions.setRecipient({
7878
address: "cosmos1abc123",
79-
memo: "test memo",
79+
memo: { value: "test memo" },
8080
});
8181
});
8282

@@ -117,7 +117,7 @@ describe("useSendFlowTransaction", () => {
117117
act(() => {
118118
result.current.actions.setRecipient({
119119
address: "solana-address",
120-
memo: "solana memo",
120+
memo: { value: "solana memo" },
121121
});
122122
});
123123

@@ -199,7 +199,7 @@ describe("useSendFlowTransaction", () => {
199199
act(() => {
200200
result.current.actions.setRecipient({
201201
address: "casper-address",
202-
memo: "transfer-id-123",
202+
memo: { value: "transfer-id-123" },
203203
});
204204
});
205205

@@ -236,7 +236,7 @@ describe("useSendFlowTransaction", () => {
236236
act(() => {
237237
result.current.actions.setRecipient({
238238
address: "xrp-address",
239-
memo: "test",
239+
memo: { value: "test" },
240240
destinationTag: "67890",
241241
});
242242
});

apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function useSendFlowBusinessLogic({
2525
}: UseSendFlowBusinessLogicParams): SendFlowBusinessContext {
2626
const [flowStatus, setFlowStatus] = useState(FLOW_STATUS.IDLE);
2727
const [recipientSearchValue, setRecipientSearchValue] = useState("");
28+
const [recipient, setRecipient] = useState<RecipientData | null>(null);
2829

2930
const accountHook = useSendFlowAccount({
3031
initialAccount: initParams?.account,
@@ -57,6 +58,7 @@ export function useSendFlowBusinessLogic({
5758
const handleRecipientSet = useCallback(
5859
(recipient: RecipientData) => {
5960
transactionHook.actions.setRecipient(recipient);
61+
setRecipient(prev => ({ ...(prev ?? {}), ...recipient }));
6062
},
6163
[transactionHook.actions],
6264
);
@@ -74,12 +76,12 @@ export function useSendFlowBusinessLogic({
7476
() => ({
7577
account: accountHook.state,
7678
transaction: transactionHook.state,
77-
recipient: null,
79+
recipient,
7880
operation: operationHook.state,
7981
isLoading: transactionHook.state.bridgePending,
8082
flowStatus,
8183
}),
82-
[accountHook.state, transactionHook.state, operationHook.state, flowStatus],
84+
[accountHook.state, transactionHook.state, recipient, operationHook.state, flowStatus],
8385
);
8486

8587
const statusActions = useMemo(

apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,33 +43,61 @@ export function useSendFlowTransaction({
4343
[bridgeUpdateTransaction],
4444
);
4545

46-
const setRecipient = useCallback(
47-
(recipient: RecipientData) => {
48-
if (!account || !transaction) return;
46+
const buildRecipientUpdates = useCallback(
47+
(currentTransaction: Transaction, recipient: RecipientData): Partial<Transaction> => {
48+
const updates: Partial<Transaction> = {};
4949

50-
const bridge = getAccountBridge(account, parentAccount);
51-
const updates: Partial<Transaction> = { recipient: recipient.address };
50+
if (recipient.address !== undefined) {
51+
updates.recipient = recipient.address;
52+
}
5253

5354
if (recipient.memo !== undefined) {
5455
Object.assign(
5556
updates,
56-
applyMemoToTransaction(transaction.family, recipient.memo, transaction),
57+
applyMemoToTransaction(
58+
currentTransaction.family,
59+
recipient.memo.value,
60+
recipient.memo.type,
61+
currentTransaction,
62+
),
5763
);
5864
}
5965

6066
if (recipient.destinationTag !== undefined) {
61-
const parsedTag = Number(recipient.destinationTag.trim());
62-
if (Number.isFinite(parsedTag)) {
63-
Object.assign(
64-
updates,
65-
applyMemoToTransaction(transaction.family, parsedTag, transaction),
66-
);
67+
const trimmed = recipient.destinationTag.trim();
68+
if (trimmed.length > 0) {
69+
const parsedTag = Number(trimmed);
70+
if (Number.isFinite(parsedTag)) {
71+
Object.assign(
72+
updates,
73+
applyMemoToTransaction(
74+
currentTransaction.family,
75+
parsedTag,
76+
undefined,
77+
currentTransaction,
78+
),
79+
);
80+
}
6781
}
6882
}
6983

70-
bridgeSetTransaction(bridge.updateTransaction(transaction, updates));
84+
return updates;
85+
},
86+
[],
87+
);
88+
89+
const setRecipient = useCallback(
90+
(recipient: RecipientData) => {
91+
if (!account || !transaction) return;
92+
93+
const bridge = getAccountBridge(account, parentAccount);
94+
const updates = buildRecipientUpdates(transaction, recipient);
95+
96+
if (Object.keys(updates).length > 0) {
97+
bridgeSetTransaction(bridge.updateTransaction(transaction, updates));
98+
}
7199
},
72-
[account, parentAccount, transaction, bridgeSetTransaction],
100+
[account, parentAccount, transaction, bridgeSetTransaction, buildRecipientUpdates],
73101
);
74102

75103
const setAccountForTransaction = useCallback(

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import React, { useCallback, useMemo } from "react";
22
import { getAccountCurrency } from "@ledgerhq/live-common/account/index";
33
import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
4+
import { RecipientAddressModal } from "./components/RecipientAddressModal";
45
import {
5-
useSendFlowData,
66
useSendFlowActions,
7+
useSendFlowData,
78
useSendFlowNavigation,
89
} from "../../context/SendFlowContext";
9-
import { RecipientAddressModal } from "./components/RecipientAddressModal";
1010

1111
export function RecipientScreen() {
12-
const { state, uiConfig, recipientSearch } = useSendFlowData();
12+
const { state, uiConfig } = useSendFlowData();
1313
const { transaction, close } = useSendFlowActions();
1414
const { navigation } = useSendFlowNavigation();
1515

@@ -22,16 +22,17 @@ export function RecipientScreen() {
2222
}, [state.account.currency, account]);
2323

2424
const handleAddressSelected = useCallback(
25-
(address: string, ensName?: string) => {
25+
(address: string, ensName?: string, goToNextStep?: boolean) => {
2626
transaction.setRecipient({
2727
address,
2828
ensName,
2929
});
3030

31-
recipientSearch.clear();
32-
navigation.goToNextStep();
31+
if (goToNextStep) {
32+
navigation.goToNextStep();
33+
}
3334
},
34-
[transaction, navigation, recipientSearch],
35+
[transaction, navigation],
3536
);
3637

3738
if (!account || !currency) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
import {
3+
Select,
4+
SelectContent,
5+
SelectItem,
6+
SelectItemText,
7+
SelectTrigger,
8+
} from "@ledgerhq/lumen-ui-react";
9+
import { useTranslation } from "react-i18next";
10+
11+
type MemoTypeSelectProps = Readonly<{
12+
currencyId: string;
13+
options: readonly string[];
14+
value?: string;
15+
onChange: (value: string) => void;
16+
}>;
17+
18+
function MemoTypeSelectComponent({ currencyId, options, value, onChange }: MemoTypeSelectProps) {
19+
const { t } = useTranslation();
20+
21+
return (
22+
<Select onValueChange={onChange} value={value}>
23+
<SelectTrigger />
24+
<SelectContent>
25+
{options.map(optionValue => (
26+
<SelectItem key={optionValue} value={optionValue}>
27+
<SelectItemText>{t(`families.${currencyId}.memoType.${optionValue}`)}</SelectItemText>
28+
</SelectItem>
29+
))}
30+
</SelectContent>
31+
</Select>
32+
);
33+
}
34+
35+
export const MemoTypeSelect = React.memo(MemoTypeSelectComponent);

0 commit comments

Comments
 (0)