diff --git a/package.json b/package.json
index 93ac62264..a45cb926d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 4074ff879..e398a4191 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -47,6 +47,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)",
diff --git a/packages/extension/src/ui/App.tsx b/packages/extension/src/ui/App.tsx
index a75eb345e..646c89ea5 100644
--- a/packages/extension/src/ui/App.tsx
+++ b/packages/extension/src/ui/App.tsx
@@ -7,6 +7,7 @@ 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 { swrCacheProvider } from "./services/swr"
import { GlobalStyle, theme } from "./theme"
@@ -24,6 +25,7 @@ export const App: FC = () => {
rel="stylesheet"
/>
+ {process.env.SHOW_DEV_UI && }
}>
}>
diff --git a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx
index aa4b645d2..668c9ad1b 100644
--- a/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx
+++ b/packages/extension/src/ui/features/accountTokens/AccountTokens.tsx
@@ -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"
@@ -8,7 +8,6 @@ import {
isDeprecated,
} from "../../../shared/wallet.service"
import { useAppState } from "../../app.state"
-import { ErrorBoundary } from "../../components/ErrorBoundary"
import ErrorBoundaryFallbackWithCopyError from "../../components/ErrorBoundaryFallbackWithCopyError"
import { IconButton } from "../../components/IconButton"
import { AddIcon } from "../../components/Icons/MuiIcons"
@@ -75,7 +74,8 @@ export const AccountTokens: FC = ({ account }) => {
{ suspense: false },
)
- const { isValidating, tokenDetails } = useTokensWithBalance(account)
+ const { isValidating, error, tokenDetails, tokenDetailsIsInitialising } =
+ useTokensWithBalance(account)
const { data: needsUpgrade = false, mutate } = useSWR(
[
@@ -131,30 +131,31 @@ export const AccountTokens: FC = ({ account }) => {
)}
{showNoBalanceForUpgrade && }
-
+ ) : (
+ <>
+
- }
- >
- }>
- <>
-
+ {tokenDetailsIsInitialising ? (
+
+ ) : (
navigate(routes.newToken()))}>
Add token
- >
-
-
+ )}
+ >
+ )}
)
}
diff --git a/packages/extension/src/ui/features/accountTokens/TokenList.tsx b/packages/extension/src/ui/features/accountTokens/TokenList.tsx
index 39ad8ff1a..e8086a783 100644
--- a/packages/extension/src/ui/features/accountTokens/TokenList.tsx
+++ b/packages/extension/src/ui/features/accountTokens/TokenList.tsx
@@ -24,7 +24,7 @@ export const TokenList: FC = ({
navigateToSend = false,
}) => {
if (!tokenList) {
- return <>>
+ return null
}
return (
diff --git a/packages/extension/src/ui/features/accountTokens/tokens.state.ts b/packages/extension/src/ui/features/accountTokens/tokens.state.ts
index a9a63595e..a925f8291 100644
--- a/packages/extension/src/ui/features/accountTokens/tokens.state.ts
+++ b/packages/extension/src/ui/features/accountTokens/tokens.state.ts
@@ -3,6 +3,7 @@ import { useEffect, useMemo } from "react"
import { number } from "starknet"
import useSWR from "swr"
import create from "zustand"
+import shallow from "zustand/shallow"
import { messageStream } from "../../../shared/messages"
import { Token, equalToken } from "../../../shared/token"
@@ -106,6 +107,7 @@ export const selectTokensByNetwork = (networkId: string) => (state: State) =>
interface UseTokens {
tokenDetails: TokenDetailsWithBalance[]
+ tokenDetailsIsInitialising: boolean
isValidating: boolean
error?: any
}
@@ -114,15 +116,28 @@ export const useTokensWithBalance = (
account?: BaseWalletAccount,
): UseTokens => {
const selectedAccount = useAccount(account)
- const tokensInNetwork = useTokens(
- selectTokensByNetwork(selectedAccount?.networkId ?? ""),
- )
- const tokenAddresses = useMemo(
- () => tokensInNetwork.map((t) => t.address),
- [tokensInNetwork],
- )
- const { data, isValidating, error, mutate } = useSWR(
+ const tokensInNetworkSelector = useMemo(() => {
+ return selectTokensByNetwork(selectedAccount?.networkId ?? "")
+ }, [selectedAccount?.networkId])
+
+ const tokenAddressesSelector = useMemo(() => {
+ return (state: State) => {
+ const tokensInNetwork = tokensInNetworkSelector(state)
+ return tokensInNetwork.map((t) => t.address)
+ }
+ }, [tokensInNetworkSelector])
+
+ // shallow compare objects and arrays
+ const tokensInNetwork = useTokens(tokensInNetworkSelector, shallow)
+ const tokenAddresses = useTokens(tokenAddressesSelector, shallow)
+
+ const {
+ data,
+ isValidating,
+ error: rawError,
+ mutate,
+ } = useSWR(
// skip if no account selected
selectedAccount && [
getAccountIdentifier(selectedAccount),
@@ -130,7 +145,7 @@ export const useTokensWithBalance = (
],
async () => {
if (!selectedAccount) {
- return {}
+ return
}
const balances = await fetchAllTokensBalance(
@@ -138,17 +153,45 @@ export const useTokensWithBalance = (
selectedAccount,
)
- return balances ?? {}
+ return balances
},
{
- suspense: true,
refreshInterval: 30000,
},
)
- // refetch balances on transaction success or token edit (token was added or removed)
+ /**
+ * FIXME:
+ * Investigate what causes the SWR hook above to cache an empty object `error: {}`, usually observed after reloading the extension
+ *
+ * This is subsequently retreived by SWR from the cache and causes an immediately defined error if left unchecked
+ *
+ * You can verify this by debugging in the `set` method of `swrCacheProvider`, and
+ * checking for SWR setting a value containing a key of `error` with an empty object {}
+ *
+ * As a workaround we check for empty object here and treat as undefined while the hook revalidates properly
+ *
+ */
+
+ const error: any = useMemo(() => {
+ if (!rawError) {
+ return
+ }
+ try {
+ if (JSON.stringify(rawError) === "{}") {
+ console.warn("FIXME: Ignoring empty object {} error")
+ return
+ }
+ } catch (e) {
+ // ignore any stringify errors
+ }
+ return rawError
+ }, [rawError])
+
+ const tokenDetailsIsInitialising = !error && !data && isValidating
+
+ // refetch balances on transaction success
useEffect(() => {
- mutate()
const subscription = messageStream.subscribe(([msg]) => {
if (msg.type === "TRANSACTION_SUCCESS") {
mutate() // refetch balances
@@ -159,7 +202,12 @@ export const useTokensWithBalance = (
subscription.unsubscribe()
}
}
- }, [tokenAddresses.join(":")])
+ }, [mutate])
+
+ // refetch balances on token edit (token was added or removed)
+ useEffect(() => {
+ mutate()
+ }, [mutate, tokenAddresses])
const tokenDetails = useMemo(() => {
return tokensInNetwork
@@ -170,7 +218,12 @@ export const useTokensWithBalance = (
.filter(
(token) => token.showAlways || (token.balance && token.balance.gt(0)),
)
- }, [tokenAddresses, data])
-
- return { tokenDetails, isValidating, error }
+ }, [tokensInNetwork, data])
+
+ return {
+ tokenDetails,
+ tokenDetailsIsInitialising,
+ isValidating,
+ error,
+ }
}
diff --git a/packages/extension/src/ui/features/dev/DevUI.tsx b/packages/extension/src/ui/features/dev/DevUI.tsx
new file mode 100644
index 000000000..8582b19e3
--- /dev/null
+++ b/packages/extension/src/ui/features/dev/DevUI.tsx
@@ -0,0 +1,46 @@
+import { FC, useCallback } from "react"
+import styled from "styled-components"
+import browser from "webextension-polyfill"
+
+import { RowCentered } from "../../components/Row"
+import { useBackupRequired } from "../recovery/backupDownload.state"
+
+const Container = styled(RowCentered)`
+ position: fixed;
+ z-index: 123;
+`
+
+const DevButton = styled.div`
+ background-color: rgba(255, 255, 255, 0.15);
+ padding: 4px 8px;
+ cursor: pointer;
+ border-radius: 500px;
+ margin-top: 4px;
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.25);
+ }
+`
+
+const DevUI: FC = () => {
+ const reset = useCallback(() => {
+ // reset cache
+ const backupState = useBackupRequired.getState()
+ localStorage.clear()
+ useBackupRequired.setState(backupState)
+ }, [])
+ const onReload = useCallback(() => {
+ const url = browser.runtime.getURL("index.html")
+ setTimeout(() => {
+ // ensure state got persisted before reloading
+ window.location.href = url
+ }, 100)
+ }, [])
+ return (
+
+ Reset cache
+ Reload
+
+ )
+}
+
+export default DevUI