Skip to content

Commit

Permalink
🥅(lld): rework ui of node erros on send flow (#8075)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey authored Oct 21, 2024
1 parent 17732b4 commit 77846a0
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-hotels-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": patch
---

Change node errors UI on send flow when a tx failed
19 changes: 19 additions & 0 deletions apps/ledger-live-desktop/src/newArch/hooks/useCopyToClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useCallback, useRef } from "react";

export function useCopyToClipboard(callback?: (text: string) => void) {
const textRef = useRef<string>();

const copy = useCallback(() => {
const text = textRef.current ?? "";
navigator.clipboard.writeText(text).then(() => {
if (callback) {
callback(text);
}
});
}, [callback]);

return (text: string) => {
textRef.current = text;
copy();
};
}
61 changes: 61 additions & 0 deletions apps/ledger-live-desktop/src/newArch/hooks/useExportLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import getUser from "~/helpers/user";
import { getAllEnvs } from "@ledgerhq/live-env";
import { webFrame, ipcRenderer } from "electron";
import { useCallback, useState } from "react";
import { useTechnicalDateTimeFn } from "~/renderer/hooks/useDateFormatter";
import logger, { memoryLogger } from "~/renderer/logger";
import { useSelector } from "react-redux";
import { accountsSelector } from "~/renderer/reducers/accounts";

export function useExportLogs() {
const getDateTxt = useTechnicalDateTimeFn();
const accounts = useSelector(accountsSelector);
const [exporting, setExporting] = useState(false);

const saveLogs = useCallback(async (path: Electron.SaveDialogReturnValue) => {
try {
const memoryLogsStr = JSON.stringify(memoryLogger.getMemoryLogs(), null, 2);
await ipcRenderer.invoke("save-logs", path, memoryLogsStr);
} catch (error) {
console.error("Error while requesting to save logs from the renderer process", error);
}
}, []);

const exportLogs = useCallback(async () => {
try {
const resourceUsage = webFrame.getResourceUsage();
const user = await getUser();
logger.log("exportLogsMeta", {
resourceUsage,
release: __APP_VERSION__,
git_commit: __GIT_REVISION__,
environment: __DEV__ ? "development" : "production",
userAgent: window.navigator.userAgent,
userAnonymousId: user.id,
env: getAllEnvs(),
accountsIds: accounts.map(a => a.id),
});

const path = await ipcRenderer.invoke("show-save-dialog", {
title: "Export logs",
defaultPath: `ledgerlive-logs-${getDateTxt()}-${__GIT_REVISION__ || "unversioned"}.json`,
filters: [{ name: "All Files", extensions: ["json"] }],
});

if (path) {
await saveLogs(path);
}
} catch (error) {
logger.critical(error as Error);
}
}, [accounts, getDateTxt, saveLogs]);

const handleExportLogs = useCallback(async () => {
if (exporting) return;
setExporting(true);
await exportLogs();
setExporting(false);
}, [exporting, exportLogs]);

return { handleExportLogs };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";
import { Flex, Text, Icons } from "@ledgerhq/react-ui";
import { useTranslation } from "react-i18next";
import styled from "styled-components";

type Props = {
onGetHelp: () => void;
};

const InteractFlex = styled(Flex)`
&:hover {
cursor: pointer;
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c10};
}
padding: 8px;
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c05};
border-radius: 8px;
column-gap: 8px;
align-items: center;
&:active {
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c20};
}
`;

const StyledText = styled(Text)`
word-break: break-all;
`;

const HelpSection = ({ onGetHelp }: Props) => {
const { t } = useTranslation();
return (
<Flex
alignItems="start"
flexDirection="column"
rowGap={2}
bg="opacityDefault.c05"
borderRadius={8}
justifyContent="space-between"
p={2}
minHeight="128px"
flex={1}
>
<Text variant="bodyLineHeight" fontSize={13}>
{t("errors.TransactionBroadcastError.helpCenterTitle")}
<Text color="neutral.c70">{t("errors.TransactionBroadcastError.helpCenterDesc")}</Text>
</Text>
<InteractFlex onClick={onGetHelp}>
<Icons.Support color="neutral.c100" size="S" />
<StyledText variant="bodyLineHeight" fontSize={13}>
{t("errors.TransactionBroadcastError.getHelp")}
</StyledText>
</InteractFlex>
</Flex>
);
};

export default HelpSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useState } from "react";
import { Flex, Text, Icons } from "@ledgerhq/react-ui";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { useCopyToClipboard } from "~/newArch/hooks/useCopyToClipboard";

type Props = {
error: Error;
onSaveLogs: () => void;
};

const InteractFlex = styled(Flex)`
&:hover {
cursor: pointer;
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c10};
}
padding: 8px;
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c05};
border-radius: 8px;
column-gap: 8px;
align-items: center;
&:active {
background-color: ${({ theme }) => theme.colors.palette.opacityDefault.c20};
}
`;

const StyledText = styled(Text)`
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
`;

const TechnicalErrorSection = ({ error, onSaveLogs }: Props) => {
const { t } = useTranslation();
const [icon, setIcon] = useState(<Icons.Copy color="neutral.c100" size="S" />);

const copyToClipboard = useCopyToClipboard(_text => {
setIcon(<Icons.Check color="success.c50" size="S" />);
setTimeout(() => {
setIcon(<Icons.Copy color="neutral.c100" size="S" />);
}, 5000);
});

const handleCopyError = () => {
copyToClipboard(error.message);
};

return (
<Flex
alignItems="flex-start"
flexDirection="column"
bg="opacityDefault.c05"
borderRadius={8}
justifyContent="space-between"
p={2}
>
<StyledText variant="bodyLineHeight" fontSize={13} width="100%">
{t("errors.TransactionBroadcastError.technicalErrorTitle")}
<Text color="neutral.c70">{error.message}</Text>
</StyledText>
<Flex columnGap={2}>
<InteractFlex onClick={onSaveLogs}>
<Icons.Download color="neutral.c100" size="S" />
<Text variant="bodyLineHeight" fontSize={13}>
{t("errors.TransactionBroadcastError.saveLogs")}
</Text>
</InteractFlex>
<InteractFlex onClick={handleCopyError}>{icon}</InteractFlex>
</Flex>
</Flex>
);
};

export default TechnicalErrorSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from "react";
import { Flex, Grid, Icons, Text } from "@ledgerhq/react-ui";
import styled, { useTheme } from "styled-components";
import { CircleWrapper } from "~/renderer/components/CryptoCurrencyIcon";
import { useExportLogs } from "LLD/hooks/useExportLogs";
import { TransactionBroadcastError } from "@ledgerhq/live-common/errors/transactionBroadcastErrors";
import { urls } from "~/config/urls";
import { openURL } from "~/renderer/linking";
import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls";
import { useTranslation } from "react-i18next";
import TechnicalErrorSection from "./TechnicalErrorSection";
import HelpSection from "./HelpSection";
import TranslatedError from "~/renderer/components/TranslatedError";
import { ErrorBody } from "~/renderer/components/ErrorBody";

type Props = {
error: TransactionBroadcastError;
};

const InteractFlex = styled(Flex)`
&:hover {
cursor: pointer;
}
`;

const NodeError: React.FC<Props> = ({ error }) => {
const theme = useTheme();
const { t } = useTranslation();
const { handleExportLogs } = useExportLogs();

const { coin: currencyName, network: networkName, url } = error;
const supportUrl = url ?? urls.contactSupport;
const localizedSupportUrl = useLocalizedUrl(supportUrl);

const [isShowMore, setIsShowMore] = useState(true);

const color = theme.colors.palette.opacityDefault.c05;

const onSaveLogs = () => handleExportLogs();

const onGetHelp = () => openURL(localizedSupportUrl);

const onShowMore = () => setIsShowMore(!isShowMore);

return (
<Flex
justifyContent="center"
alignItems="center"
width={"90%"}
rowGap={32}
flexDirection="column"
>
<Flex justifyContent="center" flexDirection="column">
<ErrorBody
title={
<Text variant="h3Inter" fontSize={24} textAlign="center">
<TranslatedError error={error} />
</Text>
}
description={
<Text variant="bodyLineHeight" fontSize={14} textAlign="center" color="neutral.c70">
{t("errors.TransactionBroadcastError.description", {
networkName,
currencyName,
})}
</Text>
}
top={
<CircleWrapper size={72} color={color}>
<Icons.DeleteCircleFill size="L" color="error.c50" />
</CircleWrapper>
}
/>
</Flex>
<Flex flexDirection="column" rowGap={16} width="100%" alignItems="flex-start">
<InteractFlex alignItems="center" onClick={onShowMore}>
<Text variant="bodyLineHeight" fontSize={13}>
{t("errors.TransactionBroadcastError.needHelp")}
</Text>

{isShowMore ? (
<Icons.ChevronDown color="neutral.c100" size="S" />
) : (
<Icons.ChevronRight color="neutral.c100" size="S" />
)}
</InteractFlex>
{isShowMore && (
<Grid columns={2} columnGap="8px" width="100%">
<HelpSection onGetHelp={onGetHelp} />
<TechnicalErrorSection error={error} onSaveLogs={onSaveLogs} />
</Grid>
)}
</Flex>
</Flex>
);
};

export default NodeError;
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import { Trans } from "react-i18next";
import styled from "styled-components";
import TrackPage from "~/renderer/analytics/TrackPage";
import Box from "~/renderer/components/Box";
import BroadcastErrorDisclaimer from "~/renderer/components/BroadcastErrorDisclaimer";
import Button from "~/renderer/components/Button";
import ErrorDisplay from "~/renderer/components/ErrorDisplay";
import RetryButton from "~/renderer/components/RetryButton";
import SuccessDisplay from "~/renderer/components/SuccessDisplay";
import { OperationDetails } from "~/renderer/drawers/OperationDetails";
import { setDrawer } from "~/renderer/drawers/Provider";
import { multiline } from "~/renderer/styles/helpers";
import { StepProps } from "../types";
import NodeError from "./Confirmation/NodeError";
import ErrorDisplay from "~/renderer/components/ErrorDisplay";
import { AccountLike } from "@ledgerhq/types-live";
import { createTransactionBroadcastError } from "@ledgerhq/live-common/errors/transactionBroadcastErrors";

const Container = styled(Box).attrs(() => ({
alignItems: "center",
Expand Down Expand Up @@ -58,15 +60,26 @@ function StepConfirmation({
</Container>
);
}

const mainAccount = account ? getMainAccount(account, parentAccount) : null;

if (error) {
// Edit ethereum transaction nonce error because transaction has been validated
if (error.name === "LedgerAPI4xx" && error.message.includes("nonce too low")) {
const mainAccount = account ? getMainAccount(account, parentAccount) : null;
if (mainAccount?.currency?.family === "evm") {
error = new TransactionHasBeenValidatedError();
}
}

const getTicker = (account: AccountLike): string => {
if (account.type === "TokenAccount") {
return account.token.ticker;
}
return account.currency.ticker;
};

const ticker = getTicker(account as AccountLike);

return (
<Container shouldSpace={signed}>
<TrackPage
Expand All @@ -75,11 +88,15 @@ function StepConfirmation({
currencyName={currencyName}
/>
{signed ? (
<BroadcastErrorDisclaimer
title={<Trans i18nKey="send.steps.confirmation.broadcastError" />}
<NodeError
error={createTransactionBroadcastError(error, {
coin: ticker,
network: String(mainAccount?.currency.name),
})}
/>
) : null}
<ErrorDisplay error={error} withExportLogs />
) : (
<ErrorDisplay error={error} withExportLogs />
)}
</Container>
);
}
Expand Down
Loading

0 comments on commit 77846a0

Please sign in to comment.