Skip to content

Commit

Permalink
🥅(lld): rework ui of node erros on send flow
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey committed Oct 11, 2024
1 parent 053111d commit d870635
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 14 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useCallback } from "react";

export function useCopyToClipboard() {
const copyToClipboard = useCallback((value: string) => {
navigator.clipboard.writeText(value);
}, []);

return { copyToClipboard };
}
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,139 @@
import React, { useState } from "react";
import { Flex, 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 { createSendFlowError } from "@ledgerhq/live-common/errors/sendFlow";
import { urls } from "~/config/urls";
import { openURL } from "~/renderer/linking";
import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls";
import { useCopyToClipboard } from "~/newArch/hooks/useCopyToClipboard";
import { useTranslation } from "react-i18next";

type Props = {
error: Error;
currencyName: string;
networkName: string;
};

interface InteractFlexProps {
hasBgColor?: boolean;
}

const InteractFlex = styled(Flex)<InteractFlexProps>`
&:hover {
cursor: pointer;
}
padding: ${({ hasBgColor }) => (hasBgColor ? "8px" : "0")};
background-color: ${({ theme, hasBgColor = false }) =>
hasBgColor ? theme.colors.palette.opacityDefault.c05 : undefined};
border-radius: 8px;
column-gap: 8px;
`;

const NodeError = ({ currencyName, networkName, error }: Props) => {
const theme = useTheme();
const { t } = useTranslation();
const { handleExportLogs } = useExportLogs();
const { copyToClipboard } = useCopyToClipboard();

const customError = createSendFlowError(error);
const url = customError.url ?? urls.contactSupport;
const localizedUrl = useLocalizedUrl(url);

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

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

const onCopyError = () => copyToClipboard(error.message);

const onSaveLogs = () => handleExportLogs();

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

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

return (
<Flex justifyContent="center" alignItems="center" rowGap={32} flexDirection="column">
<Flex justifyContent="center" alignItems="center" rowGap={16} flexDirection="column">
<CircleWrapper size={72} color={color}>
<Icons.DeleteCircleFill size="L" color="error.c50" />
</CircleWrapper>
<Text marginX={40} variant="h3Inter" fontWeight="semiBold" fontSize={24} textAlign="center">
{t("nodeErrors.title")}
</Text>
<Text
marginX={40}
variant="bodyLineHeight"
fontSize={14}
textAlign="center"
color="neutral.c70"
>
{t("nodeErrors.description", { networkName, currencyName })}
</Text>
</Flex>
<Flex flexDirection="column" width={"100%"} rowGap={16}>
<Flex alignItems={"center"}>
<Text> {t("nodeErrors.needHelp")}</Text>
<InteractFlex onClick={onShowMore}>
{isShowMore ? (
<Icons.ChevronDown color="neutral.c100" />
) : (
<Icons.ChevronRight color="neutral.c100" />
)}
</InteractFlex>
</Flex>
{isShowMore && (
<Flex columnGap={1}>
<Flex
flex={0.5}
alignItems="flex-start"
flexDirection="column"
rowGap={2}
bg="opacityDefault.c05"
borderRadius={8}
justifyContent="space-between"
p={2}
>
<Text variant="bodyLineHeight" fontSize={13}>
{t("nodeErrors.helpCenterTitle")}
<Text color="neutral.c70">{t("nodeErrors.helpCenterDesc")}</Text>
</Text>
<InteractFlex hasBgColor onClick={onGetHelp}>
<Icons.Support color="neutral.c100" />
<Text> {t("nodeErrors.getHelp")}</Text>
</InteractFlex>
</Flex>
<Flex
flex={0.5}
alignItems="flex-start"
flexDirection="column"
bg="opacityDefault.c05"
borderRadius={8}
justifyContent="space-between"
p={2}
>
<Text variant="bodyLineHeight" fontSize={13} maxHeight={70} overflowX={"hidden"}>
{t("nodeErrors.technicalErrorTitle")}
<Text flex={1} color="neutral.c70">
{error.message}
</Text>
</Text>
<Flex columnGap={2}>
<InteractFlex hasBgColor onClick={onSaveLogs}>
<Icons.Download color="neutral.c100" />
<Text> {t("nodeErrors.saveLogs")}</Text>
</InteractFlex>
<InteractFlex hasBgColor onClick={onCopyError}>
<Icons.Copy color="neutral.c100" />
</InteractFlex>
</Flex>
</Flex>
</Flex>
)}
</Flex>
</Flex>
);
};

export default NodeError;
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ 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";

const Container = styled(Box).attrs(() => ({
alignItems: "center",
Expand Down Expand Up @@ -58,10 +57,12 @@ 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();
}
Expand All @@ -74,12 +75,12 @@ function StepConfirmation({
name="Step Confirmation Error"
currencyName={currencyName}
/>
{signed ? (
<BroadcastErrorDisclaimer
title={<Trans i18nKey="send.steps.confirmation.broadcastError" />}
/>
) : null}
<ErrorDisplay error={error} withExportLogs />

<NodeError
error={error}
currencyName={String(currencyName)}
networkName={String(mainAccount?.currency.name)}
/>
</Container>
);
}
Expand Down
18 changes: 14 additions & 4 deletions apps/ledger-live-desktop/static/i18n/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2255,10 +2255,10 @@
}
},
"memoTag": {
"title": "Need a Tag/Memo?",
"description": "You might need a <0>Tag/Memo</0> for receiving this asset from an exchange. You can use any combination of numbers like 1234.",
"learnMore": "Learn more about Tag/Memo"
}
"title": "Need a Tag/Memo?",
"description": "You might need a <0>Tag/Memo</0> for receiving this asset from an exchange. You can use any combination of numbers like 1234.",
"learnMore": "Learn more about Tag/Memo"
}
},
"send": {
"title": "Send",
Expand Down Expand Up @@ -6791,5 +6791,15 @@
}
}
}
},
"nodeErrors": {
"title": "Transaction Broadcast Unsuccesful",
"description": "Your transaction failed to broadcast on the {{networkName}} network. Your {{currencyName}} have not be transferred and remain in your account.",
"helpCenterTitle": "Help center : ",
"helpCenterDesc": "Visit our Help center or contact us via the widget for assistance.",
"needHelp": "Need help?",
"getHelp": "Get Help",
"technicalErrorTitle": "Technical error : ",
"saveLogs": "Save logs"
}
}
6 changes: 5 additions & 1 deletion libs/ledger-live-common/src/errors/sendFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export const TxUnderpriced = createCustomErrorClass<{ url: string }>("TxUnderpri
export const AlreadySpentUTXO = createCustomErrorClass<{ url: string }>("AlreadySpendUTXO");
export const TxnMempoolConflict = createCustomErrorClass<{ url: string }>("TxnMempoolConflict");

export const createSendFlowError = (error: Error): Error => {
interface CustomError extends Error {
url?: string;
}

export const createSendFlowError = (error: Error): CustomError => {
const { message } = error;

if (
Expand Down

0 comments on commit d870635

Please sign in to comment.