Skip to content

Commit

Permalink
fix: token list exception (#927)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonheys committed Jul 11, 2022
1 parent 90848ed commit 52f0090
Show file tree
Hide file tree
Showing 16 changed files with 516 additions and 114 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"scripts": {
"format": "prettier --loglevel warn --write \"**/*.{js,jsx,ts,tsx,css,md,yml,json}\"",
"dev": "lerna run dev --parallel",
"dev:ui": "lerna run dev:ui --parallel",
"clean": "rm -rf packages/extension/dist packages/get-starket/dist",
"build": "lerna run build --stream",
"build:sourcemaps": "GEN_SOURCE_MAPS=true lerna run build",
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"typescript": "^4.6.3",
"typescript-styled-plugin": "^0.18.2",
"url-loader": "^4.1.1",
"vi-fetch": "^0.7.1",
"vitest": "^0.17.0",
"wait-for-expect": "^3.0.2",
"webpack": "^5.62.1",
Expand All @@ -47,6 +48,7 @@
"build:sourcemaps": "NODE_ENV=production GEN_SOURCE_MAPS=true webpack",
"start": "webpack",
"dev": "webpack --color --watch",
"dev:ui": "SHOW_DEV_UI=true webpack --color --watch",
"lint": "eslint . --cache --ext .ts,.tsx",
"test": "vitest run",
"pretest:e2e": "playwright install chromium && ([ -d dist ] || yarn build)",
Expand Down
41 changes: 39 additions & 2 deletions packages/extension/src/shared/api/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,47 @@ export type Fetcher = (
init?: RequestInit,
) => Promise<any>

export interface FetcherError extends Error {
url?: string
status?: number
statusText?: string
responseText?: string
}

export const fetcherError = (
message: string,
response: Response,
responseText: string,
) => {
const error: FetcherError = new Error(message)
error.url = response.url
error.status = response.status
error.statusText = response.statusText
error.responseText = responseText
return error
}

export const fetcher = async (input: RequestInfo | URL, init?: RequestInit) => {
const response = await fetch(input, init)
const json = await response.json()
return json
/** capture text here in the case of json parse failure we can include it in the error */
const responseText = await response.text()
if (!response.ok) {
throw fetcherError(
"An error occurred while fetching",
response,
responseText,
)
}
try {
const json = JSON.parse(responseText)
return json
} catch (parseError) {
throw fetcherError(
"An error occurred while parsing",
response,
responseText,
)
}
}

export const fetcherWithArgentApiHeadersForNetwork = (
Expand Down
9 changes: 9 additions & 0 deletions packages/extension/src/shared/utils/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Promise that will resolve after interval milliseconds
* @param ms - Delay in milliseconds
* @returns Promise that will resolve after the delay
*/

export const delay = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms))
}
33 changes: 33 additions & 0 deletions packages/extension/src/shared/utils/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const UNKNOWN_ERROR_MESSAGE = "Unknown error"

export const coerceErrorToString = (
error: any,
includeStack = true,
): string => {
const errorObject = getErrorObject(error, includeStack)
if (errorObject) {
return JSON.stringify(errorObject, null, 2)
}
const errorString = error?.toString?.() || UNKNOWN_ERROR_MESSAGE
return errorString
}

/** Convert an Error to an object with keys and values by introspecting keys */
/** Conditionally allow tests to exclude the stack trace which is environment-specific */

export const getErrorObject = (error: any, includeStack = true) => {
try {
if (typeof error === "object" && error !== null) {
const keys = Object.getOwnPropertyNames(error).filter((key) =>
includeStack ? true : key !== "stack",
)
const errorObject: Record<string, string> = {}
keys.forEach((key) => {
errorObject[key] = error[key]
})
return errorObject
}
} catch (e) {
// ignore parsing error
}
}
37 changes: 21 additions & 16 deletions packages/extension/src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,34 @@ import { AppRoutes } from "./AppRoutes"
import { ErrorBoundary } from "./components/ErrorBoundary"
import { LoadingScreen } from "./features/actions/LoadingScreen"
import { useExtensionIsInTab } from "./features/browser/tabs"
import DevUI from "./features/dev/DevUI"
import { useTracking } from "./services/analytics"
import SoftReloadProvider from "./services/resetAndReload"
import { swrCacheProvider } from "./services/swr"
import { GlobalStyle, theme } from "./theme"

export const App: FC = () => {
useTracking()
const extensionIsInTab = useExtensionIsInTab()
return (
<SWRConfig value={{ provider: () => swrCacheProvider }}>
<ThemeProvider theme={theme}>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700;900&display=swap"
rel="stylesheet"
/>
<GlobalStyle extensionIsInTab={extensionIsInTab} />
<ErrorBoundary fallback={<AppErrorBoundaryFallback />}>
<Suspense fallback={<LoadingScreen />}>
<AppRoutes />
</Suspense>
</ErrorBoundary>
</ThemeProvider>
</SWRConfig>
<SoftReloadProvider>
<SWRConfig value={{ provider: () => swrCacheProvider }}>
<ThemeProvider theme={theme}>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600;700;900&display=swap"
rel="stylesheet"
/>
<GlobalStyle extensionIsInTab={extensionIsInTab} />
{process.env.SHOW_DEV_UI && <DevUI />}
<ErrorBoundary fallback={<AppErrorBoundaryFallback />}>
<Suspense fallback={<LoadingScreen />}>
<AppRoutes />
</Suspense>
</ErrorBoundary>
</ThemeProvider>
</SWRConfig>
</SoftReloadProvider>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { FC, useCallback, useMemo } from "react"
import { FC, useMemo } from "react"
import styled from "styled-components"
import browser from "webextension-polyfill"
import { coerceErrorToString } from "../../shared/utils/error"

import { useBackupRequired } from "../features/recovery/backupDownload.state"
import { useHardResetAndReload } from "../services/resetAndReload"
import { CopyTooltip } from "./CopyTooltip"
import { ErrorBoundaryState } from "./ErrorBoundary"
import {
Expand Down Expand Up @@ -75,71 +75,44 @@ export interface IErrorBoundaryFallbackWithCopyError
message?: string
}

export const coerceErrorToString = (error: any): string => {
let errorString = error?.toString?.() || "Unknown error"
// sometimes error.toString() may return "[object Object]", attempt to stringify as a fallback
if (errorString === "[object Object]") {
try {
errorString = JSON.stringify(error, null, 2)
} catch {
// ignore attempt to stringify the error object
}
}
return errorString
}

const ErrorBoundaryFallbackWithCopyError: FC<
IErrorBoundaryFallbackWithCopyError
> = ({ error, errorInfo, message = "Sorry, an error occurred" }) => {
const errorPayload = useMemo(() => {
try {
const displayError = coerceErrorToString(error)
const displayStack = errorInfo.componentStack || "No stack trace"
return `v${version}
const ErrorBoundaryFallbackWithCopyError: FC<IErrorBoundaryFallbackWithCopyError> =
({ error, errorInfo, message = "Sorry, an error occurred" }) => {
const hardResetAndReload = useHardResetAndReload()
const errorPayload = useMemo(() => {
try {
const displayError = coerceErrorToString(error)
const displayStack = errorInfo?.componentStack || "No stack trace"
return `v${version}
${displayError}
${displayStack}
`
} catch (e) {
// ignore error
}
return fallbackErrorPayload
}, [error, errorInfo])

const onReload = useCallback(() => {
const url = browser.runtime.getURL("index.html")

// reset cache
const backupState = useBackupRequired.getState()
localStorage.clear()
useBackupRequired.setState(backupState)

setTimeout(() => {
// ensure state got persisted before reloading
window.location.href = url
}, 100)
}, [])

return (
<MessageContainer>
<ErrorIcon />
<ErrorMessageContainer>
<P>{message}</P>
</ErrorMessageContainer>
<ActionsWrapper>
<ActionContainer onClick={onReload}>
<RefreshIcon />
<span>Retry</span>
</ActionContainer>
<CopyTooltip message="Copied" copyValue={errorPayload}>
<ActionContainer>
<ContentCopyIcon />
<span>Copy error details</span>
} catch (e) {
// ignore error
}
return fallbackErrorPayload
}, [error, errorInfo])

return (
<MessageContainer>
<ErrorIcon />
<ErrorMessageContainer>
<P>{message}</P>
</ErrorMessageContainer>
<ActionsWrapper>
<ActionContainer onClick={hardResetAndReload}>
<RefreshIcon />
<span>Retry</span>
</ActionContainer>
</CopyTooltip>
</ActionsWrapper>
</MessageContainer>
)
}
<CopyTooltip message="Copied" copyValue={errorPayload}>
<ActionContainer>
<ContentCopyIcon />
<span>Copy error details</span>
</ActionContainer>
</CopyTooltip>
</ActionsWrapper>
</MessageContainer>
)
}

export default ErrorBoundaryFallbackWithCopyError
35 changes: 24 additions & 11 deletions packages/extension/src/ui/features/accountTokens/AccountTokens.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, Suspense, useEffect, useRef } from "react"
import { FC, useEffect, useRef } from "react"
import { Link, useNavigate } from "react-router-dom"
import styled from "styled-components"
import useSWR from "swr"
Expand Down Expand Up @@ -75,7 +75,8 @@ export const AccountTokens: FC<AccountTokensProps> = ({ account }) => {
{ suspense: false },
)

const { isValidating, tokenDetails } = useTokensWithBalance(account)
const { isValidating, error, tokenDetails, tokenDetailsIsInitialising } =
useTokensWithBalance(account)

const { data: needsUpgrade = false, mutate } = useSWR(
[
Expand Down Expand Up @@ -131,29 +132,41 @@ export const AccountTokens: FC<AccountTokensProps> = ({ account }) => {
)}
{showNoBalanceForUpgrade && <UpgradeBanner canNotPay />}
<PendingTransactions account={account} />
{/** TODO: remove this extra error boundary once TokenList issues are settled */}
<ErrorBoundary
fallback={
<ErrorBoundaryFallbackWithCopyError
message={"Sorry, an error occurred fetching tokens"}
/>
}
>
<Suspense fallback={<Spinner size={64} style={{ marginTop: 40 }} />}>
{error ? (
<ErrorBoundaryFallbackWithCopyError
error={error}
message={"Sorry, an error occurred fetching tokens"}
/>
) : (
<>
<TokenList
showTitle={showPendingTransactions}
variant={tokenListVariant}
isValidating={isValidating}
tokenList={tokenDetails}
variant={tokenListVariant}
/>
<TokenWrapper {...makeClickable(() => navigate(routes.newToken()))}>
<AddTokenIconButton size={40}>
<AddIcon />
</AddTokenIconButton>
<TokenTitle>Add token</TokenTitle>
</TokenWrapper>
{tokenDetailsIsInitialising ? (
<Spinner size={64} style={{ marginTop: 40 }} />
) : (
<TokenWrapper
{...makeClickable(() => navigate(routes.newToken()))}
>
<AddTokenIconButton size={40}>
<AddIcon />
</AddTokenIconButton>
<TokenTitle>Add token</TokenTitle>
</TokenWrapper>
)}
</>
</Suspense>
)}
</ErrorBoundary>
</Container>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const TokenList: FC<TokenListProps> = ({
navigateToSend = false,
}) => {
if (!tokenList) {
return <></>
return null
}

return (
Expand Down
Loading

0 comments on commit 52f0090

Please sign in to comment.