Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/violet-cheetahs-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ledger-live-desktop": minor
"@ledgerhq/live-common": minor
---

Feature/live 20724 [LLD][UI] Memo screen
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ libs/ui/packages/react/debug-storybook.log
libs/ui/packages/react/rsbuild.config.d.ts
libs/ui/packages/react/rsbuild.config.d.ts.map
libs/ui/packages/react/rsbuild.config.js
libs/ui/packages/react/rsbuild.config.js.map
libs/ui/packages/react/rsbuild.config.js.map
.zed
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* @returns Formatted address string
*/
export const formatAddress = (
address: string,
address: string | undefined,
options: {
prefixLength?: number;
suffixLength?: number;
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your SendHeader should use MVVM at this stage IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I will add unit test in an other PR for the new hook πŸ‘

Original file line number Diff line number Diff line change
@@ -1,76 +1,82 @@
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { BigNumber } from "bignumber.js";
import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react";
import { useFlowWizard } from "../../FlowWizard/FlowWizardContext";
import { useSendFlowData, useSendFlowActions } from "../context/SendFlowContext";
import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor";
import {
SEND_FLOW_STEP,
type SendFlowStep,
type SendFlowBusinessContext,
type SendFlowStep,
} from "@ledgerhq/live-common/flows/send/types";
import type { SendStepConfig } from "../types";
import { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "./utils";
import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useFlowWizard } from "../../FlowWizard/FlowWizardContext";
import { useSendFlowActions, useSendFlowData } from "../context/SendFlowContext";
import { useAvailableBalance } from "../hooks/useAvailableBalance";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can keep the import instead of adding the whole function inside the file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

import { useSendHeaderModel } from "../hooks/useSendHeaderModel";
import { MemoTypeSelect } from "../screens/Recipient/components/Memo/MemoTypeSelect";
import { MemoValueInput } from "../screens/Recipient/components/Memo/MemoValueInput";
import { SkipMemoSection } from "../screens/Recipient/components/Memo/SkipMemoSection";
import { useRecipientMemo } from "../screens/Recipient/hooks/useRecipientMemo";
import type { SendStepConfig } from "../types";

export function SendHeader() {
const wizard = useFlowWizard<SendFlowStep, SendFlowBusinessContext, SendStepConfig>();
const { state, uiConfig, recipientSearch } = useSendFlowData();
const { close, transaction } = useSendFlowActions();
const { t } = useTranslation();
const availableText = useAvailableBalance(state.account.account);

const { navigation, currentStep } = wizard;
const currentStepConfig = wizard.currentStepConfig as SendStepConfig;
const currencyId = state.account.currency?.id;

const currencyName = state.account.currency?.ticker ?? "";
const availableText = useAvailableBalance(state.account.account);
const memoDefaultOption = useMemo(() => {
return sendFeatures.getMemoDefaultOption(state.account.currency ?? undefined);
}, [state.account.currency]);

const handleBack = useCallback(() => {
if (navigation.canGoBack()) {
// Reset amount-related state when leaving Amount step (back navigation),
// otherwise the transaction amount can persist while the UI remounts empty.
if (currentStep === SEND_FLOW_STEP.AMOUNT) {
transaction.updateTransaction(tx => ({
...tx,
amount: new BigNumber(0),
useAllAmount: false,
feesStrategy: null,
}));
}
navigation.goToPreviousStep();
} else {
close();
}
}, [close, currentStep, navigation, transaction]);
const memoTypeOptions = useMemo(() => {
return uiConfig.memoOptions ?? [];
}, [uiConfig]);

const showBackButton = navigation.canGoBack();
const showTitle = currentStepConfig?.showTitle !== false;
const {
hasMemoTypeOptions,
memo,
onMemoTypeChange,
showMemoValueInput,
onMemoValueChange,
showSkipMemo,
skipMemoState,
onSkipMemoRequestConfirm,
onSkipMemoCancelConfirm,
onSkipMemoConfirm,
resetViewState,
} = useRecipientMemo({
hasMemo: uiConfig.hasMemo,
memoDefaultOption,
memoType: uiConfig.memoType,
memoTypeOptions,
onMemoChange: memo => {
transaction.setRecipient({ ...state.recipient, memo });
},
onMemoSkip: () => {
navigation.goToNextStep();
},
resetKey: `${state.account.account?.id ?? ""}|${currencyId ?? ""}|${
recipientSearch.value.length === 0 ? "empty" : "filled"
}`,
});

const title = showTitle ? t("newSendFlow.title", { currency: currencyName }) : "";
const descriptionText =
showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : "";
const {
addressInputValue,
descriptionText,
handleBack,
handleRecipientInputClick,
showBackButton,
showMemoControls,
showRecipientInput,
title,
transactionErrorName,
} = useSendHeaderModel({ availableText, resetViewState });

const showRecipientInput = currentStepConfig?.addressInput ?? false;
const isRecipientStep = currentStep === SEND_FLOW_STEP.RECIPIENT;
const isAmountStep = currentStep === SEND_FLOW_STEP.AMOUNT;

const addressInputValue = useMemo(() => {
if (isRecipientStep) return recipientSearch.value;
if (isAmountStep) return getRecipientDisplayValue(state.recipient);
return recipientSearch.value;
}, [isRecipientStep, isAmountStep, recipientSearch.value, state.recipient]);

const handleRecipientInputClick = useCallback(() => {
if (!isAmountStep) return;

const prefillValue = getRecipientSearchPrefillValue(state.recipient);
if (prefillValue) {
recipientSearch.setValue(prefillValue);
}

handleBack();
}, [handleBack, isAmountStep, recipientSearch, state.recipient]);

const recipientInputContent = useMemo(() => {
if (!showRecipientInput) return null;

Expand All @@ -91,30 +97,82 @@ export function SendHeader() {
}

return (
<AddressInput
className="-mt-12 mb-24 px-24"
defaultValue={addressInputValue}
onChange={e => recipientSearch.setValue(e.target.value)}
onClear={recipientSearch.clear}
placeholder={
uiConfig.recipientSupportsDomain
? t("newSendFlow.placeholder")
: t("newSendFlow.placeholderNoENS")
}
/>
<>
<AddressInput
className="-mt-12 mb-12 px-24"
value={addressInputValue}
onChange={e => recipientSearch.setValue(e.target.value)}
onClear={recipientSearch.clear}
placeholder={
uiConfig.recipientSupportsDomain
? t("newSendFlow.placeholder")
: t("newSendFlow.placeholderNoENS")
}
/>
{showMemoControls && currencyId ? (
<div className="mb-24 px-24">
<div className="flex flex-col gap-12">
{hasMemoTypeOptions ? (
<MemoTypeSelect
currencyId={currencyId}
options={memoTypeOptions}
value={memo.type}
onChange={onMemoTypeChange}
/>
) : null}

{showMemoValueInput ? (
<MemoValueInput
currencyId={currencyId}
value={memo.value}
maxLength={uiConfig.memoMaxLength}
transactionErrorName={transactionErrorName}
onChange={onMemoValueChange}
/>
) : null}
</div>

{showSkipMemo ? (
<SkipMemoSection
currencyId={currencyId}
state={skipMemoState}
onRequestConfirm={onSkipMemoRequestConfirm}
onCancelConfirm={onSkipMemoCancelConfirm}
onConfirm={onSkipMemoConfirm}
/>
) : null}
</div>
) : null}
</>
);
}, [
showRecipientInput,
isAmountStep,
addressInputValue,
handleRecipientInputClick,
recipientSearch,
uiConfig.recipientSupportsDomain,
uiConfig.memoMaxLength,
t,
showMemoControls,
currencyId,
hasMemoTypeOptions,
memoTypeOptions,
memo.type,
memo.value,
onMemoTypeChange,
showMemoValueInput,
transactionErrorName,
onMemoValueChange,
showSkipMemo,
skipMemoState,
onSkipMemoRequestConfirm,
onSkipMemoCancelConfirm,
onSkipMemoConfirm,
handleRecipientInputClick,
]);

return (
<div className="-mb-12 flex flex-col">
<div className="flex flex-col">
<DialogHeader
appearance="compact"
title={title}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { formatAddress } from "LLD/features/ModularDialog/components/Address/formatAddress";

export type RecipientLike = Readonly<{
address: string;
address?: string;
ensName?: string;
}>;

export function getRecipientDisplayValue(recipient: RecipientLike | null): string {
export function getRecipientDisplayValue(recipient: RecipientLike | null): string | undefined {
if (!recipient) return "";

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

export function getRecipientSearchPrefillValue(recipient: RecipientLike | null): string {
export function getRecipientSearchPrefillValue(
recipient: RecipientLike | null,
): string | undefined {
if (!recipient) return "";
return recipient.ensName?.trim() ? recipient.ensName : recipient.address;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type { SendStepConfig } from "../types";
*/

// Data Context
type DataContextValue = Readonly<{
export type DataContextValue = Readonly<{
state: SendFlowState;
uiConfig: SendFlowUiConfig;
recipientSearch: Readonly<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe("useSendFlowTransaction", () => {
act(() => {
result.current.actions.setRecipient({
address: "cosmos1abc123",
memo: "test memo",
memo: { value: "test memo" },
});
});

Expand Down Expand Up @@ -117,7 +117,7 @@ describe("useSendFlowTransaction", () => {
act(() => {
result.current.actions.setRecipient({
address: "solana-address",
memo: "solana memo",
memo: { value: "solana memo" },
});
});

Expand Down Expand Up @@ -199,7 +199,7 @@ describe("useSendFlowTransaction", () => {
act(() => {
result.current.actions.setRecipient({
address: "casper-address",
memo: "transfer-id-123",
memo: { value: "transfer-id-123" },
});
});

Expand Down Expand Up @@ -236,7 +236,7 @@ describe("useSendFlowTransaction", () => {
act(() => {
result.current.actions.setRecipient({
address: "xrp-address",
memo: "test",
memo: { value: "test" },
destinationTag: "67890",
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useCallback, useMemo } from "react";
import useBridgeTransaction from "@ledgerhq/live-common/bridge/useBridgeTransaction";
import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
import { applyMemoToTransaction } from "@ledgerhq/live-common/bridge/descriptor";
import type { Account, AccountLike } from "@ledgerhq/types-live";
import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
import useBridgeTransaction from "@ledgerhq/live-common/bridge/useBridgeTransaction";
import type { Transaction } from "@ledgerhq/live-common/generated/types";
import type {
SendFlowTransactionState,
SendFlowTransactionActions,
RecipientData,
} from "@ledgerhq/live-common/flows/send/types";
import type { Account, AccountLike } from "@ledgerhq/types-live";

type UseSendFlowTransactionParams = Readonly<{
account: AccountLike | null;
Expand Down Expand Up @@ -57,7 +57,12 @@ export function useSendFlowTransaction({
if (recipient.memo !== undefined) {
Object.assign(
updates,
applyMemoToTransaction(transaction.family, recipient.memo, transaction),
applyMemoToTransaction(
transaction.family,
recipient.memo.value,
recipient.memo.type,
transaction,
),
);
}

Expand All @@ -66,7 +71,7 @@ export function useSendFlowTransaction({
if (Number.isFinite(parsedTag)) {
Object.assign(
updates,
applyMemoToTransaction(transaction.family, parsedTag, transaction),
applyMemoToTransaction(transaction.family, parsedTag, undefined, transaction),
);
}
}
Expand Down
Loading