Skip to content

Commit

Permalink
feat: ✨ add memo tag support on send flow for LLD
Browse files Browse the repository at this point in the history
  • Loading branch information
themooneer committed Oct 30, 2024
1 parent f63c04f commit e7bf0e9
Show file tree
Hide file tree
Showing 39 changed files with 640 additions and 324 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-ants-end.md
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions apps/ledger-live-desktop/src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<LearnMoreCta url="example.domain.com" />);

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");
});
});
Original file line number Diff line number Diff line change
@@ -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(<MemoTagField showLabel={true} />);
expect(screen.getByText("MemoTagField.label")).toBeInTheDocument();
expect(screen.getByRole("textbox")).toBeInTheDocument();
expect(screen.getByPlaceholderText("MemoTagField.placeholder")).toBeInTheDocument();
});

it("should render MemoTagField without label", () => {
render(<MemoTagField showLabel={false} />);
expect(screen.queryByText("MemoTagField.label")).not.toBeInTheDocument();
});

it("should call onChange when input value changes", () => {
const handleChange = jest.fn();
render(<MemoTagField onChange={handleChange} />);
fireEvent.change(screen.getByPlaceholderText("MemoTagField.placeholder"), {
target: { value: "new memo" },
});
expect(handleChange).toHaveBeenCalledTimes(1);
});

it("should render CaracterCountComponent if provided", () => {
const CaracterCountComponent = () => <div>Character Count</div>;
render(<MemoTagField CaracterCountComponent={CaracterCountComponent} />);
expect(screen.getByText("Character Count")).toBeInTheDocument();
});

it("should render instruction text on autoFocus", () => {
render(<MemoTagField autoFocus />);
expect(screen.getByPlaceholderText("MemoTagField.placeholder")).toHaveFocus();
expect(screen.getByText("MemoTagField.instruction")).toBeInTheDocument();
});

it("should render with error", () => {
render(<MemoTagField error={new Error("Error message")} />);
expect(screen.getByTestId("input-error")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -16,15 +14,7 @@ describe("MemoTagInfoBody", () => {
render(<MemoTagInfoBody />);

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);
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<Link
size={size}
color={color}
onClick={handleOpenLMLink}
style={style}
{...(Icon && { Icon })}
>
<Trans i18nKey="common.memoTag.learnMore" />
</Link>
);
};

export default LearnMoreCta;
Original file line number Diff line number Diff line change
@@ -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 (
<Box flow={1}>
{showLabel && (
<Label>
<Flex>
<span>{t("MemoTagField.label")}</span>
&nbsp;&nbsp;
<Tooltip
placement="top"
content={<TooltipContainer>{t("MemoTagField.information")}</TooltipContainer>}
>
<InfoCircle size={16} />
</Tooltip>
</Flex>
</Label>
)}
<Flex flexDirection="column" justifyContent="center">
<Flex justifyContent="end">{CaracterCountComponent && <CaracterCountComponent />}</Flex>
<Input
placeholder={t("MemoTagField.placeholder")}
onChange={onChange}
warning={warning}
error={error}
value={value}
spellCheck="false"
ff="Inter"
maxMemoLength={maxMemoLength}
autoFocus={autoFocus}
/>
{autoFocus && <InstructionText>{t("MemoTagField.instruction")}</InstructionText>}
</Flex>
</Box>
);
};

export default MemoTagField;
Original file line number Diff line number Diff line change
@@ -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 (
<div data-testid="memo-tag-info-body">
<Text variant="paragraphLineHeight" color="neutral.c80" fontSize={13}>
<Trans i18nKey="receive.memoTag.description">
<Text variant="paragraphLineHeight" fontWeight="700" color="neutral.c90"></Text>
</Trans>
</Text>
<br />
<Link
size="small"
color={"neutral.c80"}
onClick={handleOpenLMLink}
textProps={{
fontSize: "13px",
}}
style={{ textDecoration: "underline" }}
>
<Trans i18nKey="receive.memoTag.learnMore" />
</Link>
</div>
);
};
const MemoTagInfoBody = () => (
<div data-testid="memo-tag-info-body">
<Text variant="paragraphLineHeight" color="neutral.c80" fontSize={13}>
<Trans i18nKey="receive.memoTag.description">
<Text variant="paragraphLineHeight" fontWeight="700" color="neutral.c90"></Text>
</Trans>
</Text>
<br />
<LearnMoreCta style={{ textDecoration: "underline" }} url={urls.memoTag.learnMore} />
</div>
);

export default MemoTagInfoBody;
Original file line number Diff line number Diff line change
@@ -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 (
<Flex justifyContent="center" alignItems="center" width="100%">
<Flex justifyContent="center" flexDirection="column" alignItems="center" width={343}>
<CircleWrapper color={theme.colors.palette.opacityDefault.c05} size={72}>
<Icons.InformationFill size="L" color="primary.c80" />
</CircleWrapper>
<Text fontSize={20} fontWeight={600} color="neutral.c100" mt={3}>
{t("send.info.needMemoTag.title")}
</Text>
<Text fontSize={13} fontWeight={400} color="neutral.c80" mt={4} textAlign="center">
{t("send.info.needMemoTag.description")}
</Text>
<LearnMoreCta
color={theme.colors.wallet}
style={{ fontSize: "13px", marginTop: 6 }}
Icon={() => <Icons.ExternalLink size="S" />}
url={urls.memoTag.learnMore}
/>
</Flex>
</Flex>
);
};

export default MemoTagSendInfo;
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -11,4 +10,6 @@ export const MEMO_TAG_COINS: string[] = [
"ton",
"eos",
"bsc",
"casper",
"cardano",
];
14 changes: 14 additions & 0 deletions apps/ledger-live-desktop/src/renderer/actions/UI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
);
7 changes: 7 additions & 0 deletions apps/ledger-live-desktop/src/renderer/actions/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ export const toggleSkeletonVisibility = createAction(
},
}),
);

export const toggleShouldDisplayMemoTagInfo = createAction(
"APPLICATION_SET_DATA",
(alwaysShowMemoTagInfo: boolean) => ({
alwaysShowMemoTagInfo,
}),
);
Loading

0 comments on commit e7bf0e9

Please sign in to comment.