From f09dbacab1c53b9900d2c3a07e9f40ebf2be8552 Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Fri, 26 Dec 2025 18:23:09 +0100 Subject: [PATCH 1/9] feat(desktop): [LLD][UI] Memo screen (new send flow - LIVE-20724) --- .../FlowWizard/FlowWizardOrchestrator.tsx | 111 ++++++ .../features/Send/SendFlowOrchestrator.tsx | 74 ++++ .../newArch/features/Send/SendFlowRoot.tsx | 72 ++++ .../Send/hooks/useSendFlowTransaction.ts | 114 ++++++ .../screens/Recipient/RecipientScreen.tsx | 64 ++++ .../components/MyAccountsSectionView.tsx | 39 ++ .../components/RecipientAddressModal.tsx | 52 +++ .../components/RecipientAddressModalView.tsx | 342 ++++++++++++++++++ .../hooks/useBridgeRecipientValidation.ts | 188 ++++++++++ .../useRecipientAddressModalViewModel.ts | 245 +++++++++++++ .../features/Send/screens/Recipient/types.ts | 73 ++++ .../src/newArch/features/Send/types.ts | 121 +++++++ .../static/i18n/en/app.json | 12 +- .../src/families/stellar/descriptor.ts | 2 +- 14 files changed, 1505 insertions(+), 4 deletions(-) create mode 100644 apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts create mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/types.ts diff --git a/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx b/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx new file mode 100644 index 00000000000..3a241f01731 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx @@ -0,0 +1,111 @@ +import React, { useMemo, type ComponentType, type ReactNode, type CSSProperties } from "react"; +import { useFlowWizardNavigation } from "./hooks/useFlowWizardNavigation"; +import type { + FlowStep, + FlowConfig, + StepRegistry, + StepRenderer, + FlowNavigationDirection, + AnimationConfig, + FlowWizardContextValue, + FlowStepConfig, +} from "./types"; + +const DEFAULT_ANIMATION_CONFIG: AnimationConfig = { + forward: "animate-fade-in", + backward: "animate-fade-out", +}; + +type FlowWizardOrchestratorProps< + TStep extends FlowStep, + TContextValue, + TStepConfig extends FlowStepConfig = FlowStepConfig, +> = Readonly<{ + flowConfig: FlowConfig; + stepRegistry: StepRegistry; + contextValue: TContextValue; + ContextProvider: ComponentType<{ + value: FlowWizardContextValue; + children: ReactNode; + }>; + animationConfig?: AnimationConfig; + getContainerStyle?: (stepConfig: TStepConfig) => CSSProperties | undefined; + children?: ReactNode; +}>; + +// Returns the transition class for the current direction; keeps UI concerns isolated here. +function getAnimationClass( + direction: FlowNavigationDirection, + config: AnimationConfig, +): string | undefined { + return direction === "FORWARD" ? config.forward : config.backward; +} + +/** + * FlowWizardOrchestrator + * + * Generic runner for multi-step flows: + * - drives navigation (forward/back/jump) via useFlowWizardNavigation + * - injects navigation & step metadata into the provided ContextProvider + * - renders the current step with optional enter animations + * - remains UI-agnostic: only needs a step registry and a flow config + */ +export function FlowWizardOrchestrator< + TStep extends FlowStep, + TContextValue, + TStepConfig extends FlowStepConfig = FlowStepConfig, +>({ + flowConfig, + stepRegistry, + contextValue, + ContextProvider, + animationConfig = DEFAULT_ANIMATION_CONFIG, + getContainerStyle, + children, +}: FlowWizardOrchestratorProps) { + const { state, actions, currentStepConfig } = useFlowWizardNavigation({ + flowConfig, + }); + + const enhancedContextValue = useMemo>( + () => ({ + ...contextValue, + navigation: actions, + currentStep: state.currentStep, + direction: state.direction, + currentStepConfig, + }), + [contextValue, actions, state.currentStep, state.direction, currentStepConfig], + ); + + const StepComponent = useMemo(() => { + const renderer = stepRegistry[state.currentStep]; + return renderer ?? null; + }, [state.currentStep, stepRegistry]); + + const hasNavigated = state.stepHistory.length > 0 || state.direction === "BACKWARD"; + const animationClass = hasNavigated + ? getAnimationClass(state.direction, animationConfig) + : undefined; + + const containerStyle = getContainerStyle ? getContainerStyle(currentStepConfig) : undefined; + return ( + +
+ {children} + {StepComponent && ( +
+ +
+ )} +
+
+ ); +} + +// Need to use it to create the step registry typesafe +export function createStepRegistry( + registry: StepRegistry, +): StepRegistry { + return registry; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx new file mode 100644 index 00000000000..cdeeb9e5787 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx @@ -0,0 +1,74 @@ +import React, { useMemo, type ReactNode } from "react"; +import { FlowWizardOrchestrator } from "../FlowWizard/FlowWizardOrchestrator"; +import type { StepRegistry, AnimationConfig, FlowWizardContextValue } from "../FlowWizard/types"; +import { SendFlowProvider } from "./context/SendFlowContext"; +import { useSendFlowBusinessLogic } from "./hooks/useSendFlowState"; +import { SEND_FLOW_CONFIG } from "./constants"; +import { SEND_FLOW_STEP } from "./types"; +import type { + SendFlowStep, + SendFlowInitParams, + SendFlowBusinessContext, + SendStepConfig, +} from "./types"; + +type SendFlowStepRegistry = StepRegistry; +type SendFlowWizardContext = FlowWizardContextValue< + SendFlowStep, + SendFlowBusinessContext, + SendStepConfig +>; + +type SendFlowOrchestratorProps = Readonly<{ + initParams?: SendFlowInitParams; + onClose: () => void; + stepRegistry: SendFlowStepRegistry; + animationConfig?: AnimationConfig; + children?: ReactNode; +}>; + +type SendFlowProviderWrapperProps = Readonly<{ + value: SendFlowWizardContext; + children: ReactNode; +}>; + +// Adapter that injects the Send context into the generic FlowWizard orchestrator +function SendFlowProviderWrapper({ value, children }: SendFlowProviderWrapperProps) { + return {children}; +} + +export function SendFlowOrchestrator({ + initParams, + onClose, + stepRegistry, + animationConfig, + children, +}: SendFlowOrchestratorProps) { + const skipAccountSelection = Boolean(initParams?.account) || Boolean(initParams?.fromMAD); + const businessContext = useSendFlowBusinessLogic({ initParams, onClose }); + const flowConfig = useMemo( + () => ({ + ...SEND_FLOW_CONFIG, + initialStep: skipAccountSelection + ? SEND_FLOW_STEP.RECIPIENT + : SEND_FLOW_STEP.ACCOUNT_SELECTION, + }), + [skipAccountSelection], + ); + + // Bridge the generic wizard runner with Send-specific business state and config + return ( + + flowConfig={flowConfig} + stepRegistry={stepRegistry} + contextValue={businessContext} + ContextProvider={SendFlowProviderWrapper} + animationConfig={animationConfig} + getContainerStyle={stepConfig => + stepConfig.sizeDialog ? { height: `${stepConfig.sizeDialog}px` } : { height: "612px" } + } + > + {children} + + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx new file mode 100644 index 00000000000..bed4011ff98 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx @@ -0,0 +1,72 @@ +import React, { useCallback } from "react"; +import { createPortal } from "react-dom"; +import { Dialog, DialogContent, DialogBody } from "@ledgerhq/lumen-ui-react"; +import { DomainServiceProvider } from "@ledgerhq/domain-service/hooks/index"; +import { useDispatch, useSelector } from "react-redux"; +import { SendWorkflow } from "."; +import { closeSendFlowDialog, sendFlowStateSelector } from "~/renderer/reducers/sendFlow"; +import { setMemoTagInfoBoxDisplay } from "~/renderer/actions/UI"; +import Snow, { isSnowTime } from "~/renderer/extra/Snow"; + +export function SendFlowRoot() { + const dispatch = useDispatch(); + const { isOpen, data } = useSelector(sendFlowStateSelector); + + const handleClose = useCallback(() => { + dispatch( + setMemoTagInfoBoxDisplay({ + isMemoTagBoxVisible: false, + forceAutoFocusOnMemoField: false, + }), + ); + data?.onClose?.(); + dispatch(closeSendFlowDialog()); + }, [data, dispatch]); + + const handleDialogOpenChange = useCallback( + (open: boolean) => { + if (!open) { + handleClose(); + } + }, + [handleClose], + ); + + if (!isOpen) return null; + + return ( + <> + {isSnowTime() && isOpen + ? createPortal( +
+ +
, + document.body, + ) + : null} + + + + + + + + + + + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts b/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts new file mode 100644 index 00000000000..78153f7552d --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts @@ -0,0 +1,114 @@ +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 type { Transaction } from "@ledgerhq/live-common/generated/types"; +import type { SendFlowTransactionState, SendFlowTransactionActions, RecipientData } from "../types"; + +type UseSendFlowTransactionParams = Readonly<{ + account: AccountLike | null; + parentAccount: Account | null; +}>; + +type UseSendFlowTransactionResult = Readonly<{ + state: SendFlowTransactionState; + actions: SendFlowTransactionActions; +}>; + +export function useSendFlowTransaction({ + account, + parentAccount, +}: UseSendFlowTransactionParams): UseSendFlowTransactionResult { + const { + transaction, + setTransaction: bridgeSetTransaction, + updateTransaction: bridgeUpdateTransaction, + status, + bridgeError, + bridgePending, + setAccount, + } = useBridgeTransaction(() => { + if (!account) return {}; + return { account, parentAccount: parentAccount ?? undefined }; + }); + + const setTransaction = useCallback( + (tx: Transaction) => bridgeSetTransaction(tx), + [bridgeSetTransaction], + ); + + const updateTransaction = useCallback( + (updater: (tx: Transaction) => Transaction) => bridgeUpdateTransaction(updater), + [bridgeUpdateTransaction], + ); + + const setRecipient = useCallback( + (recipient: RecipientData) => { + if (!account || !transaction) return; + + const bridge = getAccountBridge(account, parentAccount); + const updates: Partial = {}; + + if (recipient !== undefined) { + Object.assign(updates, { recipient: recipient.address }); + } + + if (recipient.memo !== undefined) { + Object.assign( + updates, + applyMemoToTransaction( + transaction.family, + recipient.memo.value, + recipient.memo.type, + transaction, + ), + ); + } + + if (recipient.destinationTag !== undefined) { + const parsedTag = Number(recipient.destinationTag.trim()); + if (Number.isFinite(parsedTag)) { + Object.assign( + updates, + applyMemoToTransaction(transaction.family, parsedTag, undefined, transaction), + ); + } + } + + if (Object.keys(updates).length > 0) { + bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); + } + }, + [account, parentAccount, transaction, bridgeSetTransaction], + ); + + const setAccountForTransaction = useCallback( + (newAccount: AccountLike, newParentAccount?: Account | null) => { + setAccount(newAccount, newParentAccount ?? undefined); + }, + [setAccount], + ); + + const state: SendFlowTransactionState = useMemo( + () => ({ + transaction: transaction ?? null, + status, + bridgeError: bridgeError ?? null, + bridgePending, + }), + [transaction, status, bridgeError, bridgePending], + ); + + const actions: SendFlowTransactionActions = useMemo( + () => ({ + setTransaction, + updateTransaction, + setRecipient, + setAccount: setAccountForTransaction, + }), + [setTransaction, updateTransaction, setRecipient, setAccountForTransaction], + ); + + return { state, actions }; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx new file mode 100644 index 00000000000..6e09fe54e48 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx @@ -0,0 +1,64 @@ +import React, { useCallback, useMemo } from "react"; +import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { useSendFlowContext } from "../../context/SendFlowContext"; +import { RecipientAddressModal } from "./components/RecipientAddressModal"; +import { Memo } from "../../types"; + +export function RecipientScreen() { + const { state, transaction, navigation, close, uiConfig } = useSendFlowContext(); + + const account = state.account.account; + const parentAccount = state.account.parentAccount ?? undefined; + + const currency: CryptoCurrency | TokenCurrency | null = useMemo(() => { + if (state.account.currency) return state.account.currency; + return account ? getAccountCurrency(account) : null; + }, [state.account.currency, account]); + + const handleAddressSelected = useCallback( + (address: string, ensName?: string, goToNextStep?: boolean) => { + transaction.setRecipient({ + address, + ensName, + }); + + if (goToNextStep) { + navigation.goToNextStep(); + } + }, + [transaction, navigation], + ); + + const handleMemoChange = useCallback( + (memo: Memo) => { + transaction.setRecipient({ + memo, + }); + }, + [transaction], + ); + + const handleMemoSkip = useCallback(() => { + navigation.goToNextStep(); + }, [navigation]); + + if (!account || !currency) { + return null; + // Account selection step should guarantee both account and currency are defined + } + + return ( + + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx new file mode 100644 index 00000000000..bb600d29d52 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Subheader } from "@ledgerhq/lumen-ui-react"; +import type { Account } from "@ledgerhq/types-live"; +import { AccountRowWithBalance } from "./AccountRowWithBalance"; + +type MyAccountsSectionViewProps = Readonly<{ + userAccountsForCurrency: Account[]; + accountNames: (string | undefined)[]; + onSelect: (account: Account) => void; +}>; + +export function MyAccountsSectionView({ + userAccountsForCurrency, + accountNames, + onSelect, +}: MyAccountsSectionViewProps) { + const { t } = useTranslation(); + + if (userAccountsForCurrency.length === 0) { + return null; + } + + return ( +
+ +
+ {userAccountsForCurrency.map((account, index) => ( + onSelect(account)} + /> + ))} +
+
+ ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx new file mode 100644 index 00000000000..ad92eb80bd1 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { RecipientAddressModalView } from "./RecipientAddressModalView"; +import { useRecipientAddressModalViewModel } from "../hooks/useRecipientAddressModalViewModel"; +import { Memo } from "../../../types"; + +type RecipientAddressModalProps = Readonly<{ + isOpen: boolean; + onClose: () => void; + account: AccountLike; + parentAccount?: Account; + currency: CryptoCurrency | TokenCurrency; + onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; + recipientSupportsDomain: boolean; + onMemoChange: (memo: Memo) => void; + onMemoSkip: () => void; +}>; + +export function RecipientAddressModal({ + account, + parentAccount, + currency, + onAddressSelected, + recipientSupportsDomain = false, + onMemoChange, + onMemoSkip, +}: RecipientAddressModalProps) { + const viewModel = useRecipientAddressModalViewModel({ + account, + parentAccount, + currency, + onAddressSelected, + recipientSupportsDomain, + }); + + return ( + + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx new file mode 100644 index 00000000000..17942ed443c --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx @@ -0,0 +1,342 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { + AddressInput, + Banner, + Button, + Checkbox, + Link, + Select, + SelectContent, + SelectItem, + SelectItemText, + SelectTrigger, + TextInput, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@ledgerhq/lumen-ui-react"; +import { useTranslation } from "react-i18next"; +import type { Account } from "@ledgerhq/types-live"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import type { AddressSearchResult, AddressValidationError, RecentAddress } from "../types"; +import { AddressMatchedSection } from "./AddressMatchedSection"; +import EmptyList from "./EmptyList"; +import { LoadingState } from "./LoadingState"; +import { MyAccountsSection } from "./MyAccountsSection"; +import { RecentAddressesSection } from "./RecentAddressesSection"; +import { ValidationBanner } from "./ValidationBanner"; +import { Information } from "@ledgerhq/lumen-ui-react/symbols"; +import { Memo } from "../../../types"; + +type RecipientAddressModalViewProps = Readonly<{ + searchValue: string; + isLoading: boolean; + result: AddressSearchResult; + recentAddresses: RecentAddress[]; + mainAccount: Account; + currency: CryptoCurrency | TokenCurrency; + recipientSupportsDomain: boolean; + showInitialState: boolean; + showInitialEmptyState: boolean; + showMatchedAddress: boolean; + showAddressValidationError: boolean; + showEmptyState: boolean; + showBridgeSenderError: boolean; + showSanctionedBanner: boolean; + showBridgeRecipientError: boolean; + showBridgeRecipientWarning: boolean; + isSanctioned: boolean; + isAddressComplete: boolean; + addressValidationErrorType: AddressValidationError | null; + bridgeRecipientError: Error | undefined; + bridgeRecipientWarning: Error | undefined; + bridgeSenderError: Error | undefined; + onSearchChange: (e: React.ChangeEvent) => void; + onClearSearch: () => void; + onRecentAddressSelect: (address: RecentAddress) => void; + onAccountSelect: (account: Account) => void; + onAddressSelect: (address: string, ensName?: string) => void; + onRemoveAddress: (address: RecentAddress) => void; + hasMemo: boolean; + memoType?: string; + memoTypeOptions?: readonly string[]; + memoDefaultOption?: string; + memoMaxLength?: number; + onMemoChange: (memo: Memo) => void; + onMemoSkip: () => void; +}>; + +export function RecipientAddressModalView({ + searchValue, + isLoading, + result, + recentAddresses, + mainAccount, + currency, + recipientSupportsDomain, + showInitialState, + showInitialEmptyState, + showMatchedAddress, + showAddressValidationError, + showEmptyState, + showBridgeSenderError, + showSanctionedBanner, + showBridgeRecipientError, + showBridgeRecipientWarning, + isSanctioned, + isAddressComplete, + addressValidationErrorType, + bridgeRecipientError, + bridgeRecipientWarning, + bridgeSenderError, + onSearchChange, + onClearSearch, + onRecentAddressSelect, + onAccountSelect, + onAddressSelect, + onRemoveAddress, + hasMemo, + memoType, + memoTypeOptions, + memoDefaultOption, + memoMaxLength, + onMemoChange, + onMemoSkip, +}: RecipientAddressModalViewProps) { + const { t } = useTranslation(); + + const [memo, setMemo] = useState({ + value: "", + type: memoDefaultOption, + }); + + const onMemoValueChange = useCallback( + (value: string) => { + const newMemo = { ...memo, value }; + setMemo(newMemo); + onMemoChange(newMemo); + }, + [memo, onMemoChange], + ); + + const onMemoTypeChange = useCallback( + (type: string) => { + const newMemo = { value: "", type }; + setMemo(newMemo); + onMemoChange(newMemo); + }, + [onMemoChange], + ); + + const showSkipMemo = useMemo(() => { + const noMemoWithoutType = !memo.type && memo.value.length === 0; + const noMemoWithType = memo.type && memo.type !== "NO_MEMO" && memo.value.length === 0; + return noMemoWithoutType || noMemoWithType; + }, [memo.type, memo.value.length]); + + const [skipMemoState, setSkipMemoState] = useState<"propose" | "toConfirm">("propose"); + + const showMemoValueInput = useMemo(() => { + return memo.type !== "NO_MEMO"; + }, [memo]); + + const hasMemoTypeOptions = useMemo(() => { + return memoType == "typed" && memoTypeOptions; + }, [memoType, memoTypeOptions]); + + const hasFilledMemo = useMemo(() => { + return hasMemo && (memo.value.length > 0 || memo.type === "NO_MEMO"); + }, [hasMemo, memo.type, memo.value.length]); + + return ( + <> +
+ + + {showMatchedAddress && hasMemo && ( + <> +
+ {hasMemoTypeOptions && ( + + )} + + {showMemoValueInput && ( + onMemoValueChange(e.target.value)} + suffix={ + + + + + + {t("newSendFlow.tagHelp.description", { + currency: currency.id, + memoLabel: t(["families." + currency.id + ".memo", "common.memo"]), + })} + + + } + className="mt-12 w-full" + value={memo.value} + maxLength={memoMaxLength} + errorMessage={ + result.bridgeErrors?.transaction + ? t("errors." + result.bridgeErrors?.transaction.name + ".title") + : undefined + } + /> + )} +
+ + {showSkipMemo && ( + <> +
+ {skipMemoState === "propose" && ( +
+ + {t("newSendFlow.skipMemo.notRequired", { + memoLabel: t(["families." + currency.id + ".memo", "common.memo"]), + })} +   + + setSkipMemoState("toConfirm")} + > + {t("common.skip")} + +
+ )} + + {skipMemoState === "toConfirm" && ( +
+ + {t("newSendFlow.skipMemo.confirm")} + + } + /> +
+ + {t("newSendFlow.skipMemo.neverAskAgain")} +
+
+ )} +
+ + )} + + )} +
+ +
+ {isLoading && ( +
+ +
+ )} + + {showInitialState && ( + <> + + + + )} + + {showMatchedAddress && (!hasMemo || hasFilledMemo) && ( + + )} + + {showAddressValidationError && ( +
+ +
+ )} + + {(showEmptyState || showInitialEmptyState) && ( +
+ +
+ )} + + {(showBridgeSenderError || + showSanctionedBanner || + showBridgeRecipientError || + showBridgeRecipientWarning) && ( +
+ {showBridgeSenderError && ( + + )} + {showSanctionedBanner && } + {showBridgeRecipientError && ( + + )} + {showBridgeRecipientWarning && ( + + )} +
+ )} +
+ + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts new file mode 100644 index 00000000000..475a4e66888 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts @@ -0,0 +1,188 @@ +import { useState, useMemo, useCallback, useRef } from "react"; +import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; +import { getMainAccount } from "@ledgerhq/live-common/account/index"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import type { TransactionStatus } from "@ledgerhq/live-common/generated/types"; +import type { BridgeValidationErrors, BridgeValidationWarnings } from "../types"; +import { applyMemoToTransaction } from "@ledgerhq/live-common/bridge/descriptor"; +import { Memo } from "../../../types"; + +export type BridgeRecipientValidationResult = { + errors: BridgeValidationErrors; + warnings: BridgeValidationWarnings; + isLoading: boolean; + status: TransactionStatus | null; +}; + +type UseBridgeRecipientValidationProps = { + recipient: string; + account: AccountLike | null; + parentAccount?: Account | null; + memo?: Memo; + enabled?: boolean; +}; + +const DEBOUNCE_DELAY = 300; + +/** + * Hook to validate recipient address using the bridge transaction status. + * This hook leverages the existing bridge infrastructure to get + * recipient and sender validation errors/warnings. + */ +export function useBridgeRecipientValidation({ + recipient, + account, + parentAccount, + memo, + enabled = true, +}: UseBridgeRecipientValidationProps): BridgeRecipientValidationResult { + const [validationState, setValidationState] = useState<{ + errors: BridgeValidationErrors; + warnings: BridgeValidationWarnings; + isLoading: boolean; + status: TransactionStatus | null; + }>({ + errors: {}, + warnings: {}, + isLoading: false, + status: null, + }); + + const lastRecipientRef = useRef(""); + const validationTriggeredRef = useRef(false); + const debounceTimeoutRef = useRef(null); + const abortControllerRef = useRef(null); + + const validateRecipient = useCallback(async () => { + if (!account || !recipient || !enabled) { + setValidationState({ + errors: {}, + warnings: {}, + isLoading: false, + status: null, + }); + return; + } + + // Cancel any pending validation + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + const signal = abortControllerRef.current.signal; + + setValidationState(prev => ({ ...prev, isLoading: true })); + + try { + const mainAccount = getMainAccount(account, parentAccount); + const bridge = getAccountBridge(account, parentAccount); + + // Create a transaction with the recipient to validate + let transaction = bridge.createTransaction(mainAccount); + transaction = bridge.updateTransaction(transaction, { recipient }); + + if (memo) { + applyMemoToTransaction(transaction.family, memo.value, memo.type, transaction); + } + + // Prepare the transaction (resolves ENS, validates format, etc.) + const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); + + if (signal.aborted) return; + + // Get the transaction status which contains errors and warnings + const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); + + if (signal.aborted) return; + + // Extract recipient and sender specific errors/warnings + const errors: BridgeValidationErrors = {}; + const warnings: BridgeValidationWarnings = {}; + + if (status.errors.recipient) { + errors.recipient = status.errors.recipient; + } + if (status.errors.sender) { + errors.sender = status.errors.sender; + } + if (status.errors.transaction) { + errors.transaction = status.errors.transaction; + } + + // Copy all warnings (there can be family-specific warnings) + Object.entries(status.warnings).forEach(([key, value]) => { + if (value) { + warnings[key] = value; + } + }); + + setValidationState({ + errors, + warnings, + isLoading: false, + status, + }); + } catch (error) { + if (signal.aborted) return; + + // On error, we don't block the user but log the issue + console.error("Bridge recipient validation failed:", error); + setValidationState({ + errors: {}, + warnings: {}, + isLoading: false, + status: null, + }); + } + }, [account, recipient, enabled, parentAccount, memo]); + + // Track recipient changes and trigger validation + if (recipient !== lastRecipientRef.current) { + lastRecipientRef.current = recipient; + validationTriggeredRef.current = false; + + // Clear any pending debounce + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + debounceTimeoutRef.current = null; + } + + // Reset state immediately when recipient is cleared + if (!recipient) { + setValidationState({ + errors: {}, + warnings: {}, + isLoading: false, + status: null, + }); + } + } + + // Debounced validation trigger + if (recipient && !validationTriggeredRef.current && enabled) { + validationTriggeredRef.current = true; + + // Clear any existing timeout + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + // Set loading state immediately for better UX + setValidationState(prev => ({ ...prev, isLoading: true })); + + // Debounce the actual validation + debounceTimeoutRef.current = setTimeout(() => { + validateRecipient(); + }, DEBOUNCE_DELAY); + } + + return useMemo( + () => ({ + errors: validationState.errors, + warnings: validationState.warnings, + isLoading: validationState.isLoading, + status: validationState.status, + }), + [validationState], + ); +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts new file mode 100644 index 00000000000..cd3c46b07b2 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts @@ -0,0 +1,245 @@ +import { useState, useCallback, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors"; +import { + getAccountCurrency, + getMainAccount, + getRecentAddressesStore, +} from "@ledgerhq/live-common/account/index"; +import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { accountsSelector } from "~/renderer/reducers/accounts"; +import type { RecentAddress } from "../types"; +import { useAddressValidation } from "./useAddressValidation"; + +type UseRecipientAddressModalViewModelProps = Readonly<{ + account: AccountLike; + parentAccount?: Account; + currency: CryptoCurrency | TokenCurrency; + onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; + recipientSupportsDomain: boolean; +}>; + +export function useRecipientAddressModalViewModel({ + account, + parentAccount, + currency, + onAddressSelected, + recipientSupportsDomain, +}: UseRecipientAddressModalViewModelProps) { + const [searchValue, setSearchValue] = useState(""); + const [refreshKey, setRefreshKey] = useState(0); + + const mainAccount = getMainAccount(account, parentAccount); + + const { result, isLoading } = useAddressValidation({ + searchValue, + currency, + account, + parentAccount, + currentAccountId: mainAccount.id, + }); + + const allAccounts = useSelector(accountsSelector); + const userAccountsForCurrency = useMemo(() => { + return allAccounts.filter(acc => { + if (acc.id === mainAccount.id) return false; + const accCurrency = getAccountCurrency(acc); + return accCurrency.id === currency.id; + }); + }, [allAccounts, currency, mainAccount.id]); + + const recentAddresses = useMemo(() => { + void refreshKey; + const addressesWithMetadata = getRecentAddressesStore().getAddressesWithMetadata(currency.id); + const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); + + const userAccountsByAddress = new Map( + userAccountsForCurrency.map(acc => [acc.freshAddress.toLowerCase(), acc]), + ); + + return addressesWithMetadata + .filter(entry => { + if (!entry?.address) return false; + if ( + selfTransferPolicy === "impossible" && + entry.address.toLowerCase() === mainAccount.freshAddress.toLowerCase() + ) { + return false; + } + return true; + }) + .map(entry => { + const matchedAccount = userAccountsByAddress.get(entry.address.toLowerCase()); + return { + address: entry.address, + currency, + lastUsedAt: new Date(entry.lastUsed), + name: entry.address, + isLedgerAccount: !!matchedAccount, + accountId: matchedAccount?.id, + } as RecentAddress; + }); + }, [currency, refreshKey, mainAccount.freshAddress, userAccountsForCurrency]); + + const hasSearchValue = searchValue.length > 0; + const showInitialState = !hasSearchValue; + + const hasRecentAddresses = recentAddresses.length > 0; + const hasUserAccounts = userAccountsForCurrency.length > 0; + const showInitialEmptyState = showInitialState && !hasRecentAddresses && !hasUserAccounts; + + const hasMemo = sendFeatures.hasMemo(currency); + const memoType = sendFeatures.getMemoType(currency); + const memoTypeOptions = sendFeatures.getMemoOptions(currency); + const memoDefaultOption = sendFeatures.getMemoDefaultOption(currency); + const memoMaxLength = sendFeatures.getMemoMaxLength(currency); + + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, []); + + const handleClearSearch = useCallback(() => { + setSearchValue(""); + }, []); + + const handleRecentAddressSelect = useCallback( + (address: RecentAddress) => { + setSearchValue(address.ensName ?? address.address); + onAddressSelected(address.address, address.ensName, !hasMemo); + }, + [hasMemo, onAddressSelected], + ); + + const handleAccountSelect = useCallback( + (selectedAccount: Account) => { + setSearchValue(selectedAccount.freshAddress); + onAddressSelected(selectedAccount.freshAddress, undefined, !hasMemo); + }, + [hasMemo, onAddressSelected], + ); + + const handleAddressSelect = useCallback( + (address: string, ensName?: string) => { + setSearchValue(ensName ?? address); + onAddressSelected(address, ensName, !hasMemo); + }, + [hasMemo, onAddressSelected], + ); + + const handleRemoveAddress = useCallback( + (address: RecentAddress) => { + getRecentAddressesStore().removeAddress(currency.id, address.address); + setRefreshKey(prev => prev + 1); + }, + [currency], + ); + + const showSearchResults = hasSearchValue && !isLoading; + const isSanctioned = result.status === "sanctioned"; + + const isAddressComplete = useMemo(() => { + return ( + result.status === "valid" || + result.status === "ens_resolved" || + result.status === "sanctioned" + ); + }, [result.status]); + + const hasAnyMatches = + (result.matchedAccounts && result.matchedAccounts.length > 0) || + !!result.matchedRecentAddress || + !!result.ensName || + result.isLedgerAccount || + isSanctioned; + + const showSanctionedBanner = isSanctioned && hasSearchValue; + + const bridgeRecipientError = result.bridgeErrors?.recipient; + const bridgeRecipientWarning = result.bridgeWarnings?.recipient; + const bridgeSenderError = result.bridgeErrors?.sender; + + const isSelfTransferError = + bridgeRecipientError instanceof InvalidAddressBecauseDestinationIsAlsoSource; + const isBridgeInvalidAddress = + bridgeRecipientError instanceof InvalidAddress && !isSelfTransferError; + + const showMatchedAddress = + showSearchResults && + (hasAnyMatches || + (result.status === "valid" && + !result.error && + !bridgeRecipientError && + !isBridgeInvalidAddress)) && + (result.status === "valid" || + (recipientSupportsDomain && result.status === "ens_resolved") || + result.isLedgerAccount || + !!result.matchedRecentAddress || + isSanctioned); + + const showAddressValidationError = + showSearchResults && + !showSanctionedBanner && + !hasAnyMatches && + (!!result.error || isBridgeInvalidAddress); + + const addressValidationErrorType = + result.error ?? (isBridgeInvalidAddress ? "incorrect_format" : null); + + const showBridgeRecipientError = + showSearchResults && + !!bridgeRecipientError && + !isBridgeInvalidAddress && + !showSanctionedBanner && + !hasAnyMatches; + const showBridgeRecipientWarning = + showSearchResults && + !!bridgeRecipientWarning && + !showBridgeRecipientError && + !showAddressValidationError; + const showBridgeSenderError = showSearchResults && !!bridgeSenderError; + + const showEmptyState = + showSearchResults && + (!isAddressComplete || !hasAnyMatches) && + !showMatchedAddress && + !showSanctionedBanner && + !showAddressValidationError && + !showBridgeRecipientError; + + return { + searchValue, + isLoading, + result, + recentAddresses, + mainAccount, + showInitialState, + showInitialEmptyState, + showSearchResults, + showMatchedAddress, + showAddressValidationError, + showEmptyState, + showBridgeSenderError, + showSanctionedBanner, + showBridgeRecipientError, + showBridgeRecipientWarning, + isSanctioned, + isAddressComplete, + addressValidationErrorType, + bridgeRecipientError, + bridgeRecipientWarning, + bridgeSenderError, + handleSearchChange, + handleClearSearch, + handleRecentAddressSelect, + handleAccountSelect, + handleAddressSelect, + handleRemoveAddress, + hasMemo, + memoType, + memoTypeOptions, + memoDefaultOption, + memoMaxLength, + }; +} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts new file mode 100644 index 00000000000..672d224e15a --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts @@ -0,0 +1,73 @@ +import type { Account } from "@ledgerhq/types-live"; +import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; + +export type RecentAddress = Readonly<{ + address: string; + currency: CryptoCurrency | TokenCurrency; + lastUsedAt: Date; + name?: string; + ensName?: string; + isLedgerAccount?: boolean; + accountId?: string; +}>; + +export type AddressValidationStatus = + | "idle" + | "loading" + | "valid" + | "invalid" + | "sanctioned" + | "ens_resolved"; + +export type AddressValidationError = + | "incorrect_format" + | "sanctioned" + | "incompatible_asset" + | "wallet_not_exist" + | null; + +export type MatchedAccount = Readonly<{ + account: Account; + accountName: string | undefined; + accountBalance: string | undefined; + accountBalanceFormatted: string | undefined; +}>; + +export type BridgeValidationErrors = { + recipient?: Error; + sender?: Error; + transaction?: Error; +}; + +export type BridgeValidationWarnings = Record; + +export type AddressSearchResult = Readonly<{ + status: AddressValidationStatus; + error: AddressValidationError; + resolvedAddress: string | undefined; + ensName: string | undefined; + isLedgerAccount: boolean; + accountName: string | undefined; + accountBalance: string | undefined; + accountBalanceFormatted: string | undefined; + isFirstInteraction: boolean; + matchedRecentAddress: RecentAddress | undefined; + matchedAccounts: MatchedAccount[]; + bridgeErrors: BridgeValidationErrors | undefined; + bridgeWarnings: BridgeValidationWarnings | undefined; +}>; + +export type AddressListItemProps = { + address: string; + name?: string; + description?: string; + date?: Date; + balance?: string; + balanceFormatted?: string; + onSelect: () => void; + onContextMenu?: (e: React.MouseEvent) => void; + showSendTo?: boolean; + isLedgerAccount?: boolean; + disabled?: boolean; + hideDescription?: boolean; +}; diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/types.ts b/apps/ledger-live-desktop/src/newArch/features/Send/types.ts new file mode 100644 index 00000000000..7e78c82a022 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Send/types.ts @@ -0,0 +1,121 @@ +import type { Account, AccountLike, Operation } from "@ledgerhq/types-live"; +import type { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; +import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types"; +import type { + FlowNavigationDirection, + FlowNavigationActions, + FlowStepConfig, + FlowConfig, + FlowStatus, + FlowStatusActions, +} from "../FlowWizard/types"; + +export const SEND_FLOW_STEP = { + ACCOUNT_SELECTION: "ACCOUNT_SELECTION", + RECIPIENT: "RECIPIENT", + AMOUNT: "AMOUNT", + SIGNATURE: "SIGNATURE", + CONFIRMATION: "CONFIRMATION", +}; + +export type Memo = { value: string; type?: string }; + +export type SendFlowStep = (typeof SEND_FLOW_STEP)[keyof typeof SEND_FLOW_STEP]; + +export type SendStepConfig = FlowStepConfig & + Readonly<{ + showTitle?: boolean; + sizeDialog?: number; + }>; + +export type SendFlowConfig = FlowConfig; +export type NavigationDirection = FlowNavigationDirection; +export type SendFlowNavigationActions = FlowNavigationActions; + +export type SendFlowUiConfig = Readonly<{ + hasMemo: boolean; + memoType?: string; + memoMaxLength?: number; + memoMaxValue?: number; + memoOptions?: readonly string[]; + recipientSupportsDomain: boolean; + hasFeePresets: boolean; + hasCustomFees: boolean; + hasCoinControl: boolean; +}>; + +export type RecipientData = Readonly<{ + address?: string; + ensName?: string; + memo?: Memo; + destinationTag?: string; +}>; + +export type SendFlowTransactionState = Readonly<{ + transaction: Transaction | null; + status: TransactionStatus; + bridgeError: Error | null; + bridgePending: boolean; +}>; + +export type SendFlowAccountState = Readonly<{ + account: AccountLike | null; + parentAccount: Account | null; + currency: CryptoOrTokenCurrency | null; +}>; + +export type SendFlowOperationResult = Readonly<{ + optimisticOperation: Operation | null; + transactionError: Error | null; + signed: boolean; +}>; + +export type SendFlowState = Readonly<{ + account: SendFlowAccountState; + transaction: SendFlowTransactionState; + recipient: RecipientData | null; + operation: SendFlowOperationResult; + isLoading: boolean; + flowStatus: FlowStatus; +}>; + +export type SendFlowTransactionActions = Readonly<{ + setTransaction: (tx: Transaction) => void; + updateTransaction: (updater: (tx: Transaction) => Transaction) => void; + setRecipient: (recipient: RecipientData) => void; + setAccount: (account: AccountLike, parentAccount?: Account | null) => void; +}>; + +export type SendFlowOperationActions = Readonly<{ + onOperationBroadcasted: (operation: Operation) => void; + onTransactionError: (error: Error) => void; + onSigned: () => void; + onRetry: () => void; +}>; + +export type SendFlowInitParams = Readonly<{ + account?: AccountLike; + parentAccount?: Account; + recipient?: string; + amount?: string; + memo?: string; + fromMAD?: boolean; +}>; + +export type SendFlowBusinessContext = Readonly<{ + state: SendFlowState; + transaction: SendFlowTransactionActions; + operation: SendFlowOperationActions; + status: FlowStatusActions; + uiConfig: SendFlowUiConfig; + close: () => void; + setAccountAndNavigate: (account: AccountLike, parentAccount?: Account) => void; +}>; + +export type SendFlowContextValue = SendFlowBusinessContext & + Readonly<{ + navigation: SendFlowNavigationActions; + currentStep: SendFlowStep; + direction: NavigationDirection; + currentStepConfig: SendStepConfig; + }>; diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 2656d20268b..4278e28c15f 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -152,7 +152,8 @@ "quote": "Quote", "memoTag": { "learnMore": "Learn more about Tag/Memo" - } + }, + "memo": "Memo" }, "devices": { "nanoS": "Nano S", @@ -5795,11 +5796,13 @@ "casper": { "transferIdPlaceholder": "Optional", "transferId": "Transfer ID (Memo)", - "transferIdWarningText": "When using a memo, carefully check the information with the recipient" + "transferIdWarningText": "When using a memo, carefully check the information with the recipient", + "memo": "Transfer ID (Memo)" }, "ton": { "commentPlaceholder": "Optional", - "comment": "Comment" + "comment": "Comment", + "memo": "Comment" }, "canton": { "commentPlaceholder": "Optional", @@ -5957,6 +5960,9 @@ }, "mina": { "memoPlaceholder": "Optional Memo" + }, + "xrp": { + "memo": "Tag" } }, "errors": { diff --git a/libs/ledger-live-common/src/families/stellar/descriptor.ts b/libs/ledger-live-common/src/families/stellar/descriptor.ts index 79ea3d9d84d..eaadfee8d9f 100644 --- a/libs/ledger-live-common/src/families/stellar/descriptor.ts +++ b/libs/ledger-live-common/src/families/stellar/descriptor.ts @@ -7,7 +7,7 @@ export const descriptor: CoinDescriptor = { memo: { type: "typed", options: StellarMemoType, - defaultOption: "MEMO_ID", + defaultOption: "MEMO_TEXT", }, }, fees: { From 4bc7b5eba3444df9ea0e7ea885547ddde3b8e40a Mon Sep 17 00:00:00 2001 From: Diyaeddine LAOUID Date: Tue, 20 Jan 2026 17:53:02 +0100 Subject: [PATCH 2/9] feat: refacto / fix conflicts / update banner --- .zed/debug.json | 5 + .../components/Address/formatAddress.ts | 2 +- .../features/Send/components/SendHeader.tsx | 171 ++++++++- .../mvvm/features/Send/components/utils.ts | 2 +- .../__tests__/useSendFlowTransaction.test.ts | 8 +- .../features/Send/hooks/useSendFlowState.ts | 1 + .../Send/hooks/useSendFlowTransaction.ts | 56 ++- .../screens/Recipient/RecipientScreen.tsx | 15 +- .../components/Memo/MemoTypeSelect.tsx | 35 ++ .../components/Memo/MemoValueInput.tsx | 52 +++ .../components/Memo/SkipMemoSection.tsx | 87 +++++ .../components/RecipientAddressModal.tsx | 42 ++- .../components/RecipientAddressModalView.tsx | 77 ++-- .../hooks/useBridgeRecipientValidation.ts | 69 ++-- .../useRecipientAddressModalViewModel.ts | 104 ++++-- .../Recipient/hooks/useRecipientMemo.ts | 125 +++++++ .../features/Send/screens/Recipient/types.ts | 1 + .../FlowWizard/FlowWizardOrchestrator.tsx | 111 ------ .../features/Send/SendFlowOrchestrator.tsx | 74 ---- .../newArch/features/Send/SendFlowRoot.tsx | 72 ---- .../Send/hooks/useSendFlowTransaction.ts | 114 ------ .../screens/Recipient/RecipientScreen.tsx | 64 ---- .../components/MyAccountsSectionView.tsx | 39 -- .../components/RecipientAddressModal.tsx | 52 --- .../components/RecipientAddressModalView.tsx | 342 ------------------ .../hooks/useBridgeRecipientValidation.ts | 188 ---------- .../useRecipientAddressModalViewModel.ts | 245 ------------- .../features/Send/screens/Recipient/types.ts | 73 ---- .../src/newArch/features/Send/types.ts | 121 ------- .../static/i18n/en/app.json | 11 +- .../src/bridge/descriptor.test.ts | 2 +- .../src/bridge/descriptor.ts | 26 +- 32 files changed, 729 insertions(+), 1657 deletions(-) create mode 100644 .zed/debug.json create mode 100644 apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoTypeSelect.tsx create mode 100644 apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoValueInput.tsx create mode 100644 apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx create mode 100644 apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts delete mode 100644 apps/ledger-live-desktop/src/newArch/features/Send/types.ts diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 00000000000..4be1a903ab3 --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,5 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[] diff --git a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts index 086ecbba942..976296a7b14 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts @@ -10,7 +10,7 @@ * @returns Formatted address string */ export const formatAddress = ( - address: string, + address?: string, options: { prefixLength?: number; suffixLength?: number; diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx index f8a8893b70d..3ca50469653 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx @@ -1,17 +1,67 @@ +import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; +import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; +import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index"; +import { useCalculate } from "@ledgerhq/live-countervalues-react"; +import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react"; +import type { AccountLike } from "@ledgerhq/types-live"; +import { useSelector } from "LLD/hooks/redux"; +import { BigNumber } from "bignumber.js"; 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 { useMaybeAccountUnit } from "~/renderer/hooks/useAccountUnit"; +import { counterValueCurrencySelector, localeSelector } from "~/renderer/reducers/settings"; import { useFlowWizard } from "../../FlowWizard/FlowWizardContext"; -import { useSendFlowData, useSendFlowActions } from "../context/SendFlowContext"; import { SEND_FLOW_STEP, type SendFlowStep, type SendFlowBusinessContext, } from "@ledgerhq/live-common/flows/send/types"; import type { SendStepConfig } from "../types"; +import { useSendFlowActions, useSendFlowData } from "../context/SendFlowContext"; +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 { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "./utils"; -import { useAvailableBalance } from "../hooks/useAvailableBalance"; + +function useAvailableBalance(account?: AccountLike | null) { + const locale = useSelector(localeSelector); + const counterValueCurrency = useSelector(counterValueCurrencySelector); + const unit = useMaybeAccountUnit(account ?? undefined); + + const accountCurrency = useMemo( + () => (account ? getAccountCurrency(account) : undefined), + [account], + ); + + const counterValue = useCalculate({ + from: accountCurrency ?? counterValueCurrency, + to: counterValueCurrency, + value: account?.balance.toNumber() ?? 0, + disableRounding: true, + }); + + const availableBalanceFormatted = useMemo(() => { + if (!account || !unit) return ""; + return formatCurrencyUnit(unit, account.balance, { + showCode: true, + locale, + }); + }, [account, unit, locale]); + + const counterValueFormatted = useMemo(() => { + if (typeof counterValue !== "number" || !counterValueCurrency) return ""; + return formatCurrencyUnit(counterValueCurrency.units[0], new BigNumber(counterValue), { + showCode: true, + locale, + }); + }, [counterValue, counterValueCurrency, locale]); + + return useMemo(() => { + if (!account) return ""; + return counterValueFormatted || availableBalanceFormatted || ""; + }, [account, counterValueFormatted, availableBalanceFormatted]); +} export function SendHeader() { const wizard = useFlowWizard(); @@ -71,6 +121,41 @@ export function SendHeader() { handleBack(); }, [handleBack, isAmountStep, recipientSearch, state.recipient]); + const showMemoControls = Boolean( + showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0, + ); + + const currencyId = state.account.currency?.id; + const memoDefaultOption = useMemo(() => { + return state.account.currency + ? sendFeatures.getMemoDefaultOption(state.account.currency) + : undefined; + }, [state.account.currency]); + + const memoTypeOptions = useMemo(() => { + return uiConfig.memoOptions ?? []; + }, [uiConfig]); + const memoType = uiConfig.memoType; + const memoMaxLength = uiConfig.memoMaxLength; + + const memoViewModel = useRecipientMemo({ + hasMemo: uiConfig.hasMemo, + memoDefaultOption, + memoType, + memoTypeOptions, + onMemoChange: memo => { + transaction.setRecipient({ memo }); + }, + onMemoSkip: () => { + navigation.goToNextStep(); + }, + resetKey: `${state.account.account?.id ?? ""}|${currencyId ?? ""}|${ + recipientSearch.value.length === 0 ? "empty" : "filled" + }`, + }); + + const transactionErrorName = state.transaction.status?.errors?.transaction?.name; + const recipientInputContent = useMemo(() => { if (!showRecipientInput) return null; @@ -91,30 +176,82 @@ export function SendHeader() { } return ( - recipientSearch.setValue(e.target.value)} - onClear={recipientSearch.clear} - placeholder={ - uiConfig.recipientSupportsDomain - ? t("newSendFlow.placeholder") - : t("newSendFlow.placeholderNoENS") - } - /> + <> + recipientSearch.setValue(e.target.value)} + onClear={recipientSearch.clear} + placeholder={ + uiConfig.recipientSupportsDomain + ? t("newSendFlow.placeholder") + : t("newSendFlow.placeholderNoENS") + } + /> + {showMemoControls && currencyId ? ( +
+
+ {memoViewModel.hasMemoTypeOptions ? ( + + ) : null} + + {memoViewModel.showMemoValueInput ? ( + + ) : null} +
+ + {memoViewModel.showSkipMemo ? ( + + ) : null} +
+ ) : null} + ); }, [ showRecipientInput, isAmountStep, addressInputValue, - handleRecipientInputClick, recipientSearch, uiConfig.recipientSupportsDomain, t, + showMemoControls, + currencyId, + memoViewModel.hasMemoTypeOptions, + memoViewModel.memo.type, + memoViewModel.memo.value, + memoViewModel.onMemoTypeChange, + memoViewModel.showMemoValueInput, + memoViewModel.onMemoValueChange, + memoViewModel.showSkipMemo, + memoViewModel.skipMemoState, + memoViewModel.onSkipMemoRequestConfirm, + memoViewModel.onSkipMemoCancelConfirm, + memoViewModel.onSkipMemoConfirm, + memoTypeOptions, + memoMaxLength, + transactionErrorName, + handleRecipientInputClick, ]); return ( -
+
; diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/__tests__/useSendFlowTransaction.test.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/__tests__/useSendFlowTransaction.test.ts index 9d917ba29f1..4029c8c8293 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/__tests__/useSendFlowTransaction.test.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/__tests__/useSendFlowTransaction.test.ts @@ -76,7 +76,7 @@ describe("useSendFlowTransaction", () => { act(() => { result.current.actions.setRecipient({ address: "cosmos1abc123", - memo: "test memo", + memo: { value: "test memo" }, }); }); @@ -117,7 +117,7 @@ describe("useSendFlowTransaction", () => { act(() => { result.current.actions.setRecipient({ address: "solana-address", - memo: "solana memo", + memo: { value: "solana memo" }, }); }); @@ -199,7 +199,7 @@ describe("useSendFlowTransaction", () => { act(() => { result.current.actions.setRecipient({ address: "casper-address", - memo: "transfer-id-123", + memo: { value: "transfer-id-123" }, }); }); @@ -236,7 +236,7 @@ describe("useSendFlowTransaction", () => { act(() => { result.current.actions.setRecipient({ address: "xrp-address", - memo: "test", + memo: { value: "test" }, destinationTag: "67890", }); }); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts index 7d4191ad6f9..61b62ff0b62 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts @@ -62,6 +62,7 @@ export function useSendFlowBusinessLogic({ (recipient: RecipientData) => { setRecipient(recipient); transactionHook.actions.setRecipient(recipient); + setRecipient(prev => (prev ? { ...prev, ...recipient } : recipient)); }, [transactionHook.actions], ); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts index 5153077fe76..3337c025ea8 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts @@ -47,33 +47,61 @@ export function useSendFlowTransaction({ [bridgeUpdateTransaction], ); - const setRecipient = useCallback( - (recipient: RecipientData) => { - if (!account || !transaction) return; + const buildRecipientUpdates = useCallback( + (currentTransaction: Transaction, recipient: RecipientData): Partial => { + const updates: Partial = {}; - const bridge = getAccountBridge(account, parentAccount); - const updates: Partial = { recipient: recipient.address }; + if (recipient.address !== undefined) { + updates.recipient = recipient.address; + } if (recipient.memo !== undefined) { Object.assign( updates, - applyMemoToTransaction(transaction.family, recipient.memo, transaction), + applyMemoToTransaction( + currentTransaction.family, + recipient.memo.value, + recipient.memo.type, + currentTransaction, + ), ); } if (recipient.destinationTag !== undefined) { - const parsedTag = Number(recipient.destinationTag.trim()); - if (Number.isFinite(parsedTag)) { - Object.assign( - updates, - applyMemoToTransaction(transaction.family, parsedTag, transaction), - ); + const trimmed = recipient.destinationTag.trim(); + if (trimmed.length > 0) { + const parsedTag = Number(trimmed); + if (Number.isFinite(parsedTag)) { + Object.assign( + updates, + applyMemoToTransaction( + currentTransaction.family, + parsedTag, + undefined, + currentTransaction, + ), + ); + } } } - bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); + return updates; + }, + [], + ); + + const setRecipient = useCallback( + (recipient: RecipientData) => { + if (!account || !transaction) return; + + const bridge = getAccountBridge(account, parentAccount); + const updates = buildRecipientUpdates(transaction, recipient); + + if (Object.keys(updates).length > 0) { + bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); + } }, - [account, parentAccount, transaction, bridgeSetTransaction], + [account, parentAccount, transaction, bridgeSetTransaction, buildRecipientUpdates], ); const setAccountForTransaction = useCallback( diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx index 8d31fdf0ae1..1213a58d3d4 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx @@ -1,12 +1,12 @@ -import React, { useCallback, useMemo } from "react"; import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import React, { useCallback, useMemo } from "react"; import { useFlowWizard } from "../../../FlowWizard/FlowWizardContext"; -import { useSendFlowData, useSendFlowActions } from "../../context/SendFlowContext"; +import { useSendFlowActions, useSendFlowData } from "../../context/SendFlowContext"; import { RecipientAddressModal } from "./components/RecipientAddressModal"; export function RecipientScreen() { - const { state, uiConfig, recipientSearch } = useSendFlowData(); + const { state, uiConfig } = useSendFlowData(); const { transaction, close } = useSendFlowActions(); const { navigation } = useFlowWizard(); @@ -19,16 +19,17 @@ export function RecipientScreen() { }, [state.account.currency, account]); const handleAddressSelected = useCallback( - (address: string, ensName?: string) => { + (address: string, ensName?: string, goToNextStep?: boolean) => { transaction.setRecipient({ address, ensName, }); - recipientSearch.clear(); - navigation.goToNextStep(); + if (goToNextStep) { + navigation.goToNextStep(); + } }, - [transaction, navigation, recipientSearch], + [transaction, navigation], ); if (!account || !currency) { diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoTypeSelect.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoTypeSelect.tsx new file mode 100644 index 00000000000..cb5dc81dec2 --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoTypeSelect.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectItemText, + SelectTrigger, +} from "@ledgerhq/lumen-ui-react"; +import { useTranslation } from "react-i18next"; + +type MemoTypeSelectProps = Readonly<{ + currencyId: string; + options: readonly string[]; + value?: string; + onChange: (value: string) => void; +}>; + +function MemoTypeSelectComponent({ currencyId, options, value, onChange }: MemoTypeSelectProps) { + const { t } = useTranslation(); + + return ( + + ); +} + +export const MemoTypeSelect = React.memo(MemoTypeSelectComponent); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoValueInput.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoValueInput.tsx new file mode 100644 index 00000000000..550308f28ae --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/MemoValueInput.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { TextInput, Tooltip, TooltipContent, TooltipTrigger } from "@ledgerhq/lumen-ui-react"; +import { Information } from "@ledgerhq/lumen-ui-react/symbols"; +import { useTranslation } from "react-i18next"; + +type MemoValueInputProps = Readonly<{ + currencyId: string; + value: string; + maxLength?: number; + transactionErrorName?: string; + onChange: (value: string) => void; +}>; + +function MemoValueInputComponent({ + currencyId, + value, + maxLength, + transactionErrorName, + onChange, +}: MemoValueInputProps) { + const { t } = useTranslation(); + const memoLabel = t([`families.${currencyId}.memo`, "common.memo"]); + const errorMessage = transactionErrorName ? t(`errors.${transactionErrorName}.title`) : undefined; + + return ( + onChange(e.target.value)} + suffix={ + + + + + +
+ {t("newSendFlow.tagHelp.description", { + currency: currencyId, + memoLabel, + })} +
+
+
+ } + className="w-full" + value={value} + maxLength={maxLength} + errorMessage={errorMessage} + /> + ); +} + +export const MemoValueInput = React.memo(MemoValueInputComponent); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx new file mode 100644 index 00000000000..8ae4bd14357 --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx @@ -0,0 +1,87 @@ +import React, { useCallback, useState } from "react"; +import { Banner, Button, Checkbox, Link } from "@ledgerhq/lumen-ui-react"; +import { useTranslation } from "react-i18next"; +import { openURL } from "~/renderer/linking"; +import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls"; +import { urls } from "~/config/urls"; +import type { SkipMemoState } from "../../hooks/useRecipientMemo"; + +type SkipMemoSectionProps = Readonly<{ + currencyId: string; + state: SkipMemoState; + onRequestConfirm: () => void; + onCancelConfirm: () => void; + onConfirm: () => void; +}>; + +function SkipMemoSectionComponent({ + currencyId, + state, + onRequestConfirm, + onCancelConfirm, + onConfirm, +}: SkipMemoSectionProps) { + const { t } = useTranslation(); + const memoLabel = t([`families.${currencyId}.memo`, "common.memo"]); + const [doNotAskAgain, setDoNotAskAgain] = useState(false); + + const learnMoreUrl = useLocalizedUrl(urls.memoTag.learnMore); + const handleLearnMore = useCallback(() => { + if (learnMoreUrl) { + openURL(learnMoreUrl); + } + }, [learnMoreUrl]); + + const toggleDoNotAskAgain = useCallback(() => { + setDoNotAskAgain(prev => !prev); + }, []); + + if (state === "propose") { + return ( +
+ + {t("newSendFlow.skipMemo.notRequired", { memoLabel })} +   + + + {t("common.skip")} + +
+ ); + } + + return ( +
+ + {t("newSendFlow.skipMemo.confirm")} + + } + secondaryAction={ + + } + /> + +
+ ); +} + +export const SkipMemoSection = React.memo(SkipMemoSectionComponent); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx index 1136f5474b7..a8159d06419 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx @@ -10,7 +10,7 @@ type RecipientAddressModalProps = Readonly<{ account: AccountLike; parentAccount?: Account; currency: CryptoOrTokenCurrency; - onAddressSelected: (address: string, ensName?: string) => void; + onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; recipientSupportsDomain: boolean; }>; @@ -37,12 +37,40 @@ export function RecipientAddressModal({ return ( ); } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx index 9da39eafd3d..4754c3a5711 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx @@ -14,13 +14,16 @@ import type { AddressValidationError as AddressValidationErrorType, } from "../types"; -type RecipientAddressModalViewProps = Readonly<{ +type RecipientAddressModalViewData = Readonly<{ searchValue: string; isLoading: boolean; result: AddressSearchResult; recentAddresses: RecentAddress[]; mainAccount: Account; currency: CryptoOrTokenCurrency; +}>; + +type RecipientAddressModalViewUi = Readonly<{ showInitialState: boolean; showInitialEmptyState: boolean; showMatchedAddress: boolean; @@ -36,39 +39,47 @@ type RecipientAddressModalViewProps = Readonly<{ bridgeRecipientError: Error | undefined; bridgeRecipientWarning: Error | undefined; bridgeSenderError: Error | undefined; + hasMemo: boolean; + hasMemoValidationError: boolean; + hasFilledMemo: boolean; +}>; + +type RecipientAddressModalViewActions = Readonly<{ onRecentAddressSelect: (address: RecentAddress) => void; onAccountSelect: (account: Account) => void; onAddressSelect: (address: string, ensName?: string) => void; onRemoveAddress: (address: RecentAddress) => void; }>; -export function RecipientAddressModalView({ - searchValue, - isLoading, - result, - recentAddresses, - mainAccount, - currency, - showInitialState, - showInitialEmptyState, - showMatchedAddress, - showAddressValidationError, - showEmptyState, - showBridgeSenderError, - showSanctionedBanner, - showBridgeRecipientError, - showBridgeRecipientWarning, - isSanctioned, - isAddressComplete, - addressValidationErrorType, - bridgeRecipientError, - bridgeRecipientWarning, - bridgeSenderError, - onRecentAddressSelect, - onAccountSelect, - onAddressSelect, - onRemoveAddress, -}: RecipientAddressModalViewProps) { +type RecipientAddressModalViewProps = Readonly<{ + data: RecipientAddressModalViewData; + ui: RecipientAddressModalViewUi; + actions: RecipientAddressModalViewActions; +}>; + +export function RecipientAddressModalView({ data, ui, actions }: RecipientAddressModalViewProps) { + const { searchValue, isLoading, result, recentAddresses, mainAccount, currency } = data; + const { + showInitialState, + showInitialEmptyState, + showMatchedAddress, + showAddressValidationError, + showEmptyState, + showBridgeSenderError, + showSanctionedBanner, + showBridgeRecipientError, + showBridgeRecipientWarning, + isSanctioned, + isAddressComplete, + addressValidationErrorType, + bridgeRecipientError, + bridgeRecipientWarning, + bridgeSenderError, + hasMemo, + hasMemoValidationError, + hasFilledMemo, + } = ui; + const shouldShowErrorBanner = !isLoading && (showBridgeSenderError || @@ -84,22 +95,22 @@ export function RecipientAddressModalView({ <> )} - {showMatchedAddress && ( + {showMatchedAddress && (!hasMemo || (hasFilledMemo && !hasMemoValidationError)) && ( (""); - const validationTriggeredRef = useRef(false); - const debounceTimeoutRef = useRef(null); + const debounceTimeoutRef = useRef | null>(null); const abortControllerRef = useRef(null); + const lastValidationKeyRef = useRef(""); + const validationTriggeredRef = useRef(false); - // Cleanup function to clear timeout and abort pending validations const cleanup = useCallback(() => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); @@ -63,10 +66,9 @@ export function useBridgeRecipientValidation({ }, []); const validateRecipient = useCallback(async () => { - // Clear timeout reference when callback executes debounceTimeoutRef.current = null; - if (!account || !recipient || !enabled) { + if (!enabled || !account || !recipient) { setValidationState({ errors: {}, warnings: {}, @@ -76,7 +78,6 @@ export function useBridgeRecipientValidation({ return; } - // Cancel any pending validation if (abortControllerRef.current) { abortControllerRef.current.abort(); } @@ -92,28 +93,31 @@ export function useBridgeRecipientValidation({ let transaction = bridge.createTransaction(mainAccount); transaction = bridge.updateTransaction(transaction, { recipient }); - const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); + if (memo) { + const memoUpdates = applyMemoToTransaction( + transaction.family, + memo.value, + memo.type, + transaction, + ); + transaction = bridge.updateTransaction(transaction, memoUpdates); + } + const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); if (signal.aborted) return; const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); - if (signal.aborted) return; const errors: BridgeValidationErrors = {}; const warnings: BridgeValidationWarnings = {}; - if (status.errors.recipient) { - errors.recipient = status.errors.recipient; - } - if (status.errors.sender) { - errors.sender = status.errors.sender; - } + if (status.errors.recipient) errors.recipient = status.errors.recipient; + if (status.errors.sender) errors.sender = status.errors.sender; + if (status.errors.transaction) errors.transaction = status.errors.transaction; Object.entries(status.warnings).forEach(([key, value]) => { - if (value) { - warnings[key] = value; - } + if (value) warnings[key] = value; }); setValidationState({ @@ -124,7 +128,6 @@ export function useBridgeRecipientValidation({ }); } catch (error) { if (signal.aborted) return; - console.error("Bridge recipient validation failed:", error); setValidationState({ errors: {}, @@ -133,18 +136,18 @@ export function useBridgeRecipientValidation({ status: null, }); } - }, [account, parentAccount, recipient, enabled]); + }, [account, enabled, memo, parentAccount, recipient]); - if (recipient !== lastRecipientRef.current) { - lastRecipientRef.current = recipient; - validationTriggeredRef.current = false; + const validationKey = `${enabled ? 1 : 0}|${account?.id ?? ""}|${parentAccount?.id ?? ""}|${recipient}|${ + memo?.type ?? "" + }|${memo?.value ?? ""}`; - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - debounceTimeoutRef.current = null; - } + if (validationKey !== lastValidationKeyRef.current) { + lastValidationKeyRef.current = validationKey; + validationTriggeredRef.current = false; + cleanup(); - if (!recipient) { + if (!enabled || !account || !recipient) { setValidationState({ errors: {}, warnings: {}, @@ -154,17 +157,11 @@ export function useBridgeRecipientValidation({ } } - if (recipient && !validationTriggeredRef.current && enabled) { + if (enabled && account && recipient && !validationTriggeredRef.current) { validationTriggeredRef.current = true; - - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - } - setValidationState(prev => ({ ...prev, isLoading: true })); - debounceTimeoutRef.current = setTimeout(() => { - validateRecipient(); + validateRecipient().catch(() => undefined); }, DEBOUNCE_DELAY); } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts index 2e29c0e4b75..93d9a3abd51 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts @@ -6,11 +6,7 @@ import { getRecentAddressesStore, } from "@ledgerhq/live-common/account/index"; import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; -import type { - Account, - AccountLike, - RecentAddress as RecentAddressFromStore, -} from "@ledgerhq/types-live"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { accountsSelector } from "~/renderer/reducers/accounts"; import type { RecentAddress } from "../types"; @@ -19,11 +15,15 @@ import { useRecipientSearchState } from "./useRecipientSearchState"; import { normalizeLastUsedTimestamp } from "../utils/dateFormatter"; import { useSendFlowData } from "../../../context/SendFlowContext"; +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + type UseRecipientAddressModalViewModelProps = Readonly<{ account: AccountLike; parentAccount?: Account; currency: CryptoCurrency | TokenCurrency; - onAddressSelected: (address: string, ensName?: string) => void; + onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; recipientSupportsDomain: boolean; }>; @@ -34,7 +34,7 @@ export function useRecipientAddressModalViewModel({ onAddressSelected, recipientSupportsDomain, }: UseRecipientAddressModalViewModelProps) { - const { recipientSearch } = useSendFlowData(); + const { recipientSearch, state } = useSendFlowData(); const [refreshCounter, setRefreshCounter] = useState(0); const mainAccount = getMainAccount(account, parentAccount); @@ -47,6 +47,13 @@ export function useRecipientAddressModalViewModel({ currentAccountId: mainAccount.id, }); + const recipientSearchState = useRecipientSearchState({ + searchValue: recipientSearch.value, + result, + isLoading, + recipientSupportsDomain, + }); + const allAccounts = useSelector(accountsSelector); const userAccountsForCurrency = useMemo(() => { const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); @@ -60,9 +67,8 @@ export function useRecipientAddressModalViewModel({ }, [allAccounts, currency, mainAccount.id]); const recentAddresses = useMemo(() => { - const addressesWithMetadata = getRecentAddressesStore().getAddresses( - currency.id, - ) as unknown as RecentAddressFromStore[]; + const raw = getRecentAddressesStore().getAddresses(currency.id); + const addressesWithMetadata = Array.isArray(raw) ? raw : []; const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); const userAccountsByAddress = new Map( @@ -71,29 +77,40 @@ export function useRecipientAddressModalViewModel({ return addressesWithMetadata .filter(entry => { - if (!entry?.address) return false; + if (!isRecord(entry)) return false; + const address = entry.address; + if (typeof address !== "string" || address.length === 0) return false; if ( selfTransferPolicy === "impossible" && - entry.address.toLowerCase() === mainAccount.freshAddress.toLowerCase() + address.toLowerCase() === mainAccount.freshAddress.toLowerCase() ) { return false; } return true; }) .map(entry => { - const matchedAccount = userAccountsByAddress.get(entry.address.toLowerCase()); - const lastUsedTimestamp = normalizeLastUsedTimestamp(entry.lastUsed); + if (!isRecord(entry) || typeof entry.address !== "string") { + // Should never happen due to filter above + return null; + } + + const address = entry.address; + const ensName = typeof entry.ensName === "string" ? entry.ensName : undefined; + const lastUsed = typeof entry.lastUsed === "number" ? entry.lastUsed : undefined; + const lastUsedTimestamp = normalizeLastUsedTimestamp(lastUsed); + const matchedAccount = userAccountsByAddress.get(address.toLowerCase()); const recentAddress: RecentAddress = { - address: entry.address, + address, currency, lastUsedAt: new Date(lastUsedTimestamp), - name: entry.address, - ensName: entry.ensName, + name: address, + ensName, isLedgerAccount: !!matchedAccount, accountId: matchedAccount?.id, }; return recentAddress; - }); + }) + .filter((value): value is RecentAddress => value !== null); // refreshKey is used to force recalculation when addresses are removed from the store // even though it's not directly used in the computation // eslint-disable-next-line react-hooks/exhaustive-deps @@ -106,23 +123,48 @@ export function useRecipientAddressModalViewModel({ const hasUserAccounts = userAccountsForCurrency.length > 0; const showInitialEmptyState = showInitialState && !hasRecentAddresses && !hasUserAccounts; + const hasMemo = sendFeatures.hasMemo(currency); + const memoType = sendFeatures.getMemoType(currency); + const memoTypeOptions = sendFeatures.getMemoOptions(currency); + const memoDefaultOption = sendFeatures.getMemoDefaultOption(currency); + const memoMaxLength = sendFeatures.getMemoMaxLength(currency); + + const hasMemoValidationError = useMemo(() => { + if (!hasMemo) return false; + return Boolean(state.transaction.status.errors.transaction); + }, [hasMemo, state.transaction.status.errors.transaction]); + + const hasFilledMemo = useMemo(() => { + if (!hasMemo) return true; + const memo = state.recipient?.memo; + if (!memo) return false; + if (memo.type === "NO_MEMO") return true; + return memo.value.length > 0; + }, [hasMemo, state.recipient?.memo]); + const handleRecentAddressSelect = useCallback( (address: RecentAddress) => { - onAddressSelected(address.address, address.ensName); + if (hasMemo) { + recipientSearch.setValue(address.ensName ?? address.address); + } + onAddressSelected(address.address, address.ensName, !hasMemo); }, - [onAddressSelected], + [hasMemo, onAddressSelected, recipientSearch], ); const handleAccountSelect = useCallback( (selectedAccount: Account) => { - onAddressSelected(selectedAccount.freshAddress); + if (hasMemo) { + recipientSearch.setValue(selectedAccount.freshAddress); + } + onAddressSelected(selectedAccount.freshAddress, undefined, !hasMemo); }, - [onAddressSelected], + [hasMemo, onAddressSelected, recipientSearch], ); const handleAddressSelect = useCallback( (address: string, ensName?: string) => { - onAddressSelected(address, ensName); + onAddressSelected(address, ensName, true); }, [onAddressSelected], ); @@ -135,13 +177,6 @@ export function useRecipientAddressModalViewModel({ [currency], ); - const searchState = useRecipientSearchState({ - searchValue: recipientSearch.value, - result, - isLoading, - recipientSupportsDomain, - }); - return { searchValue: recipientSearch.value, isLoading, @@ -150,10 +185,17 @@ export function useRecipientAddressModalViewModel({ mainAccount, showInitialState, showInitialEmptyState, + ...recipientSearchState, handleRecentAddressSelect, handleAccountSelect, handleAddressSelect, handleRemoveAddress, - ...searchState, + hasMemo, + hasMemoValidationError, + hasFilledMemo, + memoType, + memoTypeOptions, + memoDefaultOption, + memoMaxLength, }; } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts new file mode 100644 index 00000000000..4d156b79a48 --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts @@ -0,0 +1,125 @@ +import { useCallback, useMemo, useRef, useState } from "react"; +import type { Memo } from "../../../types"; + +export type SkipMemoState = "propose" | "toConfirm"; + +type UseRecipientMemoProps = Readonly<{ + hasMemo: boolean; + memoDefaultOption?: string; + memoType?: string; + memoTypeOptions?: readonly string[]; + onMemoChange: (memo: Memo) => void; + onMemoSkip: () => void; + resetKey: string; +}>; + +type UseRecipientMemoResult = Readonly<{ + memo: Memo; + hasMemoTypeOptions: boolean; + showMemoValueInput: boolean; + showSkipMemo: boolean; + skipMemoState: SkipMemoState; + hasFilledMemo: boolean; + onMemoValueChange: (value: string) => void; + onMemoTypeChange: (type: string) => void; + onSkipMemoRequestConfirm: () => void; + onSkipMemoCancelConfirm: () => void; + onSkipMemoConfirm: () => void; +}>; + +function buildDefaultMemo(memoDefaultOption?: string): Memo { + return { value: "", type: memoDefaultOption }; +} + +export function useRecipientMemo({ + hasMemo, + memoDefaultOption, + memoType, + memoTypeOptions, + onMemoChange, + onMemoSkip, + resetKey, +}: UseRecipientMemoProps): UseRecipientMemoResult { + const [memo, setMemo] = useState(() => buildDefaultMemo(memoDefaultOption)); + const [skipMemoState, setSkipMemoState] = useState("propose"); + + const lastResetKeyRef = useRef(""); + if (lastResetKeyRef.current !== resetKey) { + lastResetKeyRef.current = resetKey; + const nextDefaultMemo = buildDefaultMemo(memoDefaultOption); + + if (memo.value !== nextDefaultMemo.value || memo.type !== nextDefaultMemo.type) { + setMemo(nextDefaultMemo); + onMemoChange(nextDefaultMemo); + } + + if (skipMemoState !== "propose") { + setSkipMemoState("propose"); + } + } + + const hasMemoTypeOptions = Boolean(memoType === "typed" && memoTypeOptions?.length); + + const showMemoValueInput = memo.type !== "NO_MEMO"; + + const showSkipMemo = useMemo((): boolean => { + if (!hasMemo) return false; + const noMemoWithoutType = !memo.type && memo.value.length === 0; + const noMemoWithType = Boolean(memo.type && memo.type !== "NO_MEMO" && memo.value.length === 0); + return Boolean(noMemoWithoutType || noMemoWithType); + }, [hasMemo, memo.type, memo.value.length]); + + const hasFilledMemo = useMemo(() => { + if (!hasMemo) return true; + return memo.value.length > 0 || memo.type === "NO_MEMO"; + }, [hasMemo, memo.type, memo.value.length]); + + const onMemoValueChange = useCallback( + (value: string) => { + setMemo(prev => { + const next = { ...prev, value }; + onMemoChange(next); + return next; + }); + }, + [onMemoChange], + ); + + const onMemoTypeChange = useCallback( + (type: string) => { + const next: Memo = { value: "", type }; + setMemo(next); + onMemoChange(next); + }, + [onMemoChange], + ); + + const onSkipMemoRequestConfirm = useCallback(() => { + setSkipMemoState("toConfirm"); + }, []); + + const onSkipMemoCancelConfirm = useCallback(() => { + setSkipMemoState("propose"); + }, []); + + const onSkipMemoConfirm = useCallback(() => { + const next: Memo = { value: "", type: "NO_MEMO" }; + setMemo(next); + onMemoChange(next); + onMemoSkip(); + }, [onMemoChange, onMemoSkip]); + + return { + memo, + hasMemoTypeOptions, + showMemoValueInput, + showSkipMemo, + skipMemoState, + hasFilledMemo, + onMemoValueChange, + onMemoTypeChange, + onSkipMemoRequestConfirm, + onSkipMemoCancelConfirm, + onSkipMemoConfirm, + }; +} diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/types.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/types.ts index bb59c3e7c52..02317a64cc0 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/types.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/types.ts @@ -36,6 +36,7 @@ export type MatchedAccount = Readonly<{ export type BridgeValidationErrors = { recipient?: Error; sender?: Error; + transaction?: Error; }; export type BridgeValidationWarnings = Record; diff --git a/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx b/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx deleted file mode 100644 index 3a241f01731..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/FlowWizard/FlowWizardOrchestrator.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useMemo, type ComponentType, type ReactNode, type CSSProperties } from "react"; -import { useFlowWizardNavigation } from "./hooks/useFlowWizardNavigation"; -import type { - FlowStep, - FlowConfig, - StepRegistry, - StepRenderer, - FlowNavigationDirection, - AnimationConfig, - FlowWizardContextValue, - FlowStepConfig, -} from "./types"; - -const DEFAULT_ANIMATION_CONFIG: AnimationConfig = { - forward: "animate-fade-in", - backward: "animate-fade-out", -}; - -type FlowWizardOrchestratorProps< - TStep extends FlowStep, - TContextValue, - TStepConfig extends FlowStepConfig = FlowStepConfig, -> = Readonly<{ - flowConfig: FlowConfig; - stepRegistry: StepRegistry; - contextValue: TContextValue; - ContextProvider: ComponentType<{ - value: FlowWizardContextValue; - children: ReactNode; - }>; - animationConfig?: AnimationConfig; - getContainerStyle?: (stepConfig: TStepConfig) => CSSProperties | undefined; - children?: ReactNode; -}>; - -// Returns the transition class for the current direction; keeps UI concerns isolated here. -function getAnimationClass( - direction: FlowNavigationDirection, - config: AnimationConfig, -): string | undefined { - return direction === "FORWARD" ? config.forward : config.backward; -} - -/** - * FlowWizardOrchestrator - * - * Generic runner for multi-step flows: - * - drives navigation (forward/back/jump) via useFlowWizardNavigation - * - injects navigation & step metadata into the provided ContextProvider - * - renders the current step with optional enter animations - * - remains UI-agnostic: only needs a step registry and a flow config - */ -export function FlowWizardOrchestrator< - TStep extends FlowStep, - TContextValue, - TStepConfig extends FlowStepConfig = FlowStepConfig, ->({ - flowConfig, - stepRegistry, - contextValue, - ContextProvider, - animationConfig = DEFAULT_ANIMATION_CONFIG, - getContainerStyle, - children, -}: FlowWizardOrchestratorProps) { - const { state, actions, currentStepConfig } = useFlowWizardNavigation({ - flowConfig, - }); - - const enhancedContextValue = useMemo>( - () => ({ - ...contextValue, - navigation: actions, - currentStep: state.currentStep, - direction: state.direction, - currentStepConfig, - }), - [contextValue, actions, state.currentStep, state.direction, currentStepConfig], - ); - - const StepComponent = useMemo(() => { - const renderer = stepRegistry[state.currentStep]; - return renderer ?? null; - }, [state.currentStep, stepRegistry]); - - const hasNavigated = state.stepHistory.length > 0 || state.direction === "BACKWARD"; - const animationClass = hasNavigated - ? getAnimationClass(state.direction, animationConfig) - : undefined; - - const containerStyle = getContainerStyle ? getContainerStyle(currentStepConfig) : undefined; - return ( - -
- {children} - {StepComponent && ( -
- -
- )} -
-
- ); -} - -// Need to use it to create the step registry typesafe -export function createStepRegistry( - registry: StepRegistry, -): StepRegistry { - return registry; -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx deleted file mode 100644 index cdeeb9e5787..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowOrchestrator.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React, { useMemo, type ReactNode } from "react"; -import { FlowWizardOrchestrator } from "../FlowWizard/FlowWizardOrchestrator"; -import type { StepRegistry, AnimationConfig, FlowWizardContextValue } from "../FlowWizard/types"; -import { SendFlowProvider } from "./context/SendFlowContext"; -import { useSendFlowBusinessLogic } from "./hooks/useSendFlowState"; -import { SEND_FLOW_CONFIG } from "./constants"; -import { SEND_FLOW_STEP } from "./types"; -import type { - SendFlowStep, - SendFlowInitParams, - SendFlowBusinessContext, - SendStepConfig, -} from "./types"; - -type SendFlowStepRegistry = StepRegistry; -type SendFlowWizardContext = FlowWizardContextValue< - SendFlowStep, - SendFlowBusinessContext, - SendStepConfig ->; - -type SendFlowOrchestratorProps = Readonly<{ - initParams?: SendFlowInitParams; - onClose: () => void; - stepRegistry: SendFlowStepRegistry; - animationConfig?: AnimationConfig; - children?: ReactNode; -}>; - -type SendFlowProviderWrapperProps = Readonly<{ - value: SendFlowWizardContext; - children: ReactNode; -}>; - -// Adapter that injects the Send context into the generic FlowWizard orchestrator -function SendFlowProviderWrapper({ value, children }: SendFlowProviderWrapperProps) { - return {children}; -} - -export function SendFlowOrchestrator({ - initParams, - onClose, - stepRegistry, - animationConfig, - children, -}: SendFlowOrchestratorProps) { - const skipAccountSelection = Boolean(initParams?.account) || Boolean(initParams?.fromMAD); - const businessContext = useSendFlowBusinessLogic({ initParams, onClose }); - const flowConfig = useMemo( - () => ({ - ...SEND_FLOW_CONFIG, - initialStep: skipAccountSelection - ? SEND_FLOW_STEP.RECIPIENT - : SEND_FLOW_STEP.ACCOUNT_SELECTION, - }), - [skipAccountSelection], - ); - - // Bridge the generic wizard runner with Send-specific business state and config - return ( - - flowConfig={flowConfig} - stepRegistry={stepRegistry} - contextValue={businessContext} - ContextProvider={SendFlowProviderWrapper} - animationConfig={animationConfig} - getContainerStyle={stepConfig => - stepConfig.sizeDialog ? { height: `${stepConfig.sizeDialog}px` } : { height: "612px" } - } - > - {children} - - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx deleted file mode 100644 index bed4011ff98..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/SendFlowRoot.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useCallback } from "react"; -import { createPortal } from "react-dom"; -import { Dialog, DialogContent, DialogBody } from "@ledgerhq/lumen-ui-react"; -import { DomainServiceProvider } from "@ledgerhq/domain-service/hooks/index"; -import { useDispatch, useSelector } from "react-redux"; -import { SendWorkflow } from "."; -import { closeSendFlowDialog, sendFlowStateSelector } from "~/renderer/reducers/sendFlow"; -import { setMemoTagInfoBoxDisplay } from "~/renderer/actions/UI"; -import Snow, { isSnowTime } from "~/renderer/extra/Snow"; - -export function SendFlowRoot() { - const dispatch = useDispatch(); - const { isOpen, data } = useSelector(sendFlowStateSelector); - - const handleClose = useCallback(() => { - dispatch( - setMemoTagInfoBoxDisplay({ - isMemoTagBoxVisible: false, - forceAutoFocusOnMemoField: false, - }), - ); - data?.onClose?.(); - dispatch(closeSendFlowDialog()); - }, [data, dispatch]); - - const handleDialogOpenChange = useCallback( - (open: boolean) => { - if (!open) { - handleClose(); - } - }, - [handleClose], - ); - - if (!isOpen) return null; - - return ( - <> - {isSnowTime() && isOpen - ? createPortal( -
- -
, - document.body, - ) - : null} - - - - - - - - - - - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts b/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts deleted file mode 100644 index 78153f7552d..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/hooks/useSendFlowTransaction.ts +++ /dev/null @@ -1,114 +0,0 @@ -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 type { Transaction } from "@ledgerhq/live-common/generated/types"; -import type { SendFlowTransactionState, SendFlowTransactionActions, RecipientData } from "../types"; - -type UseSendFlowTransactionParams = Readonly<{ - account: AccountLike | null; - parentAccount: Account | null; -}>; - -type UseSendFlowTransactionResult = Readonly<{ - state: SendFlowTransactionState; - actions: SendFlowTransactionActions; -}>; - -export function useSendFlowTransaction({ - account, - parentAccount, -}: UseSendFlowTransactionParams): UseSendFlowTransactionResult { - const { - transaction, - setTransaction: bridgeSetTransaction, - updateTransaction: bridgeUpdateTransaction, - status, - bridgeError, - bridgePending, - setAccount, - } = useBridgeTransaction(() => { - if (!account) return {}; - return { account, parentAccount: parentAccount ?? undefined }; - }); - - const setTransaction = useCallback( - (tx: Transaction) => bridgeSetTransaction(tx), - [bridgeSetTransaction], - ); - - const updateTransaction = useCallback( - (updater: (tx: Transaction) => Transaction) => bridgeUpdateTransaction(updater), - [bridgeUpdateTransaction], - ); - - const setRecipient = useCallback( - (recipient: RecipientData) => { - if (!account || !transaction) return; - - const bridge = getAccountBridge(account, parentAccount); - const updates: Partial = {}; - - if (recipient !== undefined) { - Object.assign(updates, { recipient: recipient.address }); - } - - if (recipient.memo !== undefined) { - Object.assign( - updates, - applyMemoToTransaction( - transaction.family, - recipient.memo.value, - recipient.memo.type, - transaction, - ), - ); - } - - if (recipient.destinationTag !== undefined) { - const parsedTag = Number(recipient.destinationTag.trim()); - if (Number.isFinite(parsedTag)) { - Object.assign( - updates, - applyMemoToTransaction(transaction.family, parsedTag, undefined, transaction), - ); - } - } - - if (Object.keys(updates).length > 0) { - bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); - } - }, - [account, parentAccount, transaction, bridgeSetTransaction], - ); - - const setAccountForTransaction = useCallback( - (newAccount: AccountLike, newParentAccount?: Account | null) => { - setAccount(newAccount, newParentAccount ?? undefined); - }, - [setAccount], - ); - - const state: SendFlowTransactionState = useMemo( - () => ({ - transaction: transaction ?? null, - status, - bridgeError: bridgeError ?? null, - bridgePending, - }), - [transaction, status, bridgeError, bridgePending], - ); - - const actions: SendFlowTransactionActions = useMemo( - () => ({ - setTransaction, - updateTransaction, - setRecipient, - setAccount: setAccountForTransaction, - }), - [setTransaction, updateTransaction, setRecipient, setAccountForTransaction], - ); - - return { state, actions }; -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx deleted file mode 100644 index 6e09fe54e48..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/RecipientScreen.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { useSendFlowContext } from "../../context/SendFlowContext"; -import { RecipientAddressModal } from "./components/RecipientAddressModal"; -import { Memo } from "../../types"; - -export function RecipientScreen() { - const { state, transaction, navigation, close, uiConfig } = useSendFlowContext(); - - const account = state.account.account; - const parentAccount = state.account.parentAccount ?? undefined; - - const currency: CryptoCurrency | TokenCurrency | null = useMemo(() => { - if (state.account.currency) return state.account.currency; - return account ? getAccountCurrency(account) : null; - }, [state.account.currency, account]); - - const handleAddressSelected = useCallback( - (address: string, ensName?: string, goToNextStep?: boolean) => { - transaction.setRecipient({ - address, - ensName, - }); - - if (goToNextStep) { - navigation.goToNextStep(); - } - }, - [transaction, navigation], - ); - - const handleMemoChange = useCallback( - (memo: Memo) => { - transaction.setRecipient({ - memo, - }); - }, - [transaction], - ); - - const handleMemoSkip = useCallback(() => { - navigation.goToNextStep(); - }, [navigation]); - - if (!account || !currency) { - return null; - // Account selection step should guarantee both account and currency are defined - } - - return ( - - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx deleted file mode 100644 index bb600d29d52..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/MyAccountsSectionView.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Subheader } from "@ledgerhq/lumen-ui-react"; -import type { Account } from "@ledgerhq/types-live"; -import { AccountRowWithBalance } from "./AccountRowWithBalance"; - -type MyAccountsSectionViewProps = Readonly<{ - userAccountsForCurrency: Account[]; - accountNames: (string | undefined)[]; - onSelect: (account: Account) => void; -}>; - -export function MyAccountsSectionView({ - userAccountsForCurrency, - accountNames, - onSelect, -}: MyAccountsSectionViewProps) { - const { t } = useTranslation(); - - if (userAccountsForCurrency.length === 0) { - return null; - } - - return ( -
- -
- {userAccountsForCurrency.map((account, index) => ( - onSelect(account)} - /> - ))} -
-
- ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx deleted file mode 100644 index ad92eb80bd1..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModal.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { RecipientAddressModalView } from "./RecipientAddressModalView"; -import { useRecipientAddressModalViewModel } from "../hooks/useRecipientAddressModalViewModel"; -import { Memo } from "../../../types"; - -type RecipientAddressModalProps = Readonly<{ - isOpen: boolean; - onClose: () => void; - account: AccountLike; - parentAccount?: Account; - currency: CryptoCurrency | TokenCurrency; - onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; - recipientSupportsDomain: boolean; - onMemoChange: (memo: Memo) => void; - onMemoSkip: () => void; -}>; - -export function RecipientAddressModal({ - account, - parentAccount, - currency, - onAddressSelected, - recipientSupportsDomain = false, - onMemoChange, - onMemoSkip, -}: RecipientAddressModalProps) { - const viewModel = useRecipientAddressModalViewModel({ - account, - parentAccount, - currency, - onAddressSelected, - recipientSupportsDomain, - }); - - return ( - - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx deleted file mode 100644 index 17942ed443c..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx +++ /dev/null @@ -1,342 +0,0 @@ -import React, { useCallback, useMemo, useState } from "react"; -import { - AddressInput, - Banner, - Button, - Checkbox, - Link, - Select, - SelectContent, - SelectItem, - SelectItemText, - SelectTrigger, - TextInput, - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@ledgerhq/lumen-ui-react"; -import { useTranslation } from "react-i18next"; -import type { Account } from "@ledgerhq/types-live"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import type { AddressSearchResult, AddressValidationError, RecentAddress } from "../types"; -import { AddressMatchedSection } from "./AddressMatchedSection"; -import EmptyList from "./EmptyList"; -import { LoadingState } from "./LoadingState"; -import { MyAccountsSection } from "./MyAccountsSection"; -import { RecentAddressesSection } from "./RecentAddressesSection"; -import { ValidationBanner } from "./ValidationBanner"; -import { Information } from "@ledgerhq/lumen-ui-react/symbols"; -import { Memo } from "../../../types"; - -type RecipientAddressModalViewProps = Readonly<{ - searchValue: string; - isLoading: boolean; - result: AddressSearchResult; - recentAddresses: RecentAddress[]; - mainAccount: Account; - currency: CryptoCurrency | TokenCurrency; - recipientSupportsDomain: boolean; - showInitialState: boolean; - showInitialEmptyState: boolean; - showMatchedAddress: boolean; - showAddressValidationError: boolean; - showEmptyState: boolean; - showBridgeSenderError: boolean; - showSanctionedBanner: boolean; - showBridgeRecipientError: boolean; - showBridgeRecipientWarning: boolean; - isSanctioned: boolean; - isAddressComplete: boolean; - addressValidationErrorType: AddressValidationError | null; - bridgeRecipientError: Error | undefined; - bridgeRecipientWarning: Error | undefined; - bridgeSenderError: Error | undefined; - onSearchChange: (e: React.ChangeEvent) => void; - onClearSearch: () => void; - onRecentAddressSelect: (address: RecentAddress) => void; - onAccountSelect: (account: Account) => void; - onAddressSelect: (address: string, ensName?: string) => void; - onRemoveAddress: (address: RecentAddress) => void; - hasMemo: boolean; - memoType?: string; - memoTypeOptions?: readonly string[]; - memoDefaultOption?: string; - memoMaxLength?: number; - onMemoChange: (memo: Memo) => void; - onMemoSkip: () => void; -}>; - -export function RecipientAddressModalView({ - searchValue, - isLoading, - result, - recentAddresses, - mainAccount, - currency, - recipientSupportsDomain, - showInitialState, - showInitialEmptyState, - showMatchedAddress, - showAddressValidationError, - showEmptyState, - showBridgeSenderError, - showSanctionedBanner, - showBridgeRecipientError, - showBridgeRecipientWarning, - isSanctioned, - isAddressComplete, - addressValidationErrorType, - bridgeRecipientError, - bridgeRecipientWarning, - bridgeSenderError, - onSearchChange, - onClearSearch, - onRecentAddressSelect, - onAccountSelect, - onAddressSelect, - onRemoveAddress, - hasMemo, - memoType, - memoTypeOptions, - memoDefaultOption, - memoMaxLength, - onMemoChange, - onMemoSkip, -}: RecipientAddressModalViewProps) { - const { t } = useTranslation(); - - const [memo, setMemo] = useState({ - value: "", - type: memoDefaultOption, - }); - - const onMemoValueChange = useCallback( - (value: string) => { - const newMemo = { ...memo, value }; - setMemo(newMemo); - onMemoChange(newMemo); - }, - [memo, onMemoChange], - ); - - const onMemoTypeChange = useCallback( - (type: string) => { - const newMemo = { value: "", type }; - setMemo(newMemo); - onMemoChange(newMemo); - }, - [onMemoChange], - ); - - const showSkipMemo = useMemo(() => { - const noMemoWithoutType = !memo.type && memo.value.length === 0; - const noMemoWithType = memo.type && memo.type !== "NO_MEMO" && memo.value.length === 0; - return noMemoWithoutType || noMemoWithType; - }, [memo.type, memo.value.length]); - - const [skipMemoState, setSkipMemoState] = useState<"propose" | "toConfirm">("propose"); - - const showMemoValueInput = useMemo(() => { - return memo.type !== "NO_MEMO"; - }, [memo]); - - const hasMemoTypeOptions = useMemo(() => { - return memoType == "typed" && memoTypeOptions; - }, [memoType, memoTypeOptions]); - - const hasFilledMemo = useMemo(() => { - return hasMemo && (memo.value.length > 0 || memo.type === "NO_MEMO"); - }, [hasMemo, memo.type, memo.value.length]); - - return ( - <> -
- - - {showMatchedAddress && hasMemo && ( - <> -
- {hasMemoTypeOptions && ( - - )} - - {showMemoValueInput && ( - onMemoValueChange(e.target.value)} - suffix={ - - - - - - {t("newSendFlow.tagHelp.description", { - currency: currency.id, - memoLabel: t(["families." + currency.id + ".memo", "common.memo"]), - })} - - - } - className="mt-12 w-full" - value={memo.value} - maxLength={memoMaxLength} - errorMessage={ - result.bridgeErrors?.transaction - ? t("errors." + result.bridgeErrors?.transaction.name + ".title") - : undefined - } - /> - )} -
- - {showSkipMemo && ( - <> -
- {skipMemoState === "propose" && ( -
- - {t("newSendFlow.skipMemo.notRequired", { - memoLabel: t(["families." + currency.id + ".memo", "common.memo"]), - })} -   - - setSkipMemoState("toConfirm")} - > - {t("common.skip")} - -
- )} - - {skipMemoState === "toConfirm" && ( -
- - {t("newSendFlow.skipMemo.confirm")} - - } - /> -
- - {t("newSendFlow.skipMemo.neverAskAgain")} -
-
- )} -
- - )} - - )} -
- -
- {isLoading && ( -
- -
- )} - - {showInitialState && ( - <> - - - - )} - - {showMatchedAddress && (!hasMemo || hasFilledMemo) && ( - - )} - - {showAddressValidationError && ( -
- -
- )} - - {(showEmptyState || showInitialEmptyState) && ( -
- -
- )} - - {(showBridgeSenderError || - showSanctionedBanner || - showBridgeRecipientError || - showBridgeRecipientWarning) && ( -
- {showBridgeSenderError && ( - - )} - {showSanctionedBanner && } - {showBridgeRecipientError && ( - - )} - {showBridgeRecipientWarning && ( - - )} -
- )} -
- - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts deleted file mode 100644 index 475a4e66888..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { useState, useMemo, useCallback, useRef } from "react"; -import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import { getMainAccount } from "@ledgerhq/live-common/account/index"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; -import type { TransactionStatus } from "@ledgerhq/live-common/generated/types"; -import type { BridgeValidationErrors, BridgeValidationWarnings } from "../types"; -import { applyMemoToTransaction } from "@ledgerhq/live-common/bridge/descriptor"; -import { Memo } from "../../../types"; - -export type BridgeRecipientValidationResult = { - errors: BridgeValidationErrors; - warnings: BridgeValidationWarnings; - isLoading: boolean; - status: TransactionStatus | null; -}; - -type UseBridgeRecipientValidationProps = { - recipient: string; - account: AccountLike | null; - parentAccount?: Account | null; - memo?: Memo; - enabled?: boolean; -}; - -const DEBOUNCE_DELAY = 300; - -/** - * Hook to validate recipient address using the bridge transaction status. - * This hook leverages the existing bridge infrastructure to get - * recipient and sender validation errors/warnings. - */ -export function useBridgeRecipientValidation({ - recipient, - account, - parentAccount, - memo, - enabled = true, -}: UseBridgeRecipientValidationProps): BridgeRecipientValidationResult { - const [validationState, setValidationState] = useState<{ - errors: BridgeValidationErrors; - warnings: BridgeValidationWarnings; - isLoading: boolean; - status: TransactionStatus | null; - }>({ - errors: {}, - warnings: {}, - isLoading: false, - status: null, - }); - - const lastRecipientRef = useRef(""); - const validationTriggeredRef = useRef(false); - const debounceTimeoutRef = useRef(null); - const abortControllerRef = useRef(null); - - const validateRecipient = useCallback(async () => { - if (!account || !recipient || !enabled) { - setValidationState({ - errors: {}, - warnings: {}, - isLoading: false, - status: null, - }); - return; - } - - // Cancel any pending validation - if (abortControllerRef.current) { - abortControllerRef.current.abort(); - } - abortControllerRef.current = new AbortController(); - const signal = abortControllerRef.current.signal; - - setValidationState(prev => ({ ...prev, isLoading: true })); - - try { - const mainAccount = getMainAccount(account, parentAccount); - const bridge = getAccountBridge(account, parentAccount); - - // Create a transaction with the recipient to validate - let transaction = bridge.createTransaction(mainAccount); - transaction = bridge.updateTransaction(transaction, { recipient }); - - if (memo) { - applyMemoToTransaction(transaction.family, memo.value, memo.type, transaction); - } - - // Prepare the transaction (resolves ENS, validates format, etc.) - const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); - - if (signal.aborted) return; - - // Get the transaction status which contains errors and warnings - const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); - - if (signal.aborted) return; - - // Extract recipient and sender specific errors/warnings - const errors: BridgeValidationErrors = {}; - const warnings: BridgeValidationWarnings = {}; - - if (status.errors.recipient) { - errors.recipient = status.errors.recipient; - } - if (status.errors.sender) { - errors.sender = status.errors.sender; - } - if (status.errors.transaction) { - errors.transaction = status.errors.transaction; - } - - // Copy all warnings (there can be family-specific warnings) - Object.entries(status.warnings).forEach(([key, value]) => { - if (value) { - warnings[key] = value; - } - }); - - setValidationState({ - errors, - warnings, - isLoading: false, - status, - }); - } catch (error) { - if (signal.aborted) return; - - // On error, we don't block the user but log the issue - console.error("Bridge recipient validation failed:", error); - setValidationState({ - errors: {}, - warnings: {}, - isLoading: false, - status: null, - }); - } - }, [account, recipient, enabled, parentAccount, memo]); - - // Track recipient changes and trigger validation - if (recipient !== lastRecipientRef.current) { - lastRecipientRef.current = recipient; - validationTriggeredRef.current = false; - - // Clear any pending debounce - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - debounceTimeoutRef.current = null; - } - - // Reset state immediately when recipient is cleared - if (!recipient) { - setValidationState({ - errors: {}, - warnings: {}, - isLoading: false, - status: null, - }); - } - } - - // Debounced validation trigger - if (recipient && !validationTriggeredRef.current && enabled) { - validationTriggeredRef.current = true; - - // Clear any existing timeout - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - } - - // Set loading state immediately for better UX - setValidationState(prev => ({ ...prev, isLoading: true })); - - // Debounce the actual validation - debounceTimeoutRef.current = setTimeout(() => { - validateRecipient(); - }, DEBOUNCE_DELAY); - } - - return useMemo( - () => ({ - errors: validationState.errors, - warnings: validationState.warnings, - isLoading: validationState.isLoading, - status: validationState.status, - }), - [validationState], - ); -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts deleted file mode 100644 index cd3c46b07b2..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { useState, useCallback, useMemo } from "react"; -import { useSelector } from "react-redux"; -import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors"; -import { - getAccountCurrency, - getMainAccount, - getRecentAddressesStore, -} from "@ledgerhq/live-common/account/index"; -import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { accountsSelector } from "~/renderer/reducers/accounts"; -import type { RecentAddress } from "../types"; -import { useAddressValidation } from "./useAddressValidation"; - -type UseRecipientAddressModalViewModelProps = Readonly<{ - account: AccountLike; - parentAccount?: Account; - currency: CryptoCurrency | TokenCurrency; - onAddressSelected: (address: string, ensName?: string, goToNextStep?: boolean) => void; - recipientSupportsDomain: boolean; -}>; - -export function useRecipientAddressModalViewModel({ - account, - parentAccount, - currency, - onAddressSelected, - recipientSupportsDomain, -}: UseRecipientAddressModalViewModelProps) { - const [searchValue, setSearchValue] = useState(""); - const [refreshKey, setRefreshKey] = useState(0); - - const mainAccount = getMainAccount(account, parentAccount); - - const { result, isLoading } = useAddressValidation({ - searchValue, - currency, - account, - parentAccount, - currentAccountId: mainAccount.id, - }); - - const allAccounts = useSelector(accountsSelector); - const userAccountsForCurrency = useMemo(() => { - return allAccounts.filter(acc => { - if (acc.id === mainAccount.id) return false; - const accCurrency = getAccountCurrency(acc); - return accCurrency.id === currency.id; - }); - }, [allAccounts, currency, mainAccount.id]); - - const recentAddresses = useMemo(() => { - void refreshKey; - const addressesWithMetadata = getRecentAddressesStore().getAddressesWithMetadata(currency.id); - const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); - - const userAccountsByAddress = new Map( - userAccountsForCurrency.map(acc => [acc.freshAddress.toLowerCase(), acc]), - ); - - return addressesWithMetadata - .filter(entry => { - if (!entry?.address) return false; - if ( - selfTransferPolicy === "impossible" && - entry.address.toLowerCase() === mainAccount.freshAddress.toLowerCase() - ) { - return false; - } - return true; - }) - .map(entry => { - const matchedAccount = userAccountsByAddress.get(entry.address.toLowerCase()); - return { - address: entry.address, - currency, - lastUsedAt: new Date(entry.lastUsed), - name: entry.address, - isLedgerAccount: !!matchedAccount, - accountId: matchedAccount?.id, - } as RecentAddress; - }); - }, [currency, refreshKey, mainAccount.freshAddress, userAccountsForCurrency]); - - const hasSearchValue = searchValue.length > 0; - const showInitialState = !hasSearchValue; - - const hasRecentAddresses = recentAddresses.length > 0; - const hasUserAccounts = userAccountsForCurrency.length > 0; - const showInitialEmptyState = showInitialState && !hasRecentAddresses && !hasUserAccounts; - - const hasMemo = sendFeatures.hasMemo(currency); - const memoType = sendFeatures.getMemoType(currency); - const memoTypeOptions = sendFeatures.getMemoOptions(currency); - const memoDefaultOption = sendFeatures.getMemoDefaultOption(currency); - const memoMaxLength = sendFeatures.getMemoMaxLength(currency); - - const handleSearchChange = useCallback((e: React.ChangeEvent) => { - setSearchValue(e.target.value); - }, []); - - const handleClearSearch = useCallback(() => { - setSearchValue(""); - }, []); - - const handleRecentAddressSelect = useCallback( - (address: RecentAddress) => { - setSearchValue(address.ensName ?? address.address); - onAddressSelected(address.address, address.ensName, !hasMemo); - }, - [hasMemo, onAddressSelected], - ); - - const handleAccountSelect = useCallback( - (selectedAccount: Account) => { - setSearchValue(selectedAccount.freshAddress); - onAddressSelected(selectedAccount.freshAddress, undefined, !hasMemo); - }, - [hasMemo, onAddressSelected], - ); - - const handleAddressSelect = useCallback( - (address: string, ensName?: string) => { - setSearchValue(ensName ?? address); - onAddressSelected(address, ensName, !hasMemo); - }, - [hasMemo, onAddressSelected], - ); - - const handleRemoveAddress = useCallback( - (address: RecentAddress) => { - getRecentAddressesStore().removeAddress(currency.id, address.address); - setRefreshKey(prev => prev + 1); - }, - [currency], - ); - - const showSearchResults = hasSearchValue && !isLoading; - const isSanctioned = result.status === "sanctioned"; - - const isAddressComplete = useMemo(() => { - return ( - result.status === "valid" || - result.status === "ens_resolved" || - result.status === "sanctioned" - ); - }, [result.status]); - - const hasAnyMatches = - (result.matchedAccounts && result.matchedAccounts.length > 0) || - !!result.matchedRecentAddress || - !!result.ensName || - result.isLedgerAccount || - isSanctioned; - - const showSanctionedBanner = isSanctioned && hasSearchValue; - - const bridgeRecipientError = result.bridgeErrors?.recipient; - const bridgeRecipientWarning = result.bridgeWarnings?.recipient; - const bridgeSenderError = result.bridgeErrors?.sender; - - const isSelfTransferError = - bridgeRecipientError instanceof InvalidAddressBecauseDestinationIsAlsoSource; - const isBridgeInvalidAddress = - bridgeRecipientError instanceof InvalidAddress && !isSelfTransferError; - - const showMatchedAddress = - showSearchResults && - (hasAnyMatches || - (result.status === "valid" && - !result.error && - !bridgeRecipientError && - !isBridgeInvalidAddress)) && - (result.status === "valid" || - (recipientSupportsDomain && result.status === "ens_resolved") || - result.isLedgerAccount || - !!result.matchedRecentAddress || - isSanctioned); - - const showAddressValidationError = - showSearchResults && - !showSanctionedBanner && - !hasAnyMatches && - (!!result.error || isBridgeInvalidAddress); - - const addressValidationErrorType = - result.error ?? (isBridgeInvalidAddress ? "incorrect_format" : null); - - const showBridgeRecipientError = - showSearchResults && - !!bridgeRecipientError && - !isBridgeInvalidAddress && - !showSanctionedBanner && - !hasAnyMatches; - const showBridgeRecipientWarning = - showSearchResults && - !!bridgeRecipientWarning && - !showBridgeRecipientError && - !showAddressValidationError; - const showBridgeSenderError = showSearchResults && !!bridgeSenderError; - - const showEmptyState = - showSearchResults && - (!isAddressComplete || !hasAnyMatches) && - !showMatchedAddress && - !showSanctionedBanner && - !showAddressValidationError && - !showBridgeRecipientError; - - return { - searchValue, - isLoading, - result, - recentAddresses, - mainAccount, - showInitialState, - showInitialEmptyState, - showSearchResults, - showMatchedAddress, - showAddressValidationError, - showEmptyState, - showBridgeSenderError, - showSanctionedBanner, - showBridgeRecipientError, - showBridgeRecipientWarning, - isSanctioned, - isAddressComplete, - addressValidationErrorType, - bridgeRecipientError, - bridgeRecipientWarning, - bridgeSenderError, - handleSearchChange, - handleClearSearch, - handleRecentAddressSelect, - handleAccountSelect, - handleAddressSelect, - handleRemoveAddress, - hasMemo, - memoType, - memoTypeOptions, - memoDefaultOption, - memoMaxLength, - }; -} diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts b/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts deleted file mode 100644 index 672d224e15a..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/screens/Recipient/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Account } from "@ledgerhq/types-live"; -import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; - -export type RecentAddress = Readonly<{ - address: string; - currency: CryptoCurrency | TokenCurrency; - lastUsedAt: Date; - name?: string; - ensName?: string; - isLedgerAccount?: boolean; - accountId?: string; -}>; - -export type AddressValidationStatus = - | "idle" - | "loading" - | "valid" - | "invalid" - | "sanctioned" - | "ens_resolved"; - -export type AddressValidationError = - | "incorrect_format" - | "sanctioned" - | "incompatible_asset" - | "wallet_not_exist" - | null; - -export type MatchedAccount = Readonly<{ - account: Account; - accountName: string | undefined; - accountBalance: string | undefined; - accountBalanceFormatted: string | undefined; -}>; - -export type BridgeValidationErrors = { - recipient?: Error; - sender?: Error; - transaction?: Error; -}; - -export type BridgeValidationWarnings = Record; - -export type AddressSearchResult = Readonly<{ - status: AddressValidationStatus; - error: AddressValidationError; - resolvedAddress: string | undefined; - ensName: string | undefined; - isLedgerAccount: boolean; - accountName: string | undefined; - accountBalance: string | undefined; - accountBalanceFormatted: string | undefined; - isFirstInteraction: boolean; - matchedRecentAddress: RecentAddress | undefined; - matchedAccounts: MatchedAccount[]; - bridgeErrors: BridgeValidationErrors | undefined; - bridgeWarnings: BridgeValidationWarnings | undefined; -}>; - -export type AddressListItemProps = { - address: string; - name?: string; - description?: string; - date?: Date; - balance?: string; - balanceFormatted?: string; - onSelect: () => void; - onContextMenu?: (e: React.MouseEvent) => void; - showSendTo?: boolean; - isLedgerAccount?: boolean; - disabled?: boolean; - hideDescription?: boolean; -}; diff --git a/apps/ledger-live-desktop/src/newArch/features/Send/types.ts b/apps/ledger-live-desktop/src/newArch/features/Send/types.ts deleted file mode 100644 index 7e78c82a022..00000000000 --- a/apps/ledger-live-desktop/src/newArch/features/Send/types.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { Account, AccountLike, Operation } from "@ledgerhq/types-live"; -import type { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; -import type { Transaction, TransactionStatus } from "@ledgerhq/live-common/generated/types"; -import type { - FlowNavigationDirection, - FlowNavigationActions, - FlowStepConfig, - FlowConfig, - FlowStatus, - FlowStatusActions, -} from "../FlowWizard/types"; - -export const SEND_FLOW_STEP = { - ACCOUNT_SELECTION: "ACCOUNT_SELECTION", - RECIPIENT: "RECIPIENT", - AMOUNT: "AMOUNT", - SIGNATURE: "SIGNATURE", - CONFIRMATION: "CONFIRMATION", -}; - -export type Memo = { value: string; type?: string }; - -export type SendFlowStep = (typeof SEND_FLOW_STEP)[keyof typeof SEND_FLOW_STEP]; - -export type SendStepConfig = FlowStepConfig & - Readonly<{ - showTitle?: boolean; - sizeDialog?: number; - }>; - -export type SendFlowConfig = FlowConfig; -export type NavigationDirection = FlowNavigationDirection; -export type SendFlowNavigationActions = FlowNavigationActions; - -export type SendFlowUiConfig = Readonly<{ - hasMemo: boolean; - memoType?: string; - memoMaxLength?: number; - memoMaxValue?: number; - memoOptions?: readonly string[]; - recipientSupportsDomain: boolean; - hasFeePresets: boolean; - hasCustomFees: boolean; - hasCoinControl: boolean; -}>; - -export type RecipientData = Readonly<{ - address?: string; - ensName?: string; - memo?: Memo; - destinationTag?: string; -}>; - -export type SendFlowTransactionState = Readonly<{ - transaction: Transaction | null; - status: TransactionStatus; - bridgeError: Error | null; - bridgePending: boolean; -}>; - -export type SendFlowAccountState = Readonly<{ - account: AccountLike | null; - parentAccount: Account | null; - currency: CryptoOrTokenCurrency | null; -}>; - -export type SendFlowOperationResult = Readonly<{ - optimisticOperation: Operation | null; - transactionError: Error | null; - signed: boolean; -}>; - -export type SendFlowState = Readonly<{ - account: SendFlowAccountState; - transaction: SendFlowTransactionState; - recipient: RecipientData | null; - operation: SendFlowOperationResult; - isLoading: boolean; - flowStatus: FlowStatus; -}>; - -export type SendFlowTransactionActions = Readonly<{ - setTransaction: (tx: Transaction) => void; - updateTransaction: (updater: (tx: Transaction) => Transaction) => void; - setRecipient: (recipient: RecipientData) => void; - setAccount: (account: AccountLike, parentAccount?: Account | null) => void; -}>; - -export type SendFlowOperationActions = Readonly<{ - onOperationBroadcasted: (operation: Operation) => void; - onTransactionError: (error: Error) => void; - onSigned: () => void; - onRetry: () => void; -}>; - -export type SendFlowInitParams = Readonly<{ - account?: AccountLike; - parentAccount?: Account; - recipient?: string; - amount?: string; - memo?: string; - fromMAD?: boolean; -}>; - -export type SendFlowBusinessContext = Readonly<{ - state: SendFlowState; - transaction: SendFlowTransactionActions; - operation: SendFlowOperationActions; - status: FlowStatusActions; - uiConfig: SendFlowUiConfig; - close: () => void; - setAccountAndNavigate: (account: AccountLike, parentAccount?: Account) => void; -}>; - -export type SendFlowContextValue = SendFlowBusinessContext & - Readonly<{ - navigation: SendFlowNavigationActions; - currentStep: SendFlowStep; - direction: NavigationDirection; - currentStepConfig: SendStepConfig; - }>; diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 4278e28c15f..99efa936898 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -2349,12 +2349,15 @@ "walletNotExist": "Wallet doesn’t exist" }, "skipMemo": { - "title": "Skip adding {{tag}}?", - "warning": "Your asset may be lost if a {{tag}} is required by your recipient" + "title": "Skip adding {{memoLabel}}?", + "description": "Your asset may be lost if a {{memoLabel}} is required by your recipient", + "notRequired": "{{memoLabel}} not required by recipient?", + "confirm": "Yes skip", + "neverAskAgain": "Don't show this warning again" }, "tagHelp": { - "title": "What’s a {{tag}}?", - "description": "A {{tag}} is required when sending {{currency}} to a crypto exchange. It’s a feature used to identify the recipient of the transaction. Not including the {{tag}} memo may result in a loss of funds." + "inputLabel": "Enter {{memoLabel}}", + "description": "A {{memoLabel}} is required when sending {{currency}} to a crypto exchange. It’s a feature used to identify the recipient of the transaction. Not including the {{memoLabel}} memo may result in a loss of funds." }, "networkFeesInFiat": "Network fees in {{currency}}", "feesAmount": "Fees amount ({{unit}})", diff --git a/libs/ledger-live-common/src/bridge/descriptor.test.ts b/libs/ledger-live-common/src/bridge/descriptor.test.ts index 27f42cf6c6d..3c41bf91e95 100644 --- a/libs/ledger-live-common/src/bridge/descriptor.test.ts +++ b/libs/ledger-live-common/src/bridge/descriptor.test.ts @@ -320,7 +320,7 @@ describe("sendFeatures", () => { it("should get memo default option", () => { const stellar = getCryptoCurrencyById("stellar"); - expect(sendFeatures.getMemoDefaultOption(stellar)).toBe("MEMO_ID"); + expect(sendFeatures.getMemoDefaultOption(stellar)).toBe("MEMO_TEXT"); }); it("should return undefined when memo has no default option", () => { diff --git a/libs/ledger-live-common/src/bridge/descriptor.ts b/libs/ledger-live-common/src/bridge/descriptor.ts index 7f74751e23e..3e8bbfbe5ee 100644 --- a/libs/ledger-live-common/src/bridge/descriptor.ts +++ b/libs/ledger-live-common/src/bridge/descriptor.ts @@ -143,6 +143,7 @@ export type CoinDescriptor = { type MemoApplicationFn = ( memoValue: string | number | undefined, + memoType: string | undefined, currentTransaction: Record, ) => Record; @@ -215,7 +216,7 @@ export function getSendDescriptor( * Helper functions to check send flow capabilities */ const memoApplicationRegistry: Record = { - solana: (memo, transaction) => { + solana: (memo, _type, transaction) => { const currentModel = (transaction.model as Record | undefined) || {}; const currentUiState = (currentModel.uiState as Record | undefined) || {}; return { @@ -234,8 +235,8 @@ const memoApplicationRegistry: Record = { if (typeof memo === "string") return { tag: Number(memo) }; return { tag: undefined }; }, - stellar: memo => ({ memoValue: memo }), - ton: (memo, transaction) => { + stellar: (memo, type) => ({ memoValue: memo, memoType: type }), + ton: (memo, _type, transaction) => { const currentComment = (transaction.comment as Record | undefined) || {}; return { comment: { @@ -249,13 +250,26 @@ const memoApplicationRegistry: Record = { export function applyMemoToTransaction( family: string, memoValue: string | number | undefined, - currentTransaction: Record = {}, + memoTypeOrTransaction?: string | Record | null, + currentTransaction?: Record, ): Record { + const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + + const memoType = + memoTypeOrTransaction === undefined || typeof memoTypeOrTransaction === "string" + ? memoTypeOrTransaction + : undefined; + + const transaction = isRecord(memoTypeOrTransaction) + ? memoTypeOrTransaction + : currentTransaction ?? {}; + const applyFn = memoApplicationRegistry[family]; if (!applyFn) { return { memo: memoValue }; } - return applyFn(memoValue, currentTransaction); + return applyFn(memoValue, memoType, transaction); } export const sendFeatures = { @@ -309,7 +323,7 @@ export const sendFeatures = { const descriptor = getSendDescriptor(currency); return descriptor?.inputs.memo?.options; }, - getMemoDefaultOption: (currency: CryptoOrTokenCurrency | undefined): string | undefined => { + getMemoDefaultOption: (currency: CryptoOrTokenCurrency | undefined) => { const descriptor = getSendDescriptor(currency); return descriptor?.inputs.memo?.defaultOption; }, From 0c8b3d80687505706be9d925139f926734ef0609 Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Wed, 21 Jan 2026 16:48:33 +0100 Subject: [PATCH 3/9] feat: implement settings for dont show again + fix reset memo on navigation back + different fixes --- .changeset/violet-cheetahs-shave.md | 6 ++ .../features/Send/components/SendHeader.tsx | 74 +++++++++---------- .../mvvm/features/Send/components/utils.ts | 6 +- .../screens/Recipient/RecipientScreen.tsx | 3 +- .../components/AddressMatchedSection.tsx | 2 +- .../components/Memo/SkipMemoSection.tsx | 22 +++--- .../useRecipientAddressModalViewModel.test.ts | 33 ++++++--- .../Recipient/hooks/useRecipientMemo.ts | 60 +++++++++++---- .../src/renderer/actions/settings.ts | 16 ++++ .../src/renderer/reducers/settings.ts | 4 + .../Accounts/DoNotAskAgainSkipMemo.tsx | 19 +++++ .../settings/sections/Accounts/index.tsx | 2 + .../static/i18n/en/app.json | 4 + knip.json | 1 + 14 files changed, 176 insertions(+), 76 deletions(-) create mode 100644 .changeset/violet-cheetahs-shave.md create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/DoNotAskAgainSkipMemo.tsx diff --git a/.changeset/violet-cheetahs-shave.md b/.changeset/violet-cheetahs-shave.md new file mode 100644 index 00000000000..45edc808bb3 --- /dev/null +++ b/.changeset/violet-cheetahs-shave.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": minor +"@ledgerhq/live-common": minor +--- + +Feature/live 20724 [LLD][UI] Memo screen diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx index 3ca50469653..0296093730c 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx @@ -75,24 +75,6 @@ export function SendHeader() { const currencyName = state.account.currency?.ticker ?? ""; const availableText = useAvailableBalance(state.account.account); - 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 showBackButton = navigation.canGoBack(); const showTitle = currentStepConfig?.showTitle !== false; @@ -101,25 +83,12 @@ export function SendHeader() { showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : ""; 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]); + }, [isAmountStep, recipientSearch.value, state.recipient]); const showMemoControls = Boolean( showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0, @@ -144,7 +113,7 @@ export function SendHeader() { memoType, memoTypeOptions, onMemoChange: memo => { - transaction.setRecipient({ memo }); + transaction.setRecipient({ ...state.recipient, memo }); }, onMemoSkip: () => { navigation.goToNextStep(); @@ -154,6 +123,37 @@ export function SendHeader() { }`, }); + 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, + })); + + memoViewModel.resetViewState(); + } + navigation.goToPreviousStep(); + } else { + close(); + } + }, [close, currentStep, memoViewModel, navigation, transaction]); + + const handleRecipientInputClick = useCallback(() => { + if (!isAmountStep) return; + + const prefillValue = getRecipientSearchPrefillValue(state.recipient); + if (prefillValue) { + recipientSearch.setValue(prefillValue); + } + + handleBack(); + }, [handleBack, isAmountStep, recipientSearch, state.recipient]); + const transactionErrorName = state.transaction.status?.errors?.transaction?.name; const recipientInputContent = useMemo(() => { @@ -178,8 +178,8 @@ export function SendHeader() { return ( <> recipientSearch.setValue(e.target.value)} onClear={recipientSearch.clear} placeholder={ @@ -233,13 +233,13 @@ export function SendHeader() { t, showMemoControls, currencyId, + memoViewModel.showMemoValueInput, + memoViewModel.showSkipMemo, memoViewModel.hasMemoTypeOptions, memoViewModel.memo.type, memoViewModel.memo.value, memoViewModel.onMemoTypeChange, - memoViewModel.showMemoValueInput, memoViewModel.onMemoValueChange, - memoViewModel.showSkipMemo, memoViewModel.skipMemoState, memoViewModel.onSkipMemoRequestConfirm, memoViewModel.onSkipMemoCancelConfirm, diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/utils.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/components/utils.ts index 719b2f72e80..5d289f2462e 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/utils.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/utils.ts @@ -5,7 +5,7 @@ export type RecipientLike = Readonly<{ 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, { @@ -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; } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx index 1213a58d3d4..42c74273612 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/RecipientScreen.tsx @@ -21,6 +21,7 @@ export function RecipientScreen() { const handleAddressSelected = useCallback( (address: string, ensName?: string, goToNextStep?: boolean) => { transaction.setRecipient({ + ...state.recipient, address, ensName, }); @@ -29,7 +30,7 @@ export function RecipientScreen() { navigation.goToNextStep(); } }, - [transaction, navigation], + [transaction, state.recipient, navigation], ); if (!account || !currency) { diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx index d1078895f66..42a0ff51c06 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx @@ -51,7 +51,7 @@ export function AddressMatchedSection({ return `${ensName} (${formattedAddress})`; }; - const getRecentDescription = (): string => { + const getRecentDescription = (): string | undefined => { if (matchedRecentAddress) { return `Already used · ${formatRelativeDate(matchedRecentAddress.lastUsedAt)}`; } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx index 8ae4bd14357..5ce9b2efbe2 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/Memo/SkipMemoSection.tsx @@ -11,7 +11,7 @@ type SkipMemoSectionProps = Readonly<{ state: SkipMemoState; onRequestConfirm: () => void; onCancelConfirm: () => void; - onConfirm: () => void; + onConfirm: (doNotAskAgain: boolean) => void; }>; function SkipMemoSectionComponent({ @@ -36,14 +36,18 @@ function SkipMemoSectionComponent({ setDoNotAskAgain(prev => !prev); }, []); + const handleOnSkipConfirmed = useCallback(() => { + onConfirm(doNotAskAgain); + }, [onConfirm, doNotAskAgain]); + if (state === "propose") { return (
- + {t("newSendFlow.skipMemo.notRequired", { memoLabel })}   - + {t("common.skip")}
@@ -60,7 +64,7 @@ function SkipMemoSectionComponent({ onClose={onCancelConfirm} closeAriaLabel="Close banner" primaryAction={ - } @@ -70,15 +74,11 @@ function SkipMemoSectionComponent({ } /> -
); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts index 57bf62ffd47..a530b8406d2 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + import { renderHook } from "@testing-library/react"; import { useRecipientAddressModalViewModel } from "../useRecipientAddressModalViewModel"; import { useSelector } from "LLD/hooks/redux"; @@ -11,6 +13,7 @@ import { import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors"; import { createMockAccount } from "../../__integrations__/__fixtures__/accounts"; +import { SendFlowState } from "../../../../types"; jest.mock("LLD/hooks/redux"); jest.mock("../useAddressValidation"); @@ -49,6 +52,14 @@ const mockRecipientSearch = { clear: jest.fn(), }; +const DEFAULT_STATE = { + transaction: { + status: { + errors: {}, + }, + }, +} as unknown as SendFlowState; + describe("useRecipientAddressModalViewModel", () => { beforeEach(() => { jest.clearAllMocks(); @@ -67,7 +78,7 @@ describe("useRecipientAddressModalViewModel", () => { mockedSendFeatures.getSelfTransferPolicy.mockReturnValue("impossible"); mockedUseSendFlowData.mockReturnValue({ recipientSearch: mockRecipientSearch, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); mockedUseAddressValidation.mockReturnValue({ @@ -108,7 +119,7 @@ describe("useRecipientAddressModalViewModel", () => { it("shows search results when search value is provided", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "some_address" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -165,7 +176,7 @@ describe("useRecipientAddressModalViewModel", () => { result.current.handleRecentAddressSelect(recentAddress); - expect(onAddressSelected).toHaveBeenCalledWith("recent_address", undefined); + expect(onAddressSelected).toHaveBeenCalledWith("recent_address", undefined, true); }); it("calls onAddressSelected when handleAccountSelect is called", () => { @@ -186,7 +197,7 @@ describe("useRecipientAddressModalViewModel", () => { result.current.handleAccountSelect(selectedAccount); - expect(onAddressSelected).toHaveBeenCalledWith("selected_fresh_address"); + expect(onAddressSelected).toHaveBeenCalledWith("selected_fresh_address", undefined, true); }); it("calls onAddressSelected when handleAddressSelect is called", () => { @@ -203,7 +214,7 @@ describe("useRecipientAddressModalViewModel", () => { result.current.handleAddressSelect("new_address", "ens_name"); - expect(onAddressSelected).toHaveBeenCalledWith("new_address", "ens_name"); + expect(onAddressSelected).toHaveBeenCalledWith("new_address", "ens_name", true); }); it("removes address from recent addresses when handleRemoveAddress is called", () => { @@ -235,7 +246,7 @@ describe("useRecipientAddressModalViewModel", () => { it("shows sanctioned banner when address is sanctioned", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "sanctioned_address" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -275,7 +286,7 @@ describe("useRecipientAddressModalViewModel", () => { it("shows address validation error for incorrect format", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "invalid_address" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -315,7 +326,7 @@ describe("useRecipientAddressModalViewModel", () => { it("shows matched address when validation is valid", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "valid_address" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -354,7 +365,7 @@ describe("useRecipientAddressModalViewModel", () => { it("identifies self-transfer error correctly", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "source_address" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -396,7 +407,7 @@ describe("useRecipientAddressModalViewModel", () => { it("treats InvalidAddress as incorrect format for domain-like strings", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "invalid.eth" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); @@ -436,7 +447,7 @@ describe("useRecipientAddressModalViewModel", () => { it("shows empty state when no matches and not complete", () => { mockedUseSendFlowData.mockReturnValue({ recipientSearch: { ...mockRecipientSearch, value: "searching" }, - state: {} as never, + state: DEFAULT_STATE, uiConfig: {} as never, }); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts index 4d156b79a48..d718abe4da7 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts @@ -1,7 +1,8 @@ import { useCallback, useMemo, useRef, useState } from "react"; import type { Memo } from "../../../types"; +import { useDoNotAskAgainSkipMemo } from "~/renderer/actions/settings"; -export type SkipMemoState = "propose" | "toConfirm"; +export type SkipMemoState = "propose" | "toConfirm" | "confirmed"; type UseRecipientMemoProps = Readonly<{ hasMemo: boolean; @@ -24,7 +25,8 @@ type UseRecipientMemoResult = Readonly<{ onMemoTypeChange: (type: string) => void; onSkipMemoRequestConfirm: () => void; onSkipMemoCancelConfirm: () => void; - onSkipMemoConfirm: () => void; + onSkipMemoConfirm: (doNotAskAgain: boolean) => void; + resetViewState: () => void; }>; function buildDefaultMemo(memoDefaultOption?: string): Memo { @@ -77,37 +79,68 @@ export function useRecipientMemo({ const onMemoValueChange = useCallback( (value: string) => { setMemo(prev => { + if (skipMemoState !== "propose") { + setSkipMemoState("propose"); + } + const next = { ...prev, value }; onMemoChange(next); return next; }); }, - [onMemoChange], + [onMemoChange, skipMemoState], ); const onMemoTypeChange = useCallback( (type: string) => { + if (skipMemoState !== "propose") { + setSkipMemoState("propose"); + } + const next: Memo = { value: "", type }; setMemo(next); onMemoChange(next); }, - [onMemoChange], + [onMemoChange, skipMemoState], ); - const onSkipMemoRequestConfirm = useCallback(() => { - setSkipMemoState("toConfirm"); - }, []); + const [doNotAskAgainSkipMemo, setDoNotAskAgainSkipMemo] = useDoNotAskAgainSkipMemo(); const onSkipMemoCancelConfirm = useCallback(() => { setSkipMemoState("propose"); }, []); - const onSkipMemoConfirm = useCallback(() => { - const next: Memo = { value: "", type: "NO_MEMO" }; - setMemo(next); - onMemoChange(next); - onMemoSkip(); - }, [onMemoChange, onMemoSkip]); + const onSkipMemoConfirm = useCallback( + (doNotAskAgain: boolean) => { + if (doNotAskAgainSkipMemo !== doNotAskAgain) { + setDoNotAskAgainSkipMemo(doNotAskAgain); + } + + setSkipMemoState("confirmed"); + const next: Memo = { value: "", type: "NO_MEMO" }; + setMemo(next); + onMemoChange(next); + onMemoSkip(); + }, + [onMemoChange, onMemoSkip, setDoNotAskAgainSkipMemo, doNotAskAgainSkipMemo], + ); + + const onSkipMemoRequestConfirm = useCallback(() => { + if (doNotAskAgainSkipMemo) { + onSkipMemoConfirm(doNotAskAgainSkipMemo); + } else { + setSkipMemoState("toConfirm"); + } + }, [doNotAskAgainSkipMemo, onSkipMemoConfirm]); + + const resetViewState = useCallback(() => { + if (skipMemoState === "confirmed") { + const defaultMemo = buildDefaultMemo(memoDefaultOption); + setMemo(defaultMemo); + onMemoChange(defaultMemo); + setSkipMemoState("propose"); + } + }, [memoDefaultOption, onMemoChange, setMemo, skipMemoState, setSkipMemoState]); return { memo, @@ -121,5 +154,6 @@ export function useRecipientMemo({ onSkipMemoRequestConfirm, onSkipMemoCancelConfirm, onSkipMemoConfirm, + resetViewState, }; } diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index e4434407403..e74ea94c0b0 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -20,6 +20,7 @@ import { VaultSigner, SupportedCountervaluesData, CurrencySettings, + doNotAskAgainSkipMemoSelector, } from "~/renderer/reducers/settings"; import { useRefreshAccountsOrdering } from "~/renderer/actions/general"; import { Language, Locale } from "~/config/languages"; @@ -161,6 +162,21 @@ export function useFilterTokenOperationsZeroAmount(): [ ); return [value, setter]; } +export function useDoNotAskAgainSkipMemo(): [boolean, (doNotAskAgainSkipMemo: boolean) => void] { + const dispatch = useDispatch(); + const value = useSelector(doNotAskAgainSkipMemoSelector); + const setter = useCallback( + (doNotAskAgainSkipMemo: boolean) => { + dispatch( + saveSettings({ + doNotAskAgainSkipMemo, + }), + ); + }, + [dispatch], + ); + return [value, setter]; +} export type PortfolioRangeOption = { key: PortfolioRange; value: string; diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 909bff0e2e0..dfa2a0461a4 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -126,6 +126,7 @@ export type SettingsState = { lastOnboardedDevice: Device | null; alwaysShowMemoTagInfo: boolean; anonymousUserNotifications: { LNSUpsell?: number } & Record; + doNotAskAgainSkipMemo: boolean; }; export const getInitialLanguageAndLocale = (): { language: Language; locale: Locale } => { @@ -224,6 +225,7 @@ export const INITIAL_STATE: SettingsState = { lastOnboardedDevice: null, alwaysShowMemoTagInfo: true, anonymousUserNotifications: {}, + doNotAskAgainSkipMemo: false, }; /* Handlers */ @@ -731,6 +733,8 @@ export const hideEmptyTokenAccountsSelector = (state: State) => state.settings.hideEmptyTokenAccounts; export const filterTokenOperationsZeroAmountSelector = (state: State) => state.settings.filterTokenOperationsZeroAmount; + +export const doNotAskAgainSkipMemoSelector = (state: State) => state.settings.doNotAskAgainSkipMemo; export const lastSeenDeviceSelector = (state: State): DeviceModelInfo | null | undefined => { const { lastSeenDevice } = state.settings; if (!lastSeenDevice || !Object.values(DeviceModelId).includes(lastSeenDevice.modelId)) diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/DoNotAskAgainSkipMemo.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/DoNotAskAgainSkipMemo.tsx new file mode 100644 index 00000000000..d1267e487c5 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/DoNotAskAgainSkipMemo.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { SettingsSectionRow as Row } from "~/renderer/screens/settings/SettingsSection"; +import { useDoNotAskAgainSkipMemo } from "~/renderer/actions/settings"; +import Switch from "~/renderer/components/Switch"; + +export default function DoNotAskAgainSkipMemo() { + const [doNotAskAgainSkipMemo, setDoNotAskAgainSkipMemo] = useDoNotAskAgainSkipMemo(); + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx index 76debecab48..4e855ef5a9f 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx @@ -7,6 +7,7 @@ import FilterTokenOperationsZeroAmount from "./FilterTokenOperationsZeroAmount"; import SectionExport from "./Export"; import Currencies from "./Currencies"; import BlacklistedTokens from "./BlacklistedTokens"; +import DoNotAskAgainSkipMemo from "./DoNotAskAgainSkipMemo"; export default function SectionAccounts() { const { t } = useTranslation(); @@ -22,6 +23,7 @@ export default function SectionAccounts() { + diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 99efa936898..068a80313cd 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -4885,6 +4885,10 @@ "filterTokenOperationsZeroAmount": { "title": "Hide token transactions when value is 0", "desc": "Hiding token transactions without value can prevent some potential address poisoning attacks on Eth, Polygon, BSC, Tron..." + }, + "doNotAskAgainSkipMemo": { + "title": "Do not ask again skip memo on send", + "desc": "Disable the warning about skipping the memo when sending funds" } } }, diff --git a/knip.json b/knip.json index 6fd6df65c22..7cede173679 100644 --- a/knip.json +++ b/knip.json @@ -48,6 +48,7 @@ ".storybook/settingsMock.ts", "src/renderer/mockServiceWorker.js", "src/mvvm/utils/cn.ts" + "src/mvvm/features/Send/hooks/useAvailableBalance.ts" ], "ignoreDependencies": [ "prop-types", From 97d20cbb147788dfc4d7403ad6fde60d1254769e Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Tue, 27 Jan 2026 18:33:07 +0100 Subject: [PATCH 4/9] PR reviews --- .gitignore | 3 +- .zed/debug.json | 5 - .../features/Send/components/SendHeader.tsx | 108 ++++++------------ 3 files changed, 39 insertions(+), 77 deletions(-) delete mode 100644 .zed/debug.json diff --git a/.gitignore b/.gitignore index 8b48b751af2..6a4bdf06c78 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file +libs/ui/packages/react/rsbuild.config.js.map +.zed diff --git a/.zed/debug.json b/.zed/debug.json deleted file mode 100644 index 4be1a903ab3..00000000000 --- a/.zed/debug.json +++ /dev/null @@ -1,5 +0,0 @@ -// Project-local debug tasks -// -// For more documentation on how to configure debug tasks, -// see: https://zed.dev/docs/debugger -[] diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx index 0296093730c..a30e08d644f 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx @@ -1,15 +1,8 @@ -import { getAccountCurrency } from "@ledgerhq/live-common/account/index"; import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; -import { formatCurrencyUnit } from "@ledgerhq/live-common/currencies/index"; -import { useCalculate } from "@ledgerhq/live-countervalues-react"; import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react"; -import type { AccountLike } from "@ledgerhq/types-live"; -import { useSelector } from "LLD/hooks/redux"; import { BigNumber } from "bignumber.js"; import React, { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { useMaybeAccountUnit } from "~/renderer/hooks/useAccountUnit"; -import { counterValueCurrencySelector, localeSelector } from "~/renderer/reducers/settings"; import { useFlowWizard } from "../../FlowWizard/FlowWizardContext"; import { SEND_FLOW_STEP, @@ -18,51 +11,13 @@ import { } from "@ledgerhq/live-common/flows/send/types"; import type { SendStepConfig } from "../types"; import { useSendFlowActions, useSendFlowData } from "../context/SendFlowContext"; +import { useAvailableBalance } from "../hooks/useAvailableBalance"; 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 { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "./utils"; -function useAvailableBalance(account?: AccountLike | null) { - const locale = useSelector(localeSelector); - const counterValueCurrency = useSelector(counterValueCurrencySelector); - const unit = useMaybeAccountUnit(account ?? undefined); - - const accountCurrency = useMemo( - () => (account ? getAccountCurrency(account) : undefined), - [account], - ); - - const counterValue = useCalculate({ - from: accountCurrency ?? counterValueCurrency, - to: counterValueCurrency, - value: account?.balance.toNumber() ?? 0, - disableRounding: true, - }); - - const availableBalanceFormatted = useMemo(() => { - if (!account || !unit) return ""; - return formatCurrencyUnit(unit, account.balance, { - showCode: true, - locale, - }); - }, [account, unit, locale]); - - const counterValueFormatted = useMemo(() => { - if (typeof counterValue !== "number" || !counterValueCurrency) return ""; - return formatCurrencyUnit(counterValueCurrency.units[0], new BigNumber(counterValue), { - showCode: true, - locale, - }); - }, [counterValue, counterValueCurrency, locale]); - - return useMemo(() => { - if (!account) return ""; - return counterValueFormatted || availableBalanceFormatted || ""; - }, [account, counterValueFormatted, availableBalanceFormatted]); -} - export function SendHeader() { const wizard = useFlowWizard(); const { state, uiConfig, recipientSearch } = useSendFlowData(); @@ -107,7 +62,19 @@ export function SendHeader() { const memoType = uiConfig.memoType; const memoMaxLength = uiConfig.memoMaxLength; - const memoViewModel = useRecipientMemo({ + const { + resetViewState, + showMemoValueInput, + showSkipMemo, + hasMemoTypeOptions, + memo, + onMemoTypeChange, + onMemoValueChange, + skipMemoState, + onSkipMemoRequestConfirm, + onSkipMemoCancelConfirm, + onSkipMemoConfirm, + } = useRecipientMemo({ hasMemo: uiConfig.hasMemo, memoDefaultOption, memoType, @@ -135,13 +102,13 @@ export function SendHeader() { feesStrategy: null, })); - memoViewModel.resetViewState(); + resetViewState(); } navigation.goToPreviousStep(); } else { close(); } - }, [close, currentStep, memoViewModel, navigation, transaction]); + }, [close, currentStep, navigation, resetViewState, transaction]); const handleRecipientInputClick = useCallback(() => { if (!isAmountStep) return; @@ -191,33 +158,33 @@ export function SendHeader() { {showMemoControls && currencyId ? (
- {memoViewModel.hasMemoTypeOptions ? ( + {hasMemoTypeOptions ? ( ) : null} - {memoViewModel.showMemoValueInput ? ( + {showMemoValueInput ? ( ) : null}
- {memoViewModel.showSkipMemo ? ( + {showSkipMemo ? ( ) : null}
@@ -233,17 +200,16 @@ export function SendHeader() { t, showMemoControls, currencyId, - memoViewModel.showMemoValueInput, - memoViewModel.showSkipMemo, - memoViewModel.hasMemoTypeOptions, - memoViewModel.memo.type, - memoViewModel.memo.value, - memoViewModel.onMemoTypeChange, - memoViewModel.onMemoValueChange, - memoViewModel.skipMemoState, - memoViewModel.onSkipMemoRequestConfirm, - memoViewModel.onSkipMemoCancelConfirm, - memoViewModel.onSkipMemoConfirm, + showMemoValueInput, + showSkipMemo, + hasMemoTypeOptions, + memo, + onMemoTypeChange, + onMemoValueChange, + skipMemoState, + onSkipMemoRequestConfirm, + onSkipMemoCancelConfirm, + onSkipMemoConfirm, memoTypeOptions, memoMaxLength, transactionErrorName, From 3e663e2a17b43ab90a81b1b9f49f51f15651c71f Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Wed, 28 Jan 2026 12:18:23 +0100 Subject: [PATCH 5/9] feat(desktop): reviews + reset code to develop --- .../features/Send/components/SendHeader.tsx | 59 +++++----- .../Send/hooks/useSendFlowTransaction.ts | 59 ++++------ .../components/RecipientAddressModal.tsx | 46 ++------ .../components/RecipientAddressModalView.tsx | 101 +++++++++--------- .../hooks/useBridgeRecipientValidation.ts | 65 ++++++----- .../useRecipientAddressModalViewModel.ts | 68 ++++++------ 6 files changed, 177 insertions(+), 221 deletions(-) diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx index a30e08d644f..a962840d4ca 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx @@ -4,18 +4,18 @@ import { BigNumber } from "bignumber.js"; import React, { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useFlowWizard } from "../../FlowWizard/FlowWizardContext"; -import { - SEND_FLOW_STEP, - type SendFlowStep, - type SendFlowBusinessContext, -} from "@ledgerhq/live-common/flows/send/types"; -import type { SendStepConfig } from "../types"; import { useSendFlowActions, useSendFlowData } from "../context/SendFlowContext"; import { useAvailableBalance } from "../hooks/useAvailableBalance"; 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 { + SEND_FLOW_STEP, + type SendFlowStep, + type SendFlowBusinessContext, +} from "@ledgerhq/live-common/flows/send/types"; +import type { SendStepConfig } from "../types"; import { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "./utils"; export function SendHeader() { @@ -27,29 +27,13 @@ export function SendHeader() { const { navigation, currentStep } = wizard; const currentStepConfig = wizard.currentStepConfig as SendStepConfig; - const currencyName = state.account.currency?.ticker ?? ""; - const availableText = useAvailableBalance(state.account.account); - - const showBackButton = navigation.canGoBack(); - const showTitle = currentStepConfig?.showTitle !== false; - - const title = showTitle ? t("newSendFlow.title", { currency: currencyName }) : ""; - const descriptionText = - showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : ""; + const currencyId = state.account.currency?.id; const showRecipientInput = currentStepConfig?.addressInput ?? false; - const isAmountStep = currentStep === SEND_FLOW_STEP.AMOUNT; - - const addressInputValue = useMemo(() => { - if (isAmountStep) return getRecipientDisplayValue(state.recipient); - return recipientSearch.value; - }, [isAmountStep, recipientSearch.value, state.recipient]); - const showMemoControls = Boolean( showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0, ); - const currencyId = state.account.currency?.id; const memoDefaultOption = useMemo(() => { return state.account.currency ? sendFeatures.getMemoDefaultOption(state.account.currency) @@ -63,17 +47,17 @@ export function SendHeader() { const memoMaxLength = uiConfig.memoMaxLength; const { - resetViewState, - showMemoValueInput, - showSkipMemo, hasMemoTypeOptions, memo, onMemoTypeChange, + showMemoValueInput, onMemoValueChange, + showSkipMemo, skipMemoState, onSkipMemoRequestConfirm, onSkipMemoCancelConfirm, onSkipMemoConfirm, + resetViewState, } = useRecipientMemo({ hasMemo: uiConfig.hasMemo, memoDefaultOption, @@ -90,6 +74,9 @@ export function SendHeader() { }`, }); + const currencyName = state.account.currency?.ticker ?? ""; + const availableText = useAvailableBalance(state.account.account); + const handleBack = useCallback(() => { if (navigation.canGoBack()) { // Reset amount-related state when leaving Amount step (back navigation), @@ -110,6 +97,22 @@ export function SendHeader() { } }, [close, currentStep, navigation, resetViewState, transaction]); + const showBackButton = navigation.canGoBack(); + const showTitle = currentStepConfig?.showTitle !== false; + + const title = showTitle ? t("newSendFlow.title", { currency: currencyName }) : ""; + const descriptionText = + showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : ""; + + 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; @@ -200,12 +203,12 @@ export function SendHeader() { t, showMemoControls, currencyId, - showMemoValueInput, - showSkipMemo, hasMemoTypeOptions, memo, onMemoTypeChange, + showMemoValueInput, onMemoValueChange, + showSkipMemo, skipMemoState, onSkipMemoRequestConfirm, onSkipMemoCancelConfirm, diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts index 3337c025ea8..21d4e1968e6 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowTransaction.ts @@ -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; @@ -47,61 +47,38 @@ export function useSendFlowTransaction({ [bridgeUpdateTransaction], ); - const buildRecipientUpdates = useCallback( - (currentTransaction: Transaction, recipient: RecipientData): Partial => { - const updates: Partial = {}; + const setRecipient = useCallback( + (recipient: RecipientData) => { + if (!account || !transaction) return; - if (recipient.address !== undefined) { - updates.recipient = recipient.address; - } + const bridge = getAccountBridge(account, parentAccount); + const updates: Partial = { recipient: recipient.address }; if (recipient.memo !== undefined) { Object.assign( updates, applyMemoToTransaction( - currentTransaction.family, + transaction.family, recipient.memo.value, recipient.memo.type, - currentTransaction, + transaction, ), ); } if (recipient.destinationTag !== undefined) { - const trimmed = recipient.destinationTag.trim(); - if (trimmed.length > 0) { - const parsedTag = Number(trimmed); - if (Number.isFinite(parsedTag)) { - Object.assign( - updates, - applyMemoToTransaction( - currentTransaction.family, - parsedTag, - undefined, - currentTransaction, - ), - ); - } + const parsedTag = Number(recipient.destinationTag.trim()); + if (Number.isFinite(parsedTag)) { + Object.assign( + updates, + applyMemoToTransaction(transaction.family, parsedTag, undefined, transaction), + ); } } - return updates; - }, - [], - ); - - const setRecipient = useCallback( - (recipient: RecipientData) => { - if (!account || !transaction) return; - - const bridge = getAccountBridge(account, parentAccount); - const updates = buildRecipientUpdates(transaction, recipient); - - if (Object.keys(updates).length > 0) { - bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); - } + bridgeSetTransaction(bridge.updateTransaction(transaction, updates)); }, - [account, parentAccount, transaction, bridgeSetTransaction, buildRecipientUpdates], + [account, parentAccount, transaction, bridgeSetTransaction], ); const setAccountForTransaction = useCallback( diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx index a8159d06419..f4f1a4e5449 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModal.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; import type { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; -import { RecipientAddressModalView } from "./RecipientAddressModalView"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import React from "react"; import { useRecipientAddressModalViewModel } from "../hooks/useRecipientAddressModalViewModel"; +import { RecipientAddressModalView } from "./RecipientAddressModalView"; type RecipientAddressModalProps = Readonly<{ isOpen: boolean; @@ -37,40 +37,12 @@ export function RecipientAddressModal({ return ( ); } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx index 4754c3a5711..2740f8b12c4 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/RecipientAddressModalView.tsx @@ -1,29 +1,26 @@ -import React from "react"; -import type { Account } from "@ledgerhq/types-live"; import type { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; -import EmptyList from "./EmptyList"; -import { RecentAddressesSection } from "./RecentAddressesSection"; -import { MyAccountsSection } from "./MyAccountsSection"; -import { AddressMatchedSection } from "./AddressMatchedSection"; -import { LoadingState } from "./LoadingState"; -import { ValidationBanner } from "./ValidationBanner"; -import { AddressValidationError } from "./AddressValidationError"; +import type { Account } from "@ledgerhq/types-live"; +import React from "react"; import type { AddressSearchResult, - RecentAddress, AddressValidationError as AddressValidationErrorType, + RecentAddress, } from "../types"; +import { AddressMatchedSection } from "./AddressMatchedSection"; +import { AddressValidationError } from "./AddressValidationError"; +import EmptyList from "./EmptyList"; +import { LoadingState } from "./LoadingState"; +import { MyAccountsSection } from "./MyAccountsSection"; +import { RecentAddressesSection } from "./RecentAddressesSection"; +import { ValidationBanner } from "./ValidationBanner"; -type RecipientAddressModalViewData = Readonly<{ +type RecipientAddressModalViewProps = Readonly<{ searchValue: string; isLoading: boolean; result: AddressSearchResult; recentAddresses: RecentAddress[]; mainAccount: Account; currency: CryptoOrTokenCurrency; -}>; - -type RecipientAddressModalViewUi = Readonly<{ showInitialState: boolean; showInitialEmptyState: boolean; showMatchedAddress: boolean; @@ -39,47 +36,45 @@ type RecipientAddressModalViewUi = Readonly<{ bridgeRecipientError: Error | undefined; bridgeRecipientWarning: Error | undefined; bridgeSenderError: Error | undefined; - hasMemo: boolean; - hasMemoValidationError: boolean; - hasFilledMemo: boolean; -}>; - -type RecipientAddressModalViewActions = Readonly<{ onRecentAddressSelect: (address: RecentAddress) => void; onAccountSelect: (account: Account) => void; onAddressSelect: (address: string, ensName?: string) => void; onRemoveAddress: (address: RecentAddress) => void; + hasMemo: boolean; + hasMemoValidationError: boolean; + hasFilledMemo: boolean; }>; -type RecipientAddressModalViewProps = Readonly<{ - data: RecipientAddressModalViewData; - ui: RecipientAddressModalViewUi; - actions: RecipientAddressModalViewActions; -}>; - -export function RecipientAddressModalView({ data, ui, actions }: RecipientAddressModalViewProps) { - const { searchValue, isLoading, result, recentAddresses, mainAccount, currency } = data; - const { - showInitialState, - showInitialEmptyState, - showMatchedAddress, - showAddressValidationError, - showEmptyState, - showBridgeSenderError, - showSanctionedBanner, - showBridgeRecipientError, - showBridgeRecipientWarning, - isSanctioned, - isAddressComplete, - addressValidationErrorType, - bridgeRecipientError, - bridgeRecipientWarning, - bridgeSenderError, - hasMemo, - hasMemoValidationError, - hasFilledMemo, - } = ui; - +export function RecipientAddressModalView({ + searchValue, + isLoading, + result, + recentAddresses, + mainAccount, + currency, + showInitialState, + showInitialEmptyState, + showMatchedAddress, + showAddressValidationError, + showEmptyState, + showBridgeSenderError, + showSanctionedBanner, + showBridgeRecipientError, + showBridgeRecipientWarning, + isSanctioned, + isAddressComplete, + addressValidationErrorType, + bridgeRecipientError, + bridgeRecipientWarning, + bridgeSenderError, + onRecentAddressSelect, + onAccountSelect, + onAddressSelect, + onRemoveAddress, + hasMemo, + hasMemoValidationError, + hasFilledMemo, +}: RecipientAddressModalViewProps) { const shouldShowErrorBanner = !isLoading && (showBridgeSenderError || @@ -95,13 +90,13 @@ export function RecipientAddressModalView({ data, ui, actions }: RecipientAddres <> )} @@ -110,7 +105,7 @@ export function RecipientAddressModalView({ data, ui, actions }: RecipientAddres | null>(null); - const abortControllerRef = useRef(null); - const lastValidationKeyRef = useRef(""); + const lastRecipientRef = useRef(""); const validationTriggeredRef = useRef(false); + const debounceTimeoutRef = useRef(null); + const abortControllerRef = useRef(null); + // Cleanup function to clear timeout and abort pending validations const cleanup = useCallback(() => { if (debounceTimeoutRef.current) { clearTimeout(debounceTimeoutRef.current); @@ -66,9 +67,10 @@ export function useBridgeRecipientValidation({ }, []); const validateRecipient = useCallback(async () => { + // Clear timeout reference when callback executes debounceTimeoutRef.current = null; - if (!enabled || !account || !recipient) { + if (!account || !recipient || !enabled) { setValidationState({ errors: {}, warnings: {}, @@ -78,6 +80,7 @@ export function useBridgeRecipientValidation({ return; } + // Cancel any pending validation if (abortControllerRef.current) { abortControllerRef.current.abort(); } @@ -104,20 +107,27 @@ export function useBridgeRecipientValidation({ } const preparedTransaction = await bridge.prepareTransaction(mainAccount, transaction); + if (signal.aborted) return; const status = await bridge.getTransactionStatus(mainAccount, preparedTransaction); + if (signal.aborted) return; const errors: BridgeValidationErrors = {}; const warnings: BridgeValidationWarnings = {}; - if (status.errors.recipient) errors.recipient = status.errors.recipient; - if (status.errors.sender) errors.sender = status.errors.sender; - if (status.errors.transaction) errors.transaction = status.errors.transaction; + if (status.errors.recipient) { + errors.recipient = status.errors.recipient; + } + if (status.errors.sender) { + errors.sender = status.errors.sender; + } Object.entries(status.warnings).forEach(([key, value]) => { - if (value) warnings[key] = value; + if (value) { + warnings[key] = value; + } }); setValidationState({ @@ -128,6 +138,7 @@ export function useBridgeRecipientValidation({ }); } catch (error) { if (signal.aborted) return; + console.error("Bridge recipient validation failed:", error); setValidationState({ errors: {}, @@ -136,18 +147,18 @@ export function useBridgeRecipientValidation({ status: null, }); } - }, [account, enabled, memo, parentAccount, recipient]); - - const validationKey = `${enabled ? 1 : 0}|${account?.id ?? ""}|${parentAccount?.id ?? ""}|${recipient}|${ - memo?.type ?? "" - }|${memo?.value ?? ""}`; + }, [account, recipient, enabled, parentAccount, memo]); - if (validationKey !== lastValidationKeyRef.current) { - lastValidationKeyRef.current = validationKey; + if (recipient !== lastRecipientRef.current) { + lastRecipientRef.current = recipient; validationTriggeredRef.current = false; - cleanup(); - if (!enabled || !account || !recipient) { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + debounceTimeoutRef.current = null; + } + + if (!recipient) { setValidationState({ errors: {}, warnings: {}, @@ -157,11 +168,17 @@ export function useBridgeRecipientValidation({ } } - if (enabled && account && recipient && !validationTriggeredRef.current) { + if (recipient && !validationTriggeredRef.current && enabled) { validationTriggeredRef.current = true; + + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + setValidationState(prev => ({ ...prev, isLoading: true })); + debounceTimeoutRef.current = setTimeout(() => { - validateRecipient().catch(() => undefined); + validateRecipient(); }, DEBOUNCE_DELAY); } diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts index 93d9a3abd51..d55d51ec74b 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientAddressModalViewModel.ts @@ -1,23 +1,23 @@ -import { useCallback, useMemo, useState } from "react"; -import { useSelector } from "LLD/hooks/redux"; import { getAccountCurrency, getMainAccount, getRecentAddressesStore, } from "@ledgerhq/live-common/account/index"; import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; -import type { Account, AccountLike } from "@ledgerhq/types-live"; import type { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import type { + Account, + AccountLike, + RecentAddress as RecentAddressFromStore, +} from "@ledgerhq/types-live"; +import { useSelector } from "LLD/hooks/redux"; +import { useCallback, useMemo, useState } from "react"; import { accountsSelector } from "~/renderer/reducers/accounts"; +import { useSendFlowData } from "../../../context/SendFlowContext"; import type { RecentAddress } from "../types"; +import { normalizeLastUsedTimestamp } from "../utils/dateFormatter"; import { useAddressValidation } from "./useAddressValidation"; import { useRecipientSearchState } from "./useRecipientSearchState"; -import { normalizeLastUsedTimestamp } from "../utils/dateFormatter"; -import { useSendFlowData } from "../../../context/SendFlowContext"; - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} type UseRecipientAddressModalViewModelProps = Readonly<{ account: AccountLike; @@ -47,13 +47,6 @@ export function useRecipientAddressModalViewModel({ currentAccountId: mainAccount.id, }); - const recipientSearchState = useRecipientSearchState({ - searchValue: recipientSearch.value, - result, - isLoading, - recipientSupportsDomain, - }); - const allAccounts = useSelector(accountsSelector); const userAccountsForCurrency = useMemo(() => { const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); @@ -67,8 +60,9 @@ export function useRecipientAddressModalViewModel({ }, [allAccounts, currency, mainAccount.id]); const recentAddresses = useMemo(() => { - const raw = getRecentAddressesStore().getAddresses(currency.id); - const addressesWithMetadata = Array.isArray(raw) ? raw : []; + const addressesWithMetadata = getRecentAddressesStore().getAddresses( + currency.id, + ) as unknown as RecentAddressFromStore[]; const selfTransferPolicy = sendFeatures.getSelfTransferPolicy(currency); const userAccountsByAddress = new Map( @@ -77,40 +71,29 @@ export function useRecipientAddressModalViewModel({ return addressesWithMetadata .filter(entry => { - if (!isRecord(entry)) return false; - const address = entry.address; - if (typeof address !== "string" || address.length === 0) return false; + if (!entry?.address) return false; if ( selfTransferPolicy === "impossible" && - address.toLowerCase() === mainAccount.freshAddress.toLowerCase() + entry.address.toLowerCase() === mainAccount.freshAddress.toLowerCase() ) { return false; } return true; }) .map(entry => { - if (!isRecord(entry) || typeof entry.address !== "string") { - // Should never happen due to filter above - return null; - } - - const address = entry.address; - const ensName = typeof entry.ensName === "string" ? entry.ensName : undefined; - const lastUsed = typeof entry.lastUsed === "number" ? entry.lastUsed : undefined; - const lastUsedTimestamp = normalizeLastUsedTimestamp(lastUsed); - const matchedAccount = userAccountsByAddress.get(address.toLowerCase()); + const matchedAccount = userAccountsByAddress.get(entry.address.toLowerCase()); + const lastUsedTimestamp = normalizeLastUsedTimestamp(entry.lastUsed); const recentAddress: RecentAddress = { - address, + address: entry.address, currency, lastUsedAt: new Date(lastUsedTimestamp), - name: address, - ensName, + name: entry.address, + ensName: entry.ensName, isLedgerAccount: !!matchedAccount, accountId: matchedAccount?.id, }; return recentAddress; - }) - .filter((value): value is RecentAddress => value !== null); + }); // refreshKey is used to force recalculation when addresses are removed from the store // even though it's not directly used in the computation // eslint-disable-next-line react-hooks/exhaustive-deps @@ -147,6 +130,7 @@ export function useRecipientAddressModalViewModel({ if (hasMemo) { recipientSearch.setValue(address.ensName ?? address.address); } + onAddressSelected(address.address, address.ensName, !hasMemo); }, [hasMemo, onAddressSelected, recipientSearch], @@ -157,6 +141,7 @@ export function useRecipientAddressModalViewModel({ if (hasMemo) { recipientSearch.setValue(selectedAccount.freshAddress); } + onAddressSelected(selectedAccount.freshAddress, undefined, !hasMemo); }, [hasMemo, onAddressSelected, recipientSearch], @@ -177,6 +162,13 @@ export function useRecipientAddressModalViewModel({ [currency], ); + const searchState = useRecipientSearchState({ + searchValue: recipientSearch.value, + result, + isLoading, + recipientSupportsDomain, + }); + return { searchValue: recipientSearch.value, isLoading, @@ -185,7 +177,6 @@ export function useRecipientAddressModalViewModel({ mainAccount, showInitialState, showInitialEmptyState, - ...recipientSearchState, handleRecentAddressSelect, handleAccountSelect, handleAddressSelect, @@ -197,5 +188,6 @@ export function useRecipientAddressModalViewModel({ memoTypeOptions, memoDefaultOption, memoMaxLength, + ...searchState, }; } From 8feb951eb7bb4a52a8801f17cc48f47aff013b8e Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Wed, 28 Jan 2026 12:26:37 +0100 Subject: [PATCH 6/9] feat(desktop): fix knip.json --- .../ModularDialog/components/Address/formatAddress.ts | 2 +- .../src/mvvm/features/Send/hooks/useSendFlowState.ts | 1 - .../screens/Recipient/components/AddressMatchedSection.tsx | 4 +++- knip.json | 2 +- libs/ledger-live-common/src/flows/send/types.ts | 6 ++++-- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts index 976296a7b14..086ecbba942 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts @@ -10,7 +10,7 @@ * @returns Formatted address string */ export const formatAddress = ( - address?: string, + address: string, options: { prefixLength?: number; suffixLength?: number; diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts index 61b62ff0b62..7d4191ad6f9 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendFlowState.ts @@ -62,7 +62,6 @@ export function useSendFlowBusinessLogic({ (recipient: RecipientData) => { setRecipient(recipient); transactionHook.actions.setRecipient(recipient); - setRecipient(prev => (prev ? { ...prev, ...recipient } : recipient)); }, [transactionHook.actions], ); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx index 42a0ff51c06..775366cf44b 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/components/AddressMatchedSection.tsx @@ -53,7 +53,9 @@ export function AddressMatchedSection({ const getRecentDescription = (): string | undefined => { if (matchedRecentAddress) { - return `Already used · ${formatRelativeDate(matchedRecentAddress.lastUsedAt)}`; + return t("newSendFlow.alreadyUsed", { + date: formatRelativeDate(matchedRecentAddress.lastUsedAt), + }); } return formattedAddress; }; diff --git a/knip.json b/knip.json index 7cede173679..8e7e20dbe26 100644 --- a/knip.json +++ b/knip.json @@ -47,7 +47,7 @@ ".storybook/fsStub.js", ".storybook/settingsMock.ts", "src/renderer/mockServiceWorker.js", - "src/mvvm/utils/cn.ts" + "src/mvvm/utils/cn.ts", "src/mvvm/features/Send/hooks/useAvailableBalance.ts" ], "ignoreDependencies": [ diff --git a/libs/ledger-live-common/src/flows/send/types.ts b/libs/ledger-live-common/src/flows/send/types.ts index 8f1737f9f36..8cdaa044885 100644 --- a/libs/ledger-live-common/src/flows/send/types.ts +++ b/libs/ledger-live-common/src/flows/send/types.ts @@ -33,10 +33,12 @@ export type SendFlowUiConfig = Readonly<{ hasCoinControl: boolean; }>; +export type Memo = { value: string; type?: string }; + export type RecipientData = Readonly<{ - address: string; + address?: string; ensName?: string; - memo?: string; + memo?: Memo; destinationTag?: string; }>; From 22198043e8bfb115e04eefce84f1939e1a6e6bca Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Wed, 28 Jan 2026 17:04:55 +0100 Subject: [PATCH 7/9] feat(desktop): fix desktop typecheck --- .../ModularDialog/components/Address/formatAddress.ts | 2 +- .../hooks/__tests__/useRecipientAddressModalViewModel.test.ts | 2 +- .../screens/Recipient/hooks/useBridgeRecipientValidation.ts | 2 +- .../features/Send/screens/Recipient/hooks/useRecipientMemo.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts index 086ecbba942..74e57b02d27 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/ModularDialog/components/Address/formatAddress.ts @@ -10,7 +10,7 @@ * @returns Formatted address string */ export const formatAddress = ( - address: string, + address: string | undefined, options: { prefixLength?: number; suffixLength?: number; diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts index a530b8406d2..af966b8554a 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/__tests__/useRecipientAddressModalViewModel.test.ts @@ -13,7 +13,7 @@ import { import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; import { InvalidAddress, InvalidAddressBecauseDestinationIsAlsoSource } from "@ledgerhq/errors"; import { createMockAccount } from "../../__integrations__/__fixtures__/accounts"; -import { SendFlowState } from "../../../../types"; +import type { SendFlowState } from "@ledgerhq/live-common/flows/send/types"; jest.mock("LLD/hooks/redux"); jest.mock("../useAddressValidation"); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts index 37abaed0f61..a99523885d1 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useBridgeRecipientValidation.ts @@ -4,7 +4,7 @@ import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import type { TransactionStatus } from "@ledgerhq/live-common/generated/types"; import type { Account, AccountLike } from "@ledgerhq/types-live"; import { useCallback, useMemo, useRef, useState } from "react"; -import type { Memo } from "../../../types"; +import type { Memo } from "@ledgerhq/live-common/flows/send/types"; import type { BridgeValidationErrors, BridgeValidationWarnings } from "../types"; export type BridgeRecipientValidationResult = { diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts index d718abe4da7..cb5e452e355 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/screens/Recipient/hooks/useRecipientMemo.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, useRef, useState } from "react"; -import type { Memo } from "../../../types"; +import type { Memo } from "@ledgerhq/live-common/flows/send/types"; import { useDoNotAskAgainSkipMemo } from "~/renderer/actions/settings"; export type SkipMemoState = "propose" | "toConfirm" | "confirmed"; @@ -78,7 +78,7 @@ export function useRecipientMemo({ const onMemoValueChange = useCallback( (value: string) => { - setMemo(prev => { + setMemo((prev: Memo) => { if (skipMemoState !== "propose") { setSkipMemoState("propose"); } From 80fdd1d08ba1ca9e49d0f8ecf1ab1edf2fba186c Mon Sep 17 00:00:00 2001 From: Moustafa Koterba Date: Wed, 28 Jan 2026 18:21:08 +0100 Subject: [PATCH 8/9] feat(desktop): setup mvvm for SendHeader component --- .../features/Send/components/SendHeader.tsx | 102 +++++------------ .../features/Send/context/SendFlowContext.tsx | 2 +- .../features/Send/hooks/useSendHeaderModel.ts | 108 ++++++++++++++++++ 3 files changed, 136 insertions(+), 76 deletions(-) create mode 100644 apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendHeaderModel.ts diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx index a962840d4ca..d2e8d56659d 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/components/SendHeader.tsx @@ -1,50 +1,39 @@ import { sendFeatures } from "@ledgerhq/live-common/bridge/descriptor"; +import { + SEND_FLOW_STEP, + type SendFlowBusinessContext, + type SendFlowStep, +} from "@ledgerhq/live-common/flows/send/types"; import { AddressInput, DialogHeader } from "@ledgerhq/lumen-ui-react"; -import { BigNumber } from "bignumber.js"; -import React, { useCallback, useMemo } from "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"; +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 { - SEND_FLOW_STEP, - type SendFlowStep, - type SendFlowBusinessContext, -} from "@ledgerhq/live-common/flows/send/types"; import type { SendStepConfig } from "../types"; -import { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "./utils"; export function SendHeader() { const wizard = useFlowWizard(); 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 showRecipientInput = currentStepConfig?.addressInput ?? false; - const showMemoControls = Boolean( - showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0, - ); - const memoDefaultOption = useMemo(() => { - return state.account.currency - ? sendFeatures.getMemoDefaultOption(state.account.currency) - : undefined; + return sendFeatures.getMemoDefaultOption(state.account.currency ?? undefined); }, [state.account.currency]); const memoTypeOptions = useMemo(() => { return uiConfig.memoOptions ?? []; }, [uiConfig]); - const memoType = uiConfig.memoType; - const memoMaxLength = uiConfig.memoMaxLength; const { hasMemoTypeOptions, @@ -61,7 +50,7 @@ export function SendHeader() { } = useRecipientMemo({ hasMemo: uiConfig.hasMemo, memoDefaultOption, - memoType, + memoType: uiConfig.memoType, memoTypeOptions, onMemoChange: memo => { transaction.setRecipient({ ...state.recipient, memo }); @@ -74,58 +63,20 @@ export function SendHeader() { }`, }); - const currencyName = state.account.currency?.ticker ?? ""; - const availableText = useAvailableBalance(state.account.account); - - 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, - })); - - resetViewState(); - } - navigation.goToPreviousStep(); - } else { - close(); - } - }, [close, currentStep, navigation, resetViewState, transaction]); - - const showBackButton = navigation.canGoBack(); - const showTitle = currentStepConfig?.showTitle !== false; - - 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 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 transactionErrorName = state.transaction.status?.errors?.transaction?.name; - const recipientInputContent = useMemo(() => { if (!showRecipientInput) return null; @@ -174,7 +125,7 @@ export function SendHeader() { @@ -200,22 +151,23 @@ export function SendHeader() { addressInputValue, recipientSearch, uiConfig.recipientSupportsDomain, + uiConfig.memoMaxLength, t, showMemoControls, currencyId, hasMemoTypeOptions, - memo, + memoTypeOptions, + memo.type, + memo.value, onMemoTypeChange, showMemoValueInput, + transactionErrorName, onMemoValueChange, showSkipMemo, skipMemoState, onSkipMemoRequestConfirm, onSkipMemoCancelConfirm, onSkipMemoConfirm, - memoTypeOptions, - memoMaxLength, - transactionErrorName, handleRecipientInputClick, ]); diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/context/SendFlowContext.tsx b/apps/ledger-live-desktop/src/mvvm/features/Send/context/SendFlowContext.tsx index 10f2953116e..4b3e7ae6938 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/Send/context/SendFlowContext.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/context/SendFlowContext.tsx @@ -24,7 +24,7 @@ import type { SendStepConfig } from "../types"; */ // Data Context -type DataContextValue = Readonly<{ +export type DataContextValue = Readonly<{ state: SendFlowState; uiConfig: SendFlowUiConfig; recipientSearch: Readonly<{ diff --git a/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendHeaderModel.ts b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendHeaderModel.ts new file mode 100644 index 00000000000..4c6f4add878 --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/Send/hooks/useSendHeaderModel.ts @@ -0,0 +1,108 @@ +import { SendFlowStep, SEND_FLOW_STEP } from "@ledgerhq/live-common/flows/send/types"; +import { t } from "i18next"; +import { useMemo, useCallback } from "react"; +import { useFlowWizard } from "../../FlowWizard/FlowWizardContext"; +import { getRecipientDisplayValue, getRecipientSearchPrefillValue } from "../components/utils"; +import { + SendFlowBusinessContext, + useSendFlowActions, + useSendFlowData, +} from "../context/SendFlowContext"; +import { SendStepConfig } from "../types"; +import BigNumber from "bignumber.js"; + +type UseSendHeaderModelParams = Readonly<{ + availableText: string; + resetViewState: () => void; +}>; +type UseSendHeaderModelResult = Readonly<{ + addressInputValue: string | undefined; + descriptionText: string | undefined; + handleBack: () => void; + handleRecipientInputClick: () => void; + showBackButton: boolean; + showRecipientInput: boolean; + showMemoControls: boolean; + title: string | undefined; + transactionErrorName: string | undefined; +}>; + +export function useSendHeaderModel({ + availableText, + resetViewState, +}: UseSendHeaderModelParams): UseSendHeaderModelResult { + const wizard = useFlowWizard(); + const { state, uiConfig, recipientSearch } = useSendFlowData(); + const { close, transaction } = useSendFlowActions(); + + const currencyName = state.account.currency?.ticker ?? ""; + + const { navigation, currentStep } = wizard; + const currentStepConfig = wizard.currentStepConfig as SendStepConfig; + + const showRecipientInput = currentStepConfig?.addressInput ?? false; + const showMemoControls = Boolean( + showRecipientInput && uiConfig.hasMemo && recipientSearch.value.length > 0, + ); + + const showBackButton = navigation.canGoBack(); + + const showTitle = currentStepConfig?.showTitle !== false; + const title = showTitle ? t("newSendFlow.title", { currency: currencyName }) : ""; + const descriptionText = + showTitle && availableText ? t("newSendFlow.available", { amount: availableText }) : ""; + + const isRecipientStep = currentStep === SEND_FLOW_STEP.RECIPIENT; + const isAmountStep = currentStep === SEND_FLOW_STEP.AMOUNT; + + 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, + })); + + resetViewState(); + } + navigation.goToPreviousStep(); + } else { + close(); + } + }, [close, currentStep, navigation, resetViewState, transaction]); + + 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 transactionErrorName = state.transaction.status?.errors?.transaction?.name; + + return { + addressInputValue, + descriptionText, + handleBack, + handleRecipientInputClick, + showBackButton, + showMemoControls, + showRecipientInput, + title, + transactionErrorName, + }; +} From e0c01b8dd494c92ef815809fd092419c12420fbe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:29:37 +0000 Subject: [PATCH 9/9] test(lld): update screenshots (ubuntu-22.04) lld, test, screenshot --- .../settings-accounts-page-linux.png | Bin 89259 -> 82124 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/ledger-live-desktop/tests/specs/settings/settings.spec.ts-snapshots/settings-accounts-page-linux.png b/apps/ledger-live-desktop/tests/specs/settings/settings.spec.ts-snapshots/settings-accounts-page-linux.png index d79a275304823765c2e8fab8879db874957c5c65..9ce264bd9c4b167b90ceba5595de4f910774fcdb 100644 GIT binary patch literal 82124 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

`Ebq*Ptxlu+pv6sQ$w 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

Guy4~IZ7LV*5SS!O3PQpvj*G~^dqO`TzQ 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 literal 89259 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�zxCk7o)( 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