diff --git a/.changeset/forty-ants-end.md b/.changeset/forty-ants-end.md new file mode 100644 index 000000000000..a8639cb40431 --- /dev/null +++ b/.changeset/forty-ants-end.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +Unify the Memo/Tag input wording, tooltip for coins that support memo tag except (solana, ton, stellar -> at this scoping level we decided not to touch the flow/wording of memo for those coins) diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index 70cae363b2a4..4f9e147e34f9 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -188,6 +188,9 @@ export const urls = { "https://shop.ledger.com?utm_source=live&utm_medium=draw&utm_campaign=ledger_sync_lns_uncompatible&utm_content=to_shop", learnMoreLedgerSync: "https://www.ledger.com/blog-ledger-sync-synchronize-your-crypto-accounts-effortless-private-and-secure", + memoTag: { + learnMore: "https://support.ledger.com/article/4409603715217-zd", + }, }; export const vaultSigner = { diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/LearnMoreCta.test.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/LearnMoreCta.test.tsx new file mode 100644 index 000000000000..14ff941c8923 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/LearnMoreCta.test.tsx @@ -0,0 +1,27 @@ +/** + * @jest-environment jsdom + */ +import React from "react"; +import { fireEvent, render, screen } from "tests/testUtils"; +import * as LinkingHelpers from "~/renderer/linking"; +import LearnMoreCta from "../components/LearnMoreCta"; + +jest.mock("~/renderer/linking", () => ({ + openURL: jest.fn(), +})); + +describe("MemoTagInfoBody", () => { + it("should display MemoTagInfoBody correctly", async () => { + render(); + + const learnMoreLink = screen.getByText(/Learn more about Tag\/Memo/); + + expect(learnMoreLink).toBeVisible(); + + fireEvent.click(learnMoreLink); + + jest.spyOn(LinkingHelpers, "openURL"); + // spec: when a user clicks on the learn more link, it should open the link + expect(LinkingHelpers.openURL).toHaveBeenCalledWith("example.domain.com"); + }); +}); diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagField.test.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagField.test.tsx new file mode 100644 index 000000000000..d05201646a8b --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagField.test.tsx @@ -0,0 +1,54 @@ +/** + * @jest-environment jsdom + */ + +import React from "react"; +import { render, screen, fireEvent } from "tests/testUtils"; +import MemoTagField from "../components/MemoTagField"; + +jest.mock("react-i18next", () => ({ + ...jest.requireActual("react-i18next"), + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +describe("MemoTagField", () => { + it("renders MemoTagField with label and text field", () => { + render(); + expect(screen.getByText("MemoTagField.label")).toBeInTheDocument(); + expect(screen.getByRole("textbox")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("MemoTagField.placeholder")).toBeInTheDocument(); + }); + + it("should render MemoTagField without label", () => { + render(); + expect(screen.queryByText("MemoTagField.label")).not.toBeInTheDocument(); + }); + + it("should call onChange when input value changes", () => { + const handleChange = jest.fn(); + render(); + fireEvent.change(screen.getByPlaceholderText("MemoTagField.placeholder"), { + target: { value: "new memo" }, + }); + expect(handleChange).toHaveBeenCalledTimes(1); + }); + + it("should render CaracterCountComponent if provided", () => { + const CaracterCountComponent = () =>
Character Count
; + render(); + expect(screen.getByText("Character Count")).toBeInTheDocument(); + }); + + it("should render instruction text on autoFocus", () => { + render(); + expect(screen.getByPlaceholderText("MemoTagField.placeholder")).toHaveFocus(); + expect(screen.getByText("MemoTagField.instruction")).toBeInTheDocument(); + }); + + it("should render with error", () => { + render(); + expect(screen.getByTestId("input-error")).toBeInTheDocument(); + }); +}); diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagInfoBody.test.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagInfoBody.test.tsx index 3becffa6c68e..0206eaea8b66 100644 --- a/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagInfoBody.test.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/__tests__/MemoTagInfoBody.test.tsx @@ -2,10 +2,8 @@ * @jest-environment jsdom */ import React from "react"; -import { fireEvent, render, screen } from "tests/testUtils"; +import { render, screen } from "tests/testUtils"; import MemoTagInfoBody from "../components/MemoTagInfoBody"; -import { MEMO_TAG_LEARN_MORE_LINK } from "../constants"; -import * as LinkingHelpers from "~/renderer/linking"; jest.mock("~/renderer/linking", () => ({ openURL: jest.fn(), @@ -16,15 +14,7 @@ describe("MemoTagInfoBody", () => { render(); const memoTagInfoBody = screen.getByTestId("memo-tag-info-body"); - const learnMoreLink = screen.getByText(/Learn more about Tag\/Memo/); expect(memoTagInfoBody).toBeVisible(); - expect(learnMoreLink).toBeVisible(); - - fireEvent.click(learnMoreLink); - - jest.spyOn(LinkingHelpers, "openURL"); - // spec: when a user clicks on the learn more link, it should open the link - expect(LinkingHelpers.openURL).toHaveBeenCalledWith(MEMO_TAG_LEARN_MORE_LINK); }); }); diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/LearnMoreCta.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/LearnMoreCta.tsx new file mode 100644 index 000000000000..49847bb9f0fb --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/LearnMoreCta.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Link } from "@ledgerhq/react-ui"; +import { Trans } from "react-i18next"; +import { openURL } from "~/renderer/linking"; +import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls"; + +type LearnMoreCtaProps = { + size?: "small" | "medium" | "large"; + color?: string; + style?: React.CSSProperties; + Icon?: React.ComponentType<{ size: number; color?: string }>; + url: string; +}; + +const LearnMoreCta = ({ + size = "small", + color = "neutral.c80", + style, + Icon, + url, +}: LearnMoreCtaProps) => { + const localizedUrl = useLocalizedUrl(url); + + if (!localizedUrl) return null; + + const handleOpenLMLink = () => openURL(localizedUrl); + + return ( + + + + ); +}; + +export default LearnMoreCta; diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagField.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagField.tsx new file mode 100644 index 000000000000..1485ae9ae29e --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagField.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import Input, { Props as InputBaseProps } from "~/renderer/components/Input"; +import Label from "~/renderer/components/Label"; +import Box from "~/renderer/components/Box"; +import { useTranslation } from "react-i18next"; +import { Flex, Text, Tooltip } from "@ledgerhq/react-ui"; +import styled from "styled-components"; +import InfoCircle from "~/renderer/icons/InfoCircle"; + +const TooltipContainer = styled(Box)` + background-color: ${({ theme }) => theme.colors.palette.neutral.c100}; + padding: 10px; + border-radius: 4px; + display: flex; + gap: 8px; +`; + +const InstructionText = styled(Text)` + color: ${({ theme }) => theme.colors.primary.c80}; + margin-top: 6px; + font-size: 12px; + line-height: 16.8px; +`; + +type MemoTagFieldProps = InputBaseProps & { + maxMemoLength?: number; + showLabel?: boolean; + CaracterCountComponent?: React.FC; + autoFocus?: boolean; +}; + +const MemoTagField = ({ + warning, + error, + value, + onChange, + showLabel = true, + maxMemoLength, + CaracterCountComponent, + autoFocus, +}: MemoTagFieldProps) => { + const { t } = useTranslation(); + return ( + + {showLabel && ( + + )} + + {CaracterCountComponent && } + + {autoFocus && {t("MemoTagField.instruction")}} + + + ); +}; + +export default MemoTagField; diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagInfoBody.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagInfoBody.tsx index 6354db4e01d1..3365a44a3bdf 100644 --- a/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagInfoBody.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagInfoBody.tsx @@ -1,33 +1,19 @@ import React from "react"; -import { Link, Text } from "@ledgerhq/react-ui"; +import { Text } from "@ledgerhq/react-ui"; import { Trans } from "react-i18next"; -import { openURL } from "~/renderer/linking"; -import { MEMO_TAG_LEARN_MORE_LINK } from "../constants"; +import LearnMoreCta from "./LearnMoreCta"; +import { urls } from "~/config/urls"; -const MemoTagInfoBody = () => { - const handleOpenLMLink = () => openURL(MEMO_TAG_LEARN_MORE_LINK); - - return ( -
- - - - - -
- - - -
- ); -}; +const MemoTagInfoBody = () => ( +
+ + + + + +
+ +
+); export default MemoTagInfoBody; diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagSendInfo.tsx b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagSendInfo.tsx new file mode 100644 index 000000000000..c556afbd8258 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/components/MemoTagSendInfo.tsx @@ -0,0 +1,36 @@ +import { Flex, Icons } from "@ledgerhq/react-ui"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "styled-components"; +import LearnMoreCta from "LLD/features/MemoTag/components/LearnMoreCta"; +import { CircleWrapper } from "~/renderer/components/CryptoCurrencyIcon"; +import Text from "~/renderer/components/Text"; +import { urls } from "~/config/urls"; + +const MemoTagSendInfo = () => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + + + + + {t("send.info.needMemoTag.title")} + + + {t("send.info.needMemoTag.description")} + + } + url={urls.memoTag.learnMore} + /> + + + ); +}; + +export default MemoTagSendInfo; diff --git a/apps/ledger-live-desktop/src/newArch/features/MemoTag/constants.ts b/apps/ledger-live-desktop/src/newArch/features/MemoTag/constants.ts index 43d38061a4ee..77ccc2914a04 100644 --- a/apps/ledger-live-desktop/src/newArch/features/MemoTag/constants.ts +++ b/apps/ledger-live-desktop/src/newArch/features/MemoTag/constants.ts @@ -1,4 +1,3 @@ -export const MEMO_TAG_LEARN_MORE_LINK = "https://support.ledger.com/article/4409603715217-zd"; export const MEMO_TAG_COINS: string[] = [ "ripple", "stellar", @@ -11,4 +10,6 @@ export const MEMO_TAG_COINS: string[] = [ "ton", "eos", "bsc", + "casper", + "cardano", ]; diff --git a/apps/ledger-live-desktop/src/renderer/actions/UI.ts b/apps/ledger-live-desktop/src/renderer/actions/UI.ts index 4d9ee0e6b948..44c8d50ff94a 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/UI.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/UI.ts @@ -68,3 +68,17 @@ export const openPlatformAppDisclaimerDrawer = createAction( }), ); export const closePlatformAppDrawer = createAction("PLATFORM_APP_DRAWER_CLOSE"); + +export const setMemoTagInfoBoxDisplay = createAction( + "TOGGLE_MEMOTAG_DISPLAY", + ({ + isMemoTagBoxVisible, + forceAutoFocusOnMemoField, + }: { + isMemoTagBoxVisible: boolean; + forceAutoFocusOnMemoField?: boolean; + }) => ({ + isMemoTagBoxVisible, + forceAutoFocusOnMemoField, + }), +); diff --git a/apps/ledger-live-desktop/src/renderer/actions/application.ts b/apps/ledger-live-desktop/src/renderer/actions/application.ts index e86f70be0a54..c4f8331ac66c 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/application.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/application.ts @@ -34,3 +34,10 @@ export const toggleSkeletonVisibility = createAction( }, }), ); + +export const toggleShouldDisplayMemoTagInfo = createAction( + "APPLICATION_SET_DATA", + (alwaysShowMemoTagInfo: boolean) => ({ + alwaysShowMemoTagInfo, + }), +); diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoField.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoField.tsx similarity index 54% rename from apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoField.tsx rename to apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoField.tsx index 0c379939cfae..31ce9045d031 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoField.tsx @@ -1,7 +1,5 @@ import React from "react"; -import { useTranslation } from "react-i18next"; import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; import MemoValueField from "./MemoValueField"; import { CardanoAccount, @@ -16,19 +14,9 @@ const MemoField = (props: { onChange: (t: Transaction) => void; trackProperties?: Record; }) => { - const { t } = useTranslation(); return ( - - - - - - - - + ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoValueField.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoValueField.tsx similarity index 85% rename from apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoValueField.tsx rename to apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoValueField.tsx index 69072f358fa6..c68466a1c9d1 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/MemoValueField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/MemoValueField.tsx @@ -1,7 +1,5 @@ import React, { useCallback } from "react"; -import { useTranslation } from "react-i18next"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Input from "~/renderer/components/Input"; import invariant from "invariant"; import { CardanoAccount, @@ -9,6 +7,7 @@ import { TransactionStatus, } from "@ledgerhq/live-common/families/cardano/types"; import { track } from "~/renderer/analytics/segment"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; const MemoValueField = ({ onChange, @@ -16,14 +15,15 @@ const MemoValueField = ({ transaction, status, trackProperties, + autoFocus, }: { onChange: (a: Transaction) => void; account: CardanoAccount; transaction: Transaction; status: TransactionStatus; trackProperties?: Record; + autoFocus?: boolean; }) => { - const { t } = useTranslation(); invariant(transaction.family === "cardano", "Memo: cardano family expected"); const bridge = getAccountBridge(account); const onMemoValueChange = useCallback( @@ -42,10 +42,11 @@ const MemoValueField = ({ [trackProperties, onChange, bridge, transaction], ); return ( - ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/index.tsx b/apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/index.tsx similarity index 100% rename from apps/ledger-live-desktop/src/renderer/families/cardano/SendAmountFields/index.tsx rename to apps/ledger-live-desktop/src/renderer/families/cardano/SendRecipientFields/index.tsx diff --git a/apps/ledger-live-desktop/src/renderer/families/cardano/index.ts b/apps/ledger-live-desktop/src/renderer/families/cardano/index.ts index 08a93acd247a..438d6524716c 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cardano/index.ts +++ b/apps/ledger-live-desktop/src/renderer/families/cardano/index.ts @@ -1,6 +1,6 @@ import AccountBodyHeader from "./AccountBodyHeader"; import AccountSubHeader from "./AccountSubHeader"; -import sendAmountFields from "./SendAmountFields"; +import sendRecipientFields from "./SendRecipientFields"; import AccountBalanceSummaryFooter from "./AccountBalanceSummaryFooter"; import accountHeaderManageActions from "./AccountHeaderManageActions"; import { CardanoFamily } from "./types"; @@ -8,7 +8,7 @@ import { CardanoFamily } from "./types"; const family: CardanoFamily = { AccountBodyHeader, AccountSubHeader, - sendAmountFields, + sendRecipientFields, AccountBalanceSummaryFooter, accountHeaderManageActions, }; diff --git a/apps/ledger-live-desktop/src/renderer/families/cosmos/MemoValueField.tsx b/apps/ledger-live-desktop/src/renderer/families/cosmos/MemoValueField.tsx index 756f694248d6..b911d300873c 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cosmos/MemoValueField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cosmos/MemoValueField.tsx @@ -1,25 +1,25 @@ import React, { useCallback } from "react"; -import { useTranslation } from "react-i18next"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Input from "~/renderer/components/Input"; import invariant from "invariant"; import { CosmosAccount, Transaction, TransactionStatus, } from "@ledgerhq/live-common/families/cosmos/types"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; const MemoValueField = ({ onChange, account, transaction, status, + autoFocus, }: { onChange: (transaction: Transaction) => void; account: CosmosAccount; transaction: Transaction; status: TransactionStatus; + autoFocus?: boolean; }) => { - const { t } = useTranslation(); invariant(transaction.family === "cosmos", "MemoTypeField: cosmos family expected"); const bridge = getAccountBridge(account); const onMemoValueChange = useCallback( @@ -37,12 +37,12 @@ const MemoValueField = ({ // It will be usefull to block a memo wrong format // on the ledger-live mobile return ( - ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/cosmos/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/cosmos/SendRecipientFields.tsx index 8f9643e41390..26f2c15028e2 100644 --- a/apps/ledger-live-desktop/src/renderer/families/cosmos/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/cosmos/SendRecipientFields.tsx @@ -1,27 +1,12 @@ import React from "react"; -import { useTranslation } from "react-i18next"; import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; -import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip"; import MemoValueField from "./MemoValueField"; import { CosmosFamily } from "./types"; const Root: NonNullable["component"] = props => { - const { t } = useTranslation(); return ( - - - - - - - - + ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/crypto_org/MemoValueField.tsx b/apps/ledger-live-desktop/src/renderer/families/crypto_org/MemoValueField.tsx index 25e78a21fb23..4d7b3eb8d3f5 100644 --- a/apps/ledger-live-desktop/src/renderer/families/crypto_org/MemoValueField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/crypto_org/MemoValueField.tsx @@ -1,25 +1,25 @@ import React, { useCallback } from "react"; -import { useTranslation } from "react-i18next"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Input from "~/renderer/components/Input"; import { CryptoOrgAccount, Transaction, TransactionStatus, } from "@ledgerhq/live-common/families/crypto_org/types"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; const MemoValueField = ({ onChange, account, transaction, status, + autoFocus, }: { onChange: (t: Transaction) => void; account: CryptoOrgAccount; transaction: Transaction; status: TransactionStatus; + autoFocus?: boolean; }) => { - const { t } = useTranslation(); const bridge = getAccountBridge(account); const onMemoValueChange = useCallback( (memo: string) => { @@ -36,12 +36,12 @@ const MemoValueField = ({ // It will be usefull to block a memo wrong format // on the ledger-live mobile return ( - ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/crypto_org/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/crypto_org/SendRecipientFields.tsx index 51c8fe4e8b11..94e39c35b959 100644 --- a/apps/ledger-live-desktop/src/renderer/families/crypto_org/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/crypto_org/SendRecipientFields.tsx @@ -1,27 +1,12 @@ import React from "react"; -import { useTranslation } from "react-i18next"; import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; -import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip"; import MemoValueField from "./MemoValueField"; import { CryptoOrgFamily } from "./types"; const Root: NonNullable["component"] = props => { - const { t } = useTranslation(); return ( - - - - - - - - + ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/hedera/MemoField.tsx b/apps/ledger-live-desktop/src/renderer/families/hedera/MemoField.tsx index 1a79db03bd82..ce327bc6ddeb 100644 --- a/apps/ledger-live-desktop/src/renderer/families/hedera/MemoField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/hedera/MemoField.tsx @@ -1,20 +1,19 @@ import React, { useCallback } from "react"; -import { Trans, useTranslation } from "react-i18next"; +import { Trans } from "react-i18next"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import { track } from "~/renderer/analytics/segment"; import { SendAmountProps } from "./types"; -import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; -import Input from "~/renderer/components/Input"; import Text from "~/renderer/components/Text"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; + const MemoField = ({ account, transaction, onChange, status, trackProperties = {}, + autoFocus, }: SendAmountProps) => { - const { t } = useTranslation(); const MEMO_MAX_LENGTH = 100; const [memoLength, setMemoLength] = React.useState(0); const bridge = getAccountBridge(account); @@ -36,17 +35,11 @@ const MemoField = ({ ); if (!status) return null; return ( - - - - {/* memo character count */} + ( - - {/* memo input */} - - + )} + autoFocus={autoFocus} + /> ); }; export default MemoField; diff --git a/apps/ledger-live-desktop/src/renderer/families/hedera/SendAmountFields.tsx b/apps/ledger-live-desktop/src/renderer/families/hedera/SendRecipientFields.tsx similarity index 100% rename from apps/ledger-live-desktop/src/renderer/families/hedera/SendAmountFields.tsx rename to apps/ledger-live-desktop/src/renderer/families/hedera/SendRecipientFields.tsx diff --git a/apps/ledger-live-desktop/src/renderer/families/hedera/index.ts b/apps/ledger-live-desktop/src/renderer/families/hedera/index.ts index 29ff3b21cf43..e96e064b89c6 100644 --- a/apps/ledger-live-desktop/src/renderer/families/hedera/index.ts +++ b/apps/ledger-live-desktop/src/renderer/families/hedera/index.ts @@ -1,13 +1,13 @@ import AccountSubHeader from "./AccountSubHeader"; import NoAssociatedAccounts from "./NoAssociatedAccounts"; -import sendAmountFields from "./SendAmountFields"; +import sendRecipientFields from "./SendRecipientFields"; import StepReceiveFunds from "./StepReceiveFunds"; import getTransactionExplorer from "./getTransactionExplorer"; import { HederaFamily } from "./types"; const family: HederaFamily = { AccountSubHeader, - sendAmountFields, + sendRecipientFields, StepReceiveFunds, NoAssociatedAccounts, getTransactionExplorer, diff --git a/apps/ledger-live-desktop/src/renderer/families/hedera/types.ts b/apps/ledger-live-desktop/src/renderer/families/hedera/types.ts index b9d9b20c6ab0..d94767b5c091 100644 --- a/apps/ledger-live-desktop/src/renderer/families/hedera/types.ts +++ b/apps/ledger-live-desktop/src/renderer/families/hedera/types.ts @@ -11,4 +11,5 @@ export type SendAmountProps = { status: TransactionStatus; onChange: (a: Transaction) => void; trackProperties?: object; + autoFocus?: boolean; }; diff --git a/apps/ledger-live-desktop/src/renderer/families/internet_computer/MemoField.tsx b/apps/ledger-live-desktop/src/renderer/families/internet_computer/MemoField.tsx index 741b6672f19f..92552250656c 100644 --- a/apps/ledger-live-desktop/src/renderer/families/internet_computer/MemoField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/internet_computer/MemoField.tsx @@ -1,29 +1,28 @@ import React, { useCallback } from "react"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Input from "~/renderer/components/Input"; import invariant from "invariant"; import { Account } from "@ledgerhq/types-live"; import { Transaction, TransactionStatus, } from "@ledgerhq/live-common/families/internet_computer/types"; -import { useTranslation } from "react-i18next"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; const MemoField = ({ onChange, account, transaction, status, + autoFocus, }: { onChange: (a: Transaction) => void; account: Account; transaction: Transaction; status: TransactionStatus; + autoFocus?: boolean; }) => { invariant(transaction.family === "internet_computer", "Memo: Internet Computer family expected"); - const { t } = useTranslation(); - const bridge = getAccountBridge(account); const onMemoFieldChange = useCallback( @@ -37,13 +36,13 @@ const MemoField = ({ // We use transaction as an error here. // on the ledger-live mobile return ( - ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/internet_computer/SendAmountFields.tsx b/apps/ledger-live-desktop/src/renderer/families/internet_computer/SendAmountFields.tsx index faf929bf6218..1121538918e8 100644 --- a/apps/ledger-live-desktop/src/renderer/families/internet_computer/SendAmountFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/internet_computer/SendAmountFields.tsx @@ -1,9 +1,5 @@ import React from "react"; -import { Trans } from "react-i18next"; import MemoField from "./MemoField"; -import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; -import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip"; import { Transaction, TransactionStatus, @@ -17,29 +13,7 @@ const Root = (props: { onChange: (a: Transaction) => void; trackProperties?: object; }) => { - return ( - - - - - - - - - - - ); + return ; }; export default { diff --git a/apps/ledger-live-desktop/src/renderer/families/stacks/MemoValueField.tsx b/apps/ledger-live-desktop/src/renderer/families/stacks/MemoValueField.tsx index d5f9ec5ea208..44bc821e9428 100644 --- a/apps/ledger-live-desktop/src/renderer/families/stacks/MemoValueField.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/stacks/MemoValueField.tsx @@ -1,23 +1,21 @@ import React, { useCallback } from "react"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Input from "~/renderer/components/Input"; import invariant from "invariant"; import { Account } from "@ledgerhq/types-live"; import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/stacks/types"; -import { useTranslation } from "react-i18next"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; type Props = { onChange: (t: Transaction) => void; account: Account; transaction: Transaction; status: TransactionStatus; + autoFocus?: boolean; }; -const MemoValueField = ({ onChange, account, transaction, status }: Props) => { +const MemoValueField = ({ onChange, account, transaction, status, autoFocus }: Props) => { invariant(transaction.family === "stacks", "MemoField: stacks family expected"); - const { t } = useTranslation(); - const bridge = getAccountBridge(account); const onMemoValueChange = useCallback( @@ -31,12 +29,12 @@ const MemoValueField = ({ onChange, account, transaction, status }: Props) => { // It will be usefull to block a memo wrong format // on the ledger-live mobile return ( - ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/stacks/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/stacks/SendRecipientFields.tsx index bd46841b9ddb..ee83c658452f 100644 --- a/apps/ledger-live-desktop/src/renderer/families/stacks/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/stacks/SendRecipientFields.tsx @@ -1,9 +1,6 @@ import React from "react"; -import { Trans } from "react-i18next"; import MemoValueField from "./MemoValueField"; import Box from "~/renderer/components/Box"; -import Label from "~/renderer/components/Label"; -import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip"; import { Account } from "@ledgerhq/types-live"; import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/stacks/types"; @@ -17,20 +14,7 @@ type Props = { const Root = (props: Props) => { return ( - - - - - - - - + ); }; diff --git a/apps/ledger-live-desktop/src/renderer/families/xrp/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/xrp/SendRecipientFields.tsx index b198732eadb3..4ff917db0ec7 100644 --- a/apps/ledger-live-desktop/src/renderer/families/xrp/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/xrp/SendRecipientFields.tsx @@ -1,12 +1,9 @@ import React, { useCallback } from "react"; import { BigNumber } from "bignumber.js"; -import { Trans, useTranslation } from "react-i18next"; import { Account } from "@ledgerhq/types-live"; import { Transaction } from "@ledgerhq/live-common/families/xrp/types"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; -import Box from "~/renderer/components/Box"; -import Input from "~/renderer/components/Input"; -import Label from "~/renderer/components/Label"; +import MemoTagField from "LLD/features/MemoTag/components/MemoTagField"; type Props = { onChange: (a: Transaction) => void; transaction: Transaction; @@ -14,7 +11,6 @@ type Props = { }; const uint32maxPlus1 = BigNumber(2).pow(32); const TagField = ({ onChange, account, transaction }: Props) => { - const { t } = useTranslation(); const onChangeTag = useCallback( (str: string) => { const bridge = getAccountBridge(account); @@ -35,23 +31,7 @@ const TagField = ({ onChange, account, transaction }: Props) => { }, [onChange, account, transaction], ); - return ( - - - - - - - ); + return ; }; export default { component: TagField, diff --git a/apps/ledger-live-desktop/src/renderer/icons/MemoIcon.tsx b/apps/ledger-live-desktop/src/renderer/icons/MemoIcon.tsx new file mode 100644 index 000000000000..f0cd4e82b460 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/icons/MemoIcon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +const MemoIcon = ({ size = 18, color = "currentColor" }: { size?: number; color?: string }) => ( + + + + +); + +export default MemoIcon; diff --git a/apps/ledger-live-desktop/src/renderer/modals/Receive/steps/StepReceiveFunds.tsx b/apps/ledger-live-desktop/src/renderer/modals/Receive/steps/StepReceiveFunds.tsx index 55f74e5fde7d..621a79cc3931 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Receive/steps/StepReceiveFunds.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Receive/steps/StepReceiveFunds.tsx @@ -73,8 +73,7 @@ const Receive1ShareAddress = ({ const { currency } = account; const isUTXOCompliantCurrency = isUTXOCompliant(currency.family); - const shouldRenderMemoTagInfo = - currency.explorerId && MEMO_TAG_COINS.includes(currency.explorerId); + const shouldRenderMemoTagInfo = currency.family && MEMO_TAG_COINS.includes(currency.family); return ( <> diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/SendRecipientFields.tsx index bcaa39effbd6..8ce587da21b6 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/SendRecipientFields.tsx @@ -9,6 +9,7 @@ type Props = { transaction: Transaction; status: TransactionStatus; onChange: (a: Transaction) => void; + autoFocus?: boolean; }; export const getFields = (account: Account): string[] => { diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/index.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/index.tsx index ea2d721c214a..91007fa5bfad 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/index.tsx @@ -3,6 +3,8 @@ import { DomainServiceProvider } from "@ledgerhq/domain-service/hooks/index"; import Modal from "~/renderer/components/Modal"; import Body from "./Body"; import { StepId } from "./types"; +import { useDispatch } from "react-redux"; +import { setMemoTagInfoBoxDisplay } from "~/renderer/actions/UI"; type Props = { stepId?: StepId; @@ -23,13 +25,24 @@ const SendModal = ({ stepId: initialStepId, onClose }: Props) => { const handleReset = useCallback(() => setStep("recipient"), []); const handleStepChange = useCallback((stepId: StepId) => setStep(stepId), []); const isModalLocked = MODAL_LOCKED[stepId as StepId]; + const dispatch = useDispatch(); + + const handleModalClose = useCallback(() => { + dispatch( + setMemoTagInfoBoxDisplay({ + isMemoTagBoxVisible: false, + forceAutoFocusOnMemoField: false, + }), + ); + onClose?.(); + }, [dispatch, onClose]); return ( ( { + const isMemoTagBoxVisibile = useSelector(memoTagBoxVisibilitySelector); + const forceAutoFocusOnMemoField = useSelector(forceAutoFocusOnMemoFieldSelector); + if (!status || !account) return null; + const mainAccount = getMainAccount(account, parentAccount); // check if there is a stuck transaction. If so, display a warning panel with "speed up or cancel" button const stuckAccountAndOperation = getStuckAccountAndOperation(account, parentAccount); @@ -47,67 +63,105 @@ const StepRecipient = ({ currencyName={currencyName} isNFTSend={isNFTSend} /> - {mainAccount ? : null} - {error ? : null} - {isNFTSend ? ( - - - {account && ( - - )} - + {isMemoTagBoxVisibile ? ( + ) : ( - - - - - )} - {stuckAccountAndOperation ? ( - - ) : null} - - {account && transaction && mainAccount && ( <> - - + {mainAccount ? : null} + {error ? : null} + {isNFTSend ? ( + + + {account && ( + + )} + + ) : ( + + + + + )} + {stuckAccountAndOperation ? ( + + ) : null} + + {account && transaction && mainAccount && ( + <> + + + + )} )} ); }; -export class StepRecipientFooter extends PureComponent { - onNext = async () => { - const { transitionTo, shouldSkipAmount } = this.props; + +export const StepRecipientFooter = ({ + t, + account, + parentAccount, + status, + bridgePending, + transitionTo, + shouldSkipAmount, + transaction, +}: StepProps) => { + const dispatch = useDispatch(); + const { errors } = status; + const mainAccount = account ? getMainAccount(account, parentAccount) : null; + const isTerminated = mainAccount && mainAccount.currency.terminated; + const fields = ["recipient"].concat(mainAccount ? getFields(mainAccount) : []); + const hasFieldError = Object.keys(errors).some(name => fields.includes(name)); + const canNext = !bridgePending && !hasFieldError && !isTerminated; + const isMemoTagBoxVisibile = useSelector(memoTagBoxVisibilitySelector); + const alwaysShowMemoTagInfo = useSelector(alwaysShowMemoTagInfoSelector); + + const handleOnNext = async () => { + if ( + !transaction?.memo && + MEMO_TAG_COINS.includes(transaction?.family as string) && + alwaysShowMemoTagInfo + ) { + dispatch( + setMemoTagInfoBoxDisplay({ + isMemoTagBoxVisible: true, + }), + ); + return; + } if (shouldSkipAmount) { transitionTo("summary"); } else { @@ -115,28 +169,60 @@ export class StepRecipientFooter extends PureComponent { } }; - render() { - const { t, account, parentAccount, status, bridgePending } = this.props; - const { errors } = status; - const mainAccount = account ? getMainAccount(account, parentAccount) : null; - const isTerminated = mainAccount && mainAccount.currency.terminated; - const fields = ["recipient"].concat(mainAccount ? getFields(mainAccount) : []); - const hasFieldError = Object.keys(errors).some(name => fields.includes(name)); - const canNext = !bridgePending && !hasFieldError && !isTerminated; - return ( - <> - - - ); - } -} + + + + ) : ( + + ); +}; export default StepRecipient; diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepSummary.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepSummary.tsx index 3e8138ee4902..17490f4a6d96 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepSummary.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepSummary.tsx @@ -27,6 +27,8 @@ import AccountTagDerivationMode from "~/renderer/components/AccountTagDerivation import { getLLDCoinFamily } from "~/renderer/families"; import { useMaybeAccountUnit } from "~/renderer/hooks/useAccountUnit"; import { useMaybeAccountName } from "~/renderer/reducers/wallet"; +import MemoIcon from "~/renderer/icons/MemoIcon"; +import { Flex } from "@ledgerhq/react-ui"; const FromToWrapper = styled.div``; const Circle = styled.div` @@ -52,10 +54,12 @@ const Separator = styled.div` width: 100%; margin: 15px 0; `; + const WARN_FROM_UTXO_COUNT = 50; const StepSummary = (props: StepProps) => { - const { account, parentAccount, transaction, status, currencyName, isNFTSend } = props; + const { account, parentAccount, transaction, status, currencyName, isNFTSend, transitionTo } = + props; const mainAccount = account && getMainAccount(account, parentAccount); const unit = useMaybeAccountUnit(account); const accountName = useMaybeAccountName(account); @@ -80,6 +84,10 @@ const StepSummary = (props: StepProps) => { const memo = "memo" in transaction ? transaction.memo : undefined; + const handleOnEditMemo = () => { + transitionTo("recipient"); + }; + return ( { + {memo && ( + <> + + + + + + + + + + + + + {memo} + + + + + + + + )} - {memo && ( - - - - - - - {memo} - - - - )} {!isNFTSend ? ( diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/types.ts b/apps/ledger-live-desktop/src/renderer/modals/Send/types.ts index 8d96f1b3bfbf..5c1ed0aee44b 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/types.ts +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/types.ts @@ -12,7 +12,7 @@ export type StepProps = { device: Device | undefined | null; account: AccountLike | undefined | null; parentAccount: Account | undefined | null; - transaction: Transaction | undefined | null; + transaction: (Transaction & { memo?: string }) | undefined | null; status: TransactionStatus; bridgePending: boolean; error: Error | undefined | null; diff --git a/apps/ledger-live-desktop/src/renderer/reducers/UI.ts b/apps/ledger-live-desktop/src/renderer/reducers/UI.ts index 515f5bceee5f..b32f13e6247f 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/UI.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/UI.ts @@ -46,6 +46,8 @@ export type UIState = { isOpen: boolean; payload?: PlatformAppDrawers | null; }; + isMemoTagBoxVisible: boolean; + forceAutoFocusOnMemoField: boolean; }; const initialState: UIState = { @@ -57,12 +59,19 @@ const initialState: UIState = { isOpen: false, payload: undefined, }, + isMemoTagBoxVisible: false, + forceAutoFocusOnMemoField: false, }; type OpenPayload = { tabId?: string; }; +type ToggleMemoDisplayPayload = { + isMemoTagBoxVisible: boolean; + forceAutoFocusOnMemoField?: boolean; +}; + type HandlersPayloads = { INFORMATION_CENTER_OPEN: OpenPayload; INFORMATION_CENTER_SET_TAB: OpenPayload; @@ -70,6 +79,7 @@ type HandlersPayloads = { PLATFORM_APP_DRAWER_OPEN: PlatformAppDrawers; PLATFORM_APP_DRAWER_CLOSE: never; EXCHANGE_APP_DRAWER_OPEN: ExchangeAppDrawer; + TOGGLE_MEMOTAG_DISPLAY: ToggleMemoDisplayPayload; }; type UIHandlers = Handlers; @@ -131,6 +141,13 @@ const handlers: UIHandlers = { }, }; }, + TOGGLE_MEMOTAG_DISPLAY: (state, { payload }) => { + return { + ...state, + isMemoTagBoxVisible: payload.isMemoTagBoxVisible, + forceAutoFocusOnMemoField: !!payload?.forceAutoFocusOnMemoField, + }; + }, }; // Selectors @@ -140,7 +157,11 @@ export const informationCenterStateSelector = (state: State): UIState["informati state.UI.informationCenter; export const platformAppDrawerStateSelector = (state: State): UIState["platformAppDrawer"] => state.UI.platformAppDrawer; - +export const memoTagBoxVisibilitySelector = (state: State): UIState["isMemoTagBoxVisible"] => + state.UI.isMemoTagBoxVisible; +export const forceAutoFocusOnMemoFieldSelector = ( + state: State, +): UIState["forceAutoFocusOnMemoField"] => state.UI.forceAutoFocusOnMemoField; // Exporting reducer export default handleActions( diff --git a/apps/ledger-live-desktop/src/renderer/reducers/application.ts b/apps/ledger-live-desktop/src/renderer/reducers/application.ts index a670be897f3a..24fc319dd86c 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/application.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/application.ts @@ -12,6 +12,7 @@ export type ApplicationState = { debug: { alwaysShowSkeletons: boolean; }; + alwaysShowMemoTagInfo: boolean; }; const { language, region } = getParsedSystemLocale(); const osLangSupported = LanguageIds.includes(language); @@ -26,6 +27,7 @@ const state: ApplicationState = { debug: { alwaysShowSkeletons: false, }, + alwaysShowMemoTagInfo: true, }; type HandlersPayloads = { @@ -60,6 +62,8 @@ export const osLangAndRegionSelector = (state: { application: ApplicationState } state.application.osLanguage; export const isNavigationLocked = (state: { application: ApplicationState }) => state.application.navigationLocked; +export const alwaysShowMemoTagInfoSelector = (state: { application: ApplicationState }) => + state.application.alwaysShowMemoTagInfo; // Exporting reducer diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index f405f37a2ac6..0d43b9b3664d 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -148,7 +148,10 @@ "or": "OR", "clearAll": "Clear all", "buyLedger": "Buy a Ledger", - "quote": "Quote" + "quote": "Quote", + "memoTag": { + "learnMore": "Learn more about Tag/Memo" + } }, "devices": { "nanoS": "Nano S", @@ -2260,8 +2263,7 @@ }, "memoTag": { "title": "Need a Tag/Memo?", - "description": "You might need a <0>Tag/Memo for receiving this asset from an exchange. You can use any combination of numbers like 1234.", - "learnMore": "Learn more about Tag/Memo" + "description": "You might need a <0>Tag/Memo for receiving this asset from an exchange. You can use any combination of numbers like 1234." } }, "send": { @@ -2284,6 +2286,11 @@ "noResolution": { "title": "No address found for this domain" } + }, + "memoTag": { + "question": "Dont use Memo Tag?", + "learnMore": "Learn more about Tag/Memo", + "warning": " Your funds will get lost if you don’t add a Tag/Memo when sending them to any centralized exchange or custodial wallet." } }, "amount": { @@ -2361,6 +2368,15 @@ }, "footer": { "estimatedFees": "Network fees" + }, + "info":{ + "needMemoTag": { + "title": "Don’t use Tag/Memo?", + "description": "Your funds will get lost if you don’t add a Tag/Memo when sending them to any centralized exchange or custodial wallet.", + "cta.addTag": "Add Tag", + "cta.not.addTag": "Don’t add Tag", + "checkbox.label": "Do not show this again." + } } }, "releaseNotes": { @@ -5338,26 +5354,18 @@ "link": "Terms of Use", "cta": "Continue" }, + "MemoTagField": { + "label": "Tag / Memo", + "placeholder": "Enter Tag / Memo", + "information": "To ensure your transaction is deposited correctly to your account, centralized exchanges and sometimes custodial wallets require a Tag/Memo.", + "instruction": "Copy and paste the Tag/Memo provided by the exchange or custodial wallet." + }, "families": { "casper": { "transferIdPlaceholder": "Optional", "transferId": "Transfer ID (Memo)", "transferIdWarningText": "When using a memo, carefully check the information with the recipient" }, - "cardano": { - "memoPlaceholder": "Optional", - "memo": "Memo" - }, - "internet_computer": { - "memoPlaceholder": "Optional, Default: 0", - "memo": "Memo", - "memoWarningText": "The value of memo can be a number between 0 and 9223372036854775807" - }, - "stacks": { - "memoPlaceholder": "Optional", - "memo": "Memo", - "memoWarningText": "When using a Memo, carefully verify the type used with the recipient" - }, "ton": { "commentPlaceholder": "Optional", "comment": "Comment" @@ -5386,13 +5394,8 @@ "assetCode": "Asset code", "assetIssuer": "Asset issuer" }, - "cosmos": { - "memo": "Memo", - "memoPlaceholder": "Optional", - "memoWarningText": "When using a memo, carefully check the information with the recipient" - }, "solana": { - "memo": "Memo", + "memo": "Tag / Memo", "memoPlaceholder": "Optional" } }, @@ -6397,7 +6400,7 @@ }, "cryptoOrg": { "memo": "Memo", - "memoPlaceholder": "Optional", + "memoPlaceholder": "Enter Tag / Memo", "memoWarningText": "When using a memo, carefully check the information with the recipient" }, "hedera": {