diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 2e750a82979..49f84f86cca 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -85,6 +85,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-dropzone": "^14.2.9",
+ "react-error-boundary": "^4.1.2",
"react-hook-form": "7.52.0",
"react-intersection-observer": "^9.10.3",
"react-markdown": "^9.0.1",
diff --git a/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx
new file mode 100644
index 00000000000..1e3816e7700
--- /dev/null
+++ b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx
@@ -0,0 +1,44 @@
+import { CopyTextButton } from "@/components/ui/CopyTextButton";
+import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
+import { useMemo } from "react";
+
+export function UnexpectedValueErrorMessage(props: {
+ value: unknown;
+ title: string;
+ description: string;
+ className?: string;
+}) {
+ const stringifiedValue = useMemo(() => {
+ try {
+ return JSON.stringify(props.value, null, 2);
+ } catch {
+ return undefined;
+ }
+ }, [props.value]);
+
+ return (
+
+
{props.title}
+
{props.description}
+ {stringifiedValue && (
+
+
Value Received
+
+
+ {stringifiedValue}
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx
index 347fc967375..c11b32962d9 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/token-id.tsx
@@ -1,5 +1,6 @@
"use client";
+import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
@@ -53,6 +54,8 @@ interface TokenIdPageProps {
isErc721: boolean;
}
+// TODO: verify the entire nft object with zod schema and display an error message
+
export const TokenIdPage: React.FC = ({
contract,
tokenId,
@@ -132,15 +135,15 @@ export const TokenIdPage: React.FC = ({
height={isMobile ? "100%" : "300px"}
/>
+
-
- {nft.metadata.name}
+
+
{nft.metadata?.description && (
-
- {nft.metadata.description}
-
+
)}
+
= ({
);
};
+
+function NFTName(props: {
+ value: unknown;
+}) {
+ if (typeof props.value === "string") {
+ return (
+ {props.value}
+ );
+ }
+
+ return (
+
+ );
+}
+
+function NFTDescription(props: {
+ value: unknown;
+}) {
+ if (typeof props.value === "string") {
+ return {props.value}
;
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx
index c156f12dd97..c03c50a0a3c 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/table.tsx
@@ -1,7 +1,10 @@
"use client";
+import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
import { WalletAddress } from "@/components/blocks/wallet-address";
+import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { cn } from "@/lib/utils";
import {
Flex,
IconButton,
@@ -16,6 +19,7 @@ import {
Thead,
Tr,
} from "@chakra-ui/react";
+import * as Sentry from "@sentry/nextjs";
import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell";
import { useChainSlug } from "hooks/chains/chainSlug";
import {
@@ -26,6 +30,7 @@ import {
ChevronRightIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
+import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
import {
type CellProps,
type Column,
@@ -72,24 +77,51 @@ export const NFTGetAllTable: React.FC = ({
{
Header: "Name",
accessor: (row) => row.metadata.name,
- Cell: (cell: CellProps) => (
-
- {cell.value}
-
- ),
+ Cell: (cell: CellProps) => {
+ if (typeof cell.value !== "string") {
+ return (
+
+ );
+ }
+
+ return (
+
+ {cell.value}
+
+ );
+ },
},
{
Header: "Description",
accessor: (row) => row.metadata.description,
- Cell: (cell: CellProps) => (
-
- {cell.value || "No description"}
-
- ),
+ Cell: (cell: CellProps) => {
+ if (typeof cell.value !== "string") {
+ return (
+
+ );
+ }
+
+ return (
+
+ {cell.value || "No description"}
+
+ );
+ },
},
];
if (isErc721) {
@@ -280,18 +312,22 @@ export const NFTGetAllTable: React.FC = ({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={rowIndex}
>
- {row.cells.map((cell, cellIndex) => (
-
- {cell.render("Cell")}
- |
- ))}
+ {row.cells.map((cell, cellIndex) => {
+ return (
+
+
+ {cell.render("Cell")}
+
+ |
+ );
+ })}
{!failedToLoad && }
|
@@ -374,3 +410,25 @@ export const NFTGetAllTable: React.FC = ({
);
};
+
+function NFTCellErrorBoundary(errorProps: FallbackProps) {
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ Sentry.withScope((scope) => {
+ scope.setTag("component-crashed", "true");
+ scope.setTag("component-crashed-boundary", "NFTCellErrorBoundary");
+ scope.setLevel("fatal");
+ Sentry.captureException(errorProps.error);
+ });
+ }, [errorProps.error]);
+
+ return (
+
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8afc0b8f286..e4d9e8faf48 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -252,6 +252,9 @@ importers:
react-dropzone:
specifier: ^14.2.9
version: 14.2.9(react@18.3.1)
+ react-error-boundary:
+ specifier: ^4.1.2
+ version: 4.1.2(react@18.3.1)
react-hook-form:
specifier: 7.52.0
version: 7.52.0(react@18.3.1)
@@ -12836,6 +12839,11 @@ packages:
react: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
react-dom: ^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0
+ react-error-boundary@4.1.2:
+ resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==}
+ peerDependencies:
+ react: '>=16.13.1'
+
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
@@ -19041,7 +19049,7 @@ snapshots:
'@emotion/babel-plugin@11.12.0':
dependencies:
'@babel/helper-module-imports': 7.24.7
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@emotion/hash': 0.9.2
'@emotion/memoize': 0.9.0
'@emotion/serialize': 1.3.1
@@ -20210,7 +20218,7 @@ snapshots:
'@manypkg/find-root@1.1.0':
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@types/node': 12.20.55
find-up: 4.1.0
fs-extra: 8.1.0
@@ -20223,7 +20231,7 @@ snapshots:
'@manypkg/get-packages@1.1.3':
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0
fs-extra: 8.1.0
@@ -25685,7 +25693,7 @@ snapshots:
babel-plugin-macros@3.1.0:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
cosmiconfig: 7.1.0
resolve: 1.22.8
@@ -26946,7 +26954,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
csstype: 3.1.3
dom-serializer@0.2.2:
@@ -32443,6 +32451,11 @@ snapshots:
react-dom: 18.3.1(react@18.3.1)
react-is: 18.1.0
+ react-error-boundary@4.1.2(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.25.7
+ react: 18.3.1
+
react-fast-compare@3.2.2: {}
react-focus-lock@2.12.1(@types/react@18.3.11)(react@18.3.1):
@@ -32761,7 +32774,7 @@ snapshots:
react-select@5.8.0(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
'@emotion/cache': 11.13.1
'@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1)
'@floating-ui/dom': 1.6.5
@@ -32808,7 +32821,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.25.7
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1