Skip to content

Commit bb3bc72

Browse files
authored
Merge pull request #14169 from LedgerHQ/feat/amount-screen-redesign
feat(lwd): amount screen new send flow
2 parents daca028 + 287e785 commit bb3bc72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3492
-1
lines changed

.changeset/spicy-wasps-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ledger-live-desktop": minor
3+
---
4+
5+
feat(LWD): Amount screen of send flow revamp

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ import { SignatureScreen } from "./screens/Signature/SignatureScreen";
1010
import { ConfirmationScreen } from "./screens/Confirmation/ConfirmationScreen";
1111
import { SendFlowLayout } from "./components/SendFlowLayout";
1212
import { RecipientScreen } from "./screens/Recipient/RecipientScreen";
13+
import { AmountScreen } from "./screens/Amount/AmountScreen";
1314
import type { StepRegistry } from "@ledgerhq/live-common/flows/wizard/types";
1415

1516
const stepRegistry: StepRegistry<SendFlowStep> = {
1617
[SEND_FLOW_STEP.RECIPIENT]: RecipientScreen,
17-
[SEND_FLOW_STEP.AMOUNT]: () => <></>,
18+
[SEND_FLOW_STEP.AMOUNT]: AmountScreen,
1819
[SEND_FLOW_STEP.SIGNATURE]: SignatureScreen,
1920
[SEND_FLOW_STEP.CONFIRMATION]: ConfirmationScreen,
2021
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
import { AmountScreenInner } from "./components/AmountScreenInner";
3+
import { useAmountScreen } from "./hooks/useAmountScreen";
4+
5+
export function AmountScreen() {
6+
const viewModel = useAmountScreen();
7+
8+
if (!viewModel.ready) {
9+
return null;
10+
}
11+
12+
return (
13+
<AmountScreenInner
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}
24+
/>
25+
);
26+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from "react";
2+
import { Button } from "@ledgerhq/lumen-ui-react";
3+
import { LedgerLogo } from "@ledgerhq/lumen-ui-react/symbols";
4+
import type { FeePresetOption } from "../hooks/useFeePresetOptions";
5+
import type { FeeFiatMap } from "../hooks/useFeePresetFiatValues";
6+
import type { FeePresetLegendMap } from "../hooks/useFeePresetLegends";
7+
import { useSendFlowData } from "../../../context/SendFlowContext";
8+
import { NetworkFeesMenu } from "./Fees/NetworkFeesMenu";
9+
10+
type AmountFooterProps = Readonly<{
11+
feesRowLabel: string;
12+
feesRowValue: string;
13+
feesRowStrategyLabel: string;
14+
selectedFeeStrategy: string | null;
15+
feePresetOptions: readonly FeePresetOption[];
16+
fiatByPreset: FeeFiatMap;
17+
legendByPreset: FeePresetLegendMap;
18+
onSelectFeeStrategy: (strategy: string) => void;
19+
reviewLabel: string;
20+
reviewShowIcon: boolean;
21+
reviewDisabled: boolean;
22+
reviewLoading: boolean;
23+
onReview: () => void;
24+
onGetFunds?: () => void;
25+
}>;
26+
27+
export function AmountFooter({
28+
feesRowLabel,
29+
feesRowValue,
30+
feesRowStrategyLabel,
31+
selectedFeeStrategy,
32+
feePresetOptions,
33+
fiatByPreset,
34+
legendByPreset,
35+
onSelectFeeStrategy,
36+
reviewLabel,
37+
reviewShowIcon,
38+
reviewDisabled,
39+
reviewLoading,
40+
onReview,
41+
onGetFunds,
42+
}: AmountFooterProps) {
43+
const { state } = useSendFlowData();
44+
const { account } = state.account;
45+
const { transaction } = state.transaction;
46+
47+
if (!account || !transaction) {
48+
return null;
49+
}
50+
return (
51+
<div className="mt-56 pt-12">
52+
<div className="border-t border-muted-subtle" />
53+
<NetworkFeesMenu
54+
display={{
55+
label: feesRowLabel,
56+
value: feesRowValue,
57+
strategyLabel: feesRowStrategyLabel,
58+
}}
59+
selection={{
60+
selectedStrategy: selectedFeeStrategy,
61+
onSelectStrategy: onSelectFeeStrategy,
62+
}}
63+
presets={{
64+
options: feePresetOptions,
65+
fiatByPreset,
66+
legendByPreset,
67+
}}
68+
/>
69+
<Button
70+
appearance="base"
71+
size="lg"
72+
isFull
73+
onClick={reviewShowIcon ? onReview : onGetFunds}
74+
disabled={reviewDisabled}
75+
loading={reviewLoading}
76+
icon={reviewShowIcon ? LedgerLogo : undefined}
77+
className="rounded-full"
78+
>
79+
{reviewLoading ? "" : reviewLabel}
80+
</Button>
81+
</div>
82+
);
83+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from "react";
2+
import { AmountInput, IconButton } from "@ledgerhq/lumen-ui-react";
3+
import { TransferVertical } from "@ledgerhq/lumen-ui-react/symbols";
4+
import { cn } from "LLD/utils/cn";
5+
import type { AmountScreenMessage } from "../types";
6+
import { AmountMessageText } from "./AmountMessageText";
7+
8+
type AmountInputSectionProps = Readonly<{
9+
amountValue: string;
10+
amountInputMaxDecimalLength: number;
11+
currencyText: string;
12+
currencyPosition: "left" | "right";
13+
isInputDisabled: boolean;
14+
onAmountChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
15+
onToggleInputMode: () => void;
16+
toggleLabel: string;
17+
secondaryValue: string;
18+
amountMessage: AmountScreenMessage | null | undefined;
19+
}>;
20+
21+
export function AmountInputSection({
22+
amountValue,
23+
amountInputMaxDecimalLength,
24+
currencyText,
25+
currencyPosition,
26+
isInputDisabled,
27+
onAmountChange,
28+
onToggleInputMode,
29+
toggleLabel,
30+
secondaryValue,
31+
amountMessage,
32+
}: AmountInputSectionProps) {
33+
return (
34+
<section className="relative flex flex-col items-center pt-56 text-center">
35+
<div className="relative flex w-full items-center justify-center">
36+
<AmountInput
37+
value={amountValue}
38+
placeholder="0"
39+
onChange={onAmountChange}
40+
maxDecimalLength={amountInputMaxDecimalLength}
41+
currencyText={currencyText}
42+
currencyPosition={currencyPosition}
43+
disabled={isInputDisabled}
44+
autoFocus
45+
aria-invalid={amountMessage?.type === "error"}
46+
className={cn(
47+
"heading-0-semi-bold text-base placeholder:text-muted",
48+
amountMessage?.type === "error" && "text-error",
49+
)}
50+
/>
51+
<IconButton
52+
icon={TransferVertical}
53+
size="xs"
54+
appearance="gray"
55+
aria-label={toggleLabel}
56+
className="absolute top-12 right-8"
57+
onClick={onToggleInputMode}
58+
/>
59+
</div>
60+
<p className="mt-8 body-2 text-muted">{secondaryValue}</p>
61+
<div className="mt-8 min-h-20">
62+
<AmountMessageText message={amountMessage} />
63+
</div>
64+
</section>
65+
);
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from "react";
2+
import { cva } from "class-variance-authority";
3+
import { cn } from "LLD/utils/cn";
4+
import type { AmountScreenMessage } from "../types";
5+
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+
16+
type AmountMessageTextProps = Readonly<{
17+
message: AmountScreenMessage | null | undefined;
18+
}>;
19+
20+
export function AmountMessageText({ message }: AmountMessageTextProps) {
21+
if (!message) return null;
22+
23+
return <p className={cn(messageStyles({ type: message.type }))}>{message.text}</p>;
24+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useMemo } from "react";
2+
import type { Account, AccountLike } from "@ledgerhq/types-live";
3+
import type { Transaction } from "@ledgerhq/live-common/generated/types";
4+
import type { SendFlowTransactionActions } from "@ledgerhq/live-common/flows/send/types";
5+
import { getAccountCurrency, getMainAccount } from "@ledgerhq/coin-framework/account/helpers";
6+
import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor";
7+
import { EvmGasOptionsSyncPlugin } from "./plugins/EvmGasOptionsSyncPlugin";
8+
9+
type AmountPluginProps = Readonly<{
10+
account: AccountLike;
11+
parentAccount: Account | null;
12+
transaction: Transaction;
13+
transactionActions: SendFlowTransactionActions;
14+
}>;
15+
16+
type AmountPluginComponent = (props: AmountPluginProps) => React.ReactElement | null;
17+
18+
const pluginRegistry: Readonly<Record<string, AmountPluginComponent>> = {
19+
evmGasOptionsSync: EvmGasOptionsSyncPlugin,
20+
};
21+
22+
export function AmountPluginsHost(props: AmountPluginProps) {
23+
const mainAccount = useMemo(
24+
() => getMainAccount(props.account, props.parentAccount ?? undefined),
25+
[props.account, props.parentAccount],
26+
);
27+
const currency = useMemo(() => getAccountCurrency(mainAccount), [mainAccount]);
28+
29+
const pluginIds = useMemo(() => sendFeatures.getAmountPlugins(currency), [currency]);
30+
31+
return (
32+
<>
33+
{pluginIds.map(id => {
34+
const Plugin = pluginRegistry[id];
35+
if (!Plugin) return null;
36+
return <Plugin key={id} {...props} />;
37+
})}
38+
</>
39+
);
40+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useCallback } from "react";
2+
import type { Account, AccountLike } from "@ledgerhq/types-live";
3+
import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types";
4+
import type {
5+
SendFlowTransactionActions,
6+
SendFlowUiConfig,
7+
} from "@ledgerhq/live-common/flows/send/types";
8+
import { useAmountScreenViewModel } from "../hooks/useAmountScreenViewModel";
9+
import { AmountScreenView } from "./AmountScreenView";
10+
import { AmountPluginsHost } from "./AmountPluginsHost";
11+
12+
type AmountScreenInnerProps = Readonly<{
13+
account: AccountLike;
14+
parentAccount: Account | null;
15+
transaction: Transaction;
16+
status: TransactionStatus;
17+
bridgePending: boolean;
18+
bridgeError: Error | null;
19+
uiConfig: SendFlowUiConfig;
20+
transactionActions: SendFlowTransactionActions;
21+
onReview: () => void;
22+
onGetFunds: () => void;
23+
}>;
24+
25+
export function AmountScreenInner({
26+
account,
27+
parentAccount,
28+
transaction,
29+
status,
30+
bridgePending,
31+
bridgeError,
32+
uiConfig,
33+
transactionActions,
34+
onReview,
35+
onGetFunds,
36+
}: AmountScreenInnerProps) {
37+
const handleReview = useCallback(() => {
38+
onReview();
39+
}, [onReview]);
40+
41+
const viewModel = useAmountScreenViewModel({
42+
account,
43+
parentAccount,
44+
transaction,
45+
status,
46+
bridgePending,
47+
bridgeError,
48+
uiConfig,
49+
transactionActions,
50+
});
51+
52+
return (
53+
<>
54+
<AmountPluginsHost
55+
account={account}
56+
parentAccount={parentAccount}
57+
transaction={transaction}
58+
transactionActions={transactionActions}
59+
/>
60+
<AmountScreenView {...viewModel} onReview={handleReview} onGetFunds={onGetFunds} />
61+
</>
62+
);
63+
}

0 commit comments

Comments
 (0)