From d870635f8200bb52333714fde150ba74ed6a9669 Mon Sep 17 00:00:00 2001 From: Lucas Werey Date: Fri, 11 Oct 2024 12:49:12 +0200 Subject: [PATCH] :goal_net:(lld): rework ui of node erros on send flow --- .changeset/happy-hotels-sin.md | 5 + .../src/newArch/hooks/useCopyToClipboard.ts | 9 ++ .../src/newArch/hooks/useExportLogs.ts | 61 ++++++++ .../steps/Confirmation/NodeError/index.tsx | 139 ++++++++++++++++++ .../modals/Send/steps/StepConfirmation.tsx | 19 +-- .../static/i18n/en/app.json | 18 ++- .../ledger-live-common/src/errors/sendFlow.ts | 6 +- 7 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 .changeset/happy-hotels-sin.md create mode 100644 apps/ledger-live-desktop/src/newArch/hooks/useCopyToClipboard.ts create mode 100644 apps/ledger-live-desktop/src/newArch/hooks/useExportLogs.ts create mode 100644 apps/ledger-live-desktop/src/renderer/modals/Send/steps/Confirmation/NodeError/index.tsx diff --git a/.changeset/happy-hotels-sin.md b/.changeset/happy-hotels-sin.md new file mode 100644 index 000000000000..78a35833e122 --- /dev/null +++ b/.changeset/happy-hotels-sin.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Change node errors UI on send flow when a tx failed diff --git a/apps/ledger-live-desktop/src/newArch/hooks/useCopyToClipboard.ts b/apps/ledger-live-desktop/src/newArch/hooks/useCopyToClipboard.ts new file mode 100644 index 000000000000..d62a16067138 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/hooks/useCopyToClipboard.ts @@ -0,0 +1,9 @@ +import { useCallback } from "react"; + +export function useCopyToClipboard() { + const copyToClipboard = useCallback((value: string) => { + navigator.clipboard.writeText(value); + }, []); + + return { copyToClipboard }; +} diff --git a/apps/ledger-live-desktop/src/newArch/hooks/useExportLogs.ts b/apps/ledger-live-desktop/src/newArch/hooks/useExportLogs.ts new file mode 100644 index 000000000000..e7af04cce7e2 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/hooks/useExportLogs.ts @@ -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 }; +} diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/Confirmation/NodeError/index.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/Confirmation/NodeError/index.tsx new file mode 100644 index 000000000000..94b071ee9b9c --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/Confirmation/NodeError/index.tsx @@ -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)` + &: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 ( + + + + + + + {t("nodeErrors.title")} + + + {t("nodeErrors.description", { networkName, currencyName })} + + + + + {t("nodeErrors.needHelp")} + + {isShowMore ? ( + + ) : ( + + )} + + + {isShowMore && ( + + + + {t("nodeErrors.helpCenterTitle")} + {t("nodeErrors.helpCenterDesc")} + + + + {t("nodeErrors.getHelp")} + + + + + {t("nodeErrors.technicalErrorTitle")} + + {error.message} + + + + + + {t("nodeErrors.saveLogs")} + + + + + + + + )} + + + ); +}; + +export default NodeError; diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx index b94bf389b9c1..035270ec6fb9 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx @@ -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", @@ -58,10 +57,12 @@ function StepConfirmation({ ); } + + 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(); } @@ -74,12 +75,12 @@ function StepConfirmation({ name="Step Confirmation Error" currencyName={currencyName} /> - {signed ? ( - } - /> - ) : null} - + + ); } diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index fa027947bb6b..694bf3dc4108 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -2255,10 +2255,10 @@ } }, "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" - } + "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" + } }, "send": { "title": "Send", @@ -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" } } diff --git a/libs/ledger-live-common/src/errors/sendFlow.ts b/libs/ledger-live-common/src/errors/sendFlow.ts index 323f68f1b671..7ce4b9897f80 100644 --- a/libs/ledger-live-common/src/errors/sendFlow.ts +++ b/libs/ledger-live-common/src/errors/sendFlow.ts @@ -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 (