Skip to content

Commit 287e785

Browse files
Moustafa-Koterbadilaouid
authored andcommitted
feat(desktop): add send max descriptor
1 parent 5b855f7 commit 287e785

File tree

7 files changed

+139
-92
lines changed

7 files changed

+139
-92
lines changed
Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,26 @@
1-
import React, { useCallback } from "react";
2-
import { useNavigate } from "react-router";
3-
import { useSendFlowActions, useSendFlowData } from "../../context/SendFlowContext";
4-
import { getMainAccount } from "@ledgerhq/coin-framework/account/helpers";
1+
import React from "react";
52
import { AmountScreenInner } from "./components/AmountScreenInner";
6-
import { useFlowWizard } from "LLD/features/FlowWizard/FlowWizardContext";
3+
import { useAmountScreen } from "./hooks/useAmountScreen";
74

85
export function AmountScreen() {
9-
const { state, uiConfig } = useSendFlowData();
10-
const { transaction: transactionActions, close } = useSendFlowActions();
11-
const { navigation } = useFlowWizard();
12-
const navigate = useNavigate();
6+
const viewModel = useAmountScreen();
137

14-
const { account, parentAccount } = state.account;
15-
const { bridgePending, bridgeError, status, transaction } = state.transaction;
16-
17-
const handleGetFunds = useCallback(() => {
18-
if (!account) return;
19-
const mainAccount = getMainAccount(account, parentAccount ?? undefined);
20-
const currencyId = "currency" in mainAccount ? mainAccount.currency.id : undefined;
21-
22-
// Navigate to exchange (Buy) page with preselected currency (same behavior as Buy button)
23-
navigate("/exchange", {
24-
state: {
25-
currency: currencyId,
26-
account: mainAccount.id,
27-
mode: "buy",
28-
},
29-
});
30-
// Close the send flow dialog
31-
close();
32-
}, [account, close, navigate, parentAccount]);
33-
34-
if (!account || !transaction || !status) {
8+
if (!viewModel.ready) {
359
return null;
3610
}
3711

3812
return (
3913
<AmountScreenInner
40-
account={account}
41-
parentAccount={parentAccount}
42-
transaction={transaction}
43-
status={status}
44-
bridgePending={bridgePending}
45-
bridgeError={bridgeError}
46-
uiConfig={uiConfig}
47-
transactionActions={transactionActions}
48-
onReview={() => navigation.goToNextStep()}
49-
onGetFunds={handleGetFunds}
14+
account={viewModel.account}
15+
parentAccount={viewModel.parentAccount}
16+
transaction={viewModel.transaction}
17+
status={viewModel.status}
18+
bridgePending={viewModel.bridgePending}
19+
bridgeError={viewModel.bridgeError}
20+
uiConfig={viewModel.uiConfig}
21+
transactionActions={viewModel.transactionActions}
22+
onReview={viewModel.onReview}
23+
onGetFunds={viewModel.onGetFunds}
5024
/>
5125
);
5226
}
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import React from "react";
2+
import { cva } from "class-variance-authority";
23
import { cn } from "LLD/utils/cn";
34
import type { AmountScreenMessage } from "../types";
45

6+
const messageStyles = cva("text-center body-2", {
7+
variants: {
8+
type: {
9+
error: "text-error",
10+
warning: "text-warning",
11+
info: "text-base",
12+
},
13+
},
14+
});
15+
516
type AmountMessageTextProps = Readonly<{
617
message: AmountScreenMessage | null | undefined;
718
}>;
819

920
export function AmountMessageText({ message }: AmountMessageTextProps) {
1021
if (!message) return null;
1122

12-
return (
13-
<p
14-
className={cn(
15-
"text-center body-2",
16-
message.type === "error" && "text-error",
17-
message.type === "warning" && "text-warning",
18-
message.type === "info" && "text-base",
19-
)}
20-
>
21-
{message.text}
22-
</p>
23-
);
23+
return <p className={cn(messageStyles({ type: message.type }))}>{message.text}</p>;
2424
}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/gener
1212
import { getCryptoCurrencyById } from "@ledgerhq/live-common/currencies/index";
1313
import { createMockAccount } from "../../../Recipient/__integrations__/__fixtures__/accounts";
1414

15-
jest.mock("react-i18next", () => ({
16-
...jest.requireActual("react-i18next"),
17-
useTranslation: () => ({ t: (key: string) => key }),
18-
}));
19-
2015
jest.mock("@ledgerhq/live-common/bridge/impl");
2116
jest.mock("@ledgerhq/coin-framework/account/helpers");
2217
jest.mock("@ledgerhq/live-common/bridge/descriptor", () => ({
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useCallback } from "react";
2+
import { useNavigate } from "react-router";
3+
import { useSendFlowActions, useSendFlowData } from "../../../context/SendFlowContext";
4+
import { getMainAccount } from "@ledgerhq/coin-framework/account/helpers";
5+
import { useFlowWizard } from "LLD/features/FlowWizard/FlowWizardContext";
6+
import type { Account, AccountLike } from "@ledgerhq/types-live";
7+
import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types";
8+
import type {
9+
SendFlowTransactionActions,
10+
SendFlowUiConfig,
11+
} from "@ledgerhq/live-common/flows/send/types";
12+
13+
type AmountScreenViewModelBase = Readonly<{
14+
onReview: () => void;
15+
onGetFunds: () => void;
16+
}>;
17+
18+
export type AmountScreenViewModel =
19+
| (AmountScreenViewModelBase & { ready: false })
20+
| (AmountScreenViewModelBase &
21+
Readonly<{
22+
ready: true;
23+
account: AccountLike;
24+
parentAccount: Account | null;
25+
transaction: Transaction;
26+
status: TransactionStatus;
27+
bridgePending: boolean;
28+
bridgeError: Error | null;
29+
uiConfig: SendFlowUiConfig;
30+
transactionActions: SendFlowTransactionActions;
31+
}>);
32+
33+
export function useAmountScreen(): AmountScreenViewModel {
34+
const { state, uiConfig } = useSendFlowData();
35+
const { transaction: transactionActions, close } = useSendFlowActions();
36+
const { navigation } = useFlowWizard();
37+
const navigate = useNavigate();
38+
39+
const { account, parentAccount } = state.account;
40+
const { bridgePending, bridgeError, status, transaction } = state.transaction;
41+
42+
const onGetFunds = useCallback(() => {
43+
if (!account) return;
44+
const mainAccount = getMainAccount(account, parentAccount ?? undefined);
45+
const currencyId = "currency" in mainAccount ? mainAccount.currency.id : undefined;
46+
47+
navigate("/exchange", {
48+
state: {
49+
currency: currencyId,
50+
account: mainAccount.id,
51+
mode: "buy",
52+
},
53+
});
54+
close();
55+
}, [account, close, navigate, parentAccount]);
56+
57+
const onReview = useCallback(() => {
58+
navigation.goToNextStep();
59+
}, [navigation]);
60+
61+
if (!account || !transaction || !status || !uiConfig || !transactionActions) {
62+
return { ready: false, onReview, onGetFunds };
63+
}
64+
65+
return {
66+
ready: true,
67+
account,
68+
parentAccount: parentAccount ?? null,
69+
transaction,
70+
status,
71+
bridgePending: bridgePending ?? false,
72+
bridgeError: bridgeError ?? null,
73+
uiConfig,
74+
transactionActions,
75+
onReview,
76+
onGetFunds,
77+
};
78+
}

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Amount/hooks/useFeePresetFiatValues.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/formatCu
99
import type { Currency, Unit } from "@ledgerhq/types-cryptoassets";
1010
import { buildEstimationKey } from "../utils/feeEstimation";
1111

12+
function getFeesStrategyForPreset(presetId: string): Transaction["feesStrategy"] | null {
13+
if (presetId === "slow") return "slow";
14+
if (presetId === "medium") return "medium";
15+
if (presetId === "fast") return "fast";
16+
if (presetId === "custom") return "custom";
17+
return null;
18+
}
19+
20+
function formatCountervalueAsFiat(
21+
fiatUnit: Unit,
22+
countervalue: BigNumber | null | undefined,
23+
): string | null {
24+
return countervalue !== null && countervalue !== undefined
25+
? formatCurrencyUnit(fiatUnit, countervalue, {
26+
showCode: true,
27+
disableRounding: true,
28+
})
29+
: null;
30+
}
31+
1232
export type FeeFiatMap = Readonly<Record<string, string | null>>;
1333

1434
type Params = Readonly<{
@@ -39,13 +59,7 @@ async function estimateFiatValuesForPresets(params: {
3959
try {
4060
const entries = await Promise.all(
4161
params.presetIds.map(async presetId => {
42-
const feesStrategy: Transaction["feesStrategy"] =
43-
presetId === "slow" ||
44-
presetId === "medium" ||
45-
presetId === "fast" ||
46-
presetId === "custom"
47-
? presetId
48-
: null;
62+
const feesStrategy = getFeesStrategyForPreset(presetId);
4963
const txWithStrategy = params.bridge.updateTransaction(params.transaction, {
5064
feesStrategy,
5165
});
@@ -57,13 +71,7 @@ async function estimateFiatValuesForPresets(params: {
5771
const estimatedFees = status.estimatedFees ?? new BigNumber(0);
5872

5973
const countervalue = params.convertCountervalue(params.mainAccount.currency, estimatedFees);
60-
const fiatValue =
61-
countervalue !== null && countervalue !== undefined
62-
? formatCurrencyUnit(params.fiatUnit, countervalue, {
63-
showCode: true,
64-
disableRounding: true,
65-
})
66-
: null;
74+
const fiatValue = formatCountervalueAsFiat(params.fiatUnit, countervalue);
6775

6876
return [presetId, fiatValue] as const;
6977
}),
@@ -140,13 +148,7 @@ export function useFeePresetFiatValues({
140148
const next: Record<string, string | null> = {};
141149
for (const option of feePresetOptions) {
142150
const countervalue = convertCountervalue(mainAccount.currency, option.amount);
143-
next[option.id] =
144-
countervalue !== null && countervalue !== undefined
145-
? formatCurrencyUnit(fiatUnit, countervalue, {
146-
showCode: true,
147-
disableRounding: true,
148-
})
149-
: null;
151+
next[option.id] = formatCountervalueAsFiat(fiatUnit, countervalue);
150152
}
151153
return next;
152154
}, [

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Amount/hooks/useQuickActions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useMaybeAccountUnit } from "~/renderer/hooks/useAccountUnit";
77
import { areAmountsEqual } from "../utils/amount";
88
import type { AmountScreenQuickAction } from "../types";
99
import { getAccountCurrency, getMainAccount } from "@ledgerhq/coin-framework/account/helpers";
10+
import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor";
1011

1112
type UseQuickActionsParams = Readonly<{
1213
account: AccountLike;
@@ -88,6 +89,7 @@ export function useQuickActions({
8889
};
8990
});
9091

92+
const canSendMax = sendFeatures.canSendMax(accountCurrency);
9193
const isMaxActive = transaction.useAllAmount ?? false;
9294
actions.push({
9395
id: "max",
@@ -98,7 +100,7 @@ export function useQuickActions({
98100
onSelectMax();
99101
},
100102
active: isMaxActive,
101-
disabled,
103+
disabled: disabled || !canSendMax,
102104
});
103105

104106
return actions;
@@ -112,6 +114,7 @@ export function useQuickActions({
112114
transaction.amount,
113115
transaction.useAllAmount,
114116
tolerance,
117+
accountCurrency,
115118
]);
116119

117120
return quickActions;

apps/ledger-live-desktop/src/mvvm/features/Send/screens/Amount/utils/amountInput.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,23 @@ export type FormattedAmount = Readonly<{
99
value: BigNumber;
1010
}>;
1111

12-
export function formatAmountForInput(unit: Unit, amount: BigNumber, locale: string): string {
13-
if (amount.isZero()) return "";
14-
15-
return formatCurrencyUnit(unit, amount, {
12+
const formatUnitForInput = (unit: Unit, amount: BigNumber, locale: string): string =>
13+
formatCurrencyUnit(unit, amount, {
1614
showCode: false,
1715
disableRounding: true,
1816
useGrouping: false,
1917
locale,
2018
});
19+
20+
export function formatAmountForInput(unit: Unit, amount: BigNumber, locale: string): string {
21+
if (amount.isZero()) return "";
22+
return formatUnitForInput(unit, amount, locale);
2123
}
2224

2325
export function formatFiatForInput(unit: Unit, amount: BigNumber, locale: string): string {
2426
if (amount.isZero()) return "";
25-
26-
const formatted = formatCurrencyUnit(unit, amount, {
27-
showCode: false,
28-
disableRounding: true,
29-
useGrouping: false,
30-
locale,
31-
});
32-
const clamped = clampDecimals(formatted);
33-
return trimTrailingZeros(clamped);
27+
const formatted = formatUnitForInput(unit, amount, locale);
28+
return trimTrailingZeros(clampDecimals(formatted));
3429
}
3530

3631
export function processRawInput(rawValue: string, unit: Unit, locale: string): FormattedAmount {

0 commit comments

Comments
 (0)