From 5f11ae0cd4cb70c878e7c2081ec75b9a2c1a7fec Mon Sep 17 00:00:00 2001 From: MananTank Date: Tue, 14 Oct 2025 18:37:48 +0000 Subject: [PATCH] Dashboard: Show Purchase data in bridge tx page, add tx status card in project>bridge page (#8251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR primarily focuses on enhancing the transaction status viewing capabilities in the application. It introduces new components and modifies existing ones to improve user experience and functionality related to transaction data. ### Detailed summary - Added `ViewTxStatus` component for viewing transaction status. - Updated `Page` component to include `ViewTxStatus` and adjust layout. - Improved error handling for transaction retrieval. - Introduced `WithPurchaseData` story for `BridgeStatus`. - Enhanced `BridgeStatus` to display purchase data if available. - Modified `RouteDiscovery` to streamline form submission and error handling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **New Features** * Added a “View Transaction Status” tool with chain selector, hash validation, and link to results in a new tab. * Transaction page shows a “Purchase Data” section with formatted JSON when available. * **Bug Fixes** * More graceful handling for missing/invalid transactions/receipts with an inline “Transaction not found” message. * **UI** * Reworked Route Discovery into an inline form with clearer error display and updated spacing. * **Tests** * Added a Storybook variant showcasing purchase data on the Completed status. --- .../tx/[txHash]/bridge-status.stories.tsx | 14 ++++ .../[chain_id]/tx/[txHash]/bridge-status.tsx | 42 ++++++++-- .../(chain)/[chain_id]/tx/[txHash]/page.tsx | 18 ++-- .../(sidebar)/bridge/RouteDiscovery.tsx | 56 ++++++++----- .../(sidebar)/bridge/RouteDiscoveryCard.tsx | 84 ------------------- .../[project_slug]/(sidebar)/bridge/page.tsx | 19 +++-- .../(sidebar)/bridge/view-tx-status.tsx | 72 ++++++++++++++++ 7 files changed, 184 insertions(+), 121 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscoveryCard.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/view-tx-status.tsx diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.stories.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.stories.tsx index 5fd50ea3e1e..190d9a22520 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.stories.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.stories.tsx @@ -104,3 +104,17 @@ export const NotFound: Story = { client: storybookThirdwebClient, }, }; + +export const WithPurchaseData: Story = { + args: { + bridgeStatus: { + ...completedStatus, + purchaseData: { + userId: "68d645b7ded999651272bf1e", + credits: 32000, + transactionId: "fd2606d1-90df-45c6-bd2c-b19a34764a31", + }, + }, + client: storybookThirdwebClient, + }, +}; diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.tsx index 5d4ed8b5cd4..ac220c4b6fa 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/bridge-status.tsx @@ -1,10 +1,11 @@ "use client"; import { useQuery } from "@tanstack/react-query"; +import { CodeClient } from "@workspace/ui/components/code/code.client"; import { Img } from "@workspace/ui/components/img"; import { Spinner } from "@workspace/ui/components/spinner"; import { ArrowRightIcon, CircleCheckIcon, CircleXIcon } from "lucide-react"; import Link from "next/link"; -import type { ThirdwebClient } from "thirdweb"; +import { NATIVE_TOKEN_ADDRESS, type ThirdwebClient } from "thirdweb"; import type { Status, Token } from "thirdweb/bridge"; import { status } from "thirdweb/bridge"; import { toTokens } from "thirdweb/utils"; @@ -15,11 +16,17 @@ import { cn } from "@/lib/utils"; import { fetchChain } from "@/utils/fetchChain"; import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler"; +type PurchaseData = Exclude["purchaseData"]; + export function BridgeStatus(props: { bridgeStatus: Status; client: ThirdwebClient; }) { const { bridgeStatus } = props; + const purchaseDataString = + bridgeStatus.status !== "NOT_FOUND" && bridgeStatus.purchaseData + ? getPurchaseData(bridgeStatus.purchaseData) + : undefined; return (
@@ -39,7 +46,7 @@ export function BridgeStatus(props: { /> )} -
+

Status

@@ -80,6 +87,18 @@ export function BridgeStatus(props: { />
+ + {purchaseDataString && ( +
+

Purchase Data

+ +
+ )}
); } @@ -96,7 +115,7 @@ function TokenInfo(props: { const chainQuery = useChainQuery(props.token.chainId); return ( -
+

{props.label} @@ -107,7 +126,12 @@ function TokenInfo(props: {
+

Transactions

@@ -370,3 +394,11 @@ export function BridgeStatusWithPolling(props: { /> ); } + +function getPurchaseData(purchaseData: PurchaseData) { + try { + return JSON.stringify(purchaseData, null, 2); + } catch { + return undefined; + } +} diff --git a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx index b67d14b1c66..85c94ea603e 100644 --- a/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx +++ b/apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx @@ -8,7 +8,6 @@ import { Clock4Icon, InfoIcon, } from "lucide-react"; -import { notFound } from "next/navigation"; import { toTokens } from "thirdweb"; import { status } from "thirdweb/bridge"; import type { ChainMetadata } from "thirdweb/chains"; @@ -55,10 +54,10 @@ export default async function Page(props: { const [transaction, receipt, bridgeStatus] = await Promise.all([ eth_getTransactionByHash(rpcRequest, { hash: params.txHash, - }), + }).catch(() => undefined), eth_getTransactionReceipt(rpcRequest, { hash: params.txHash, - }), + }).catch(() => undefined), status({ chainId: chain.chainId, transactionHash: params.txHash, @@ -66,8 +65,17 @@ export default async function Page(props: { }).catch(() => undefined), ]); - if (!transaction.blockHash) { - notFound(); + if (!transaction?.blockHash || !receipt) { + return ( +
+
+
+ +
+ Transaction not found +
+
+ ); } const block = await eth_getBlockByHash(rpcRequest, { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscovery.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscovery.tsx index 2a2be2c15a5..521e867939c 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscovery.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscovery.tsx @@ -2,6 +2,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@tanstack/react-query"; import type { ProjectResponse } from "@thirdweb-dev/service-utils"; +import { Button } from "@workspace/ui/components/button"; +import { Spinner } from "@workspace/ui/components/spinner"; +import { PlusIcon } from "lucide-react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type { ThirdwebClient } from "thirdweb"; @@ -19,7 +22,6 @@ import { type RouteDiscoveryValidationSchema, routeDiscoveryValidationSchema, } from "@/schema/validations"; -import { RouteDiscoveryCard } from "./RouteDiscoveryCard"; export const RouteDiscovery = ({ project, @@ -77,33 +79,21 @@ export const RouteDiscovery = ({ }, ); + const errorText = form.getFieldState("tokenAddress").error?.message; + return (
- -
-

+
+
+

Add a token to Bridge

-

+

Select your chain and input the token address to automatically kick-off the token route discovery process.
This may take up to 20-40 minutes to complete.

-
- +
+
+ {errorText ? ( +

{errorText}

+ ) : ( +
+ )} + + +
+
+
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscoveryCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscoveryCard.tsx deleted file mode 100644 index 9a3cf057ad9..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/RouteDiscoveryCard.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type React from "react"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/Spinner"; -import { cn } from "@/lib/utils"; - -export function RouteDiscoveryCard( - props: React.PropsWithChildren<{ - bottomText: React.ReactNode; - header?: { - description: string | undefined; - title: string; - }; - errorText: string | undefined; - noPermissionText: string | undefined; - saveButton?: { - onClick?: () => void; - disabled: boolean; - isPending: boolean; - type?: "submit"; - variant?: - | "ghost" - | "default" - | "primary" - | "destructive" - | "outline" - | "secondary"; - className?: string; - label?: string; - }; - }>, -) { - return ( -
-
- {props.header && ( - <> -

- {props.header.title} -

- {props.header.description && ( -

- {props.header.description} -

- )} - - )} - - {props.children} -
- -
- {props.noPermissionText ? ( -

- {props.noPermissionText} -

- ) : props.errorText ? ( -

{props.errorText}

- ) : ( -

{props.bottomText}

- )} - - {props.saveButton && !props.noPermissionText && ( - - )} -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/page.tsx index 4c6f13ff4a8..9a1d6f99911 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/page.tsx @@ -11,6 +11,7 @@ import { PayAnalytics } from "../payments/components/PayAnalytics"; import { getUniversalBridgeFiltersFromSearchParams } from "../payments/components/time"; import { QuickStartSection } from "./QuickstartSection.client"; import { RouteDiscovery } from "./RouteDiscovery"; +import { ViewTxStatus } from "./view-tx-status"; export default async function Page(props: { params: Promise<{ @@ -84,7 +85,7 @@ export default async function Page(props: { ], }} > -
+
- + + +
+ +
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/view-tx-status.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/view-tx-status.tsx new file mode 100644 index 00000000000..07925e9fd2c --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/bridge/view-tx-status.tsx @@ -0,0 +1,72 @@ +"use client"; +import { Button } from "@workspace/ui/components/button"; +import { ArrowUpRightIcon } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function ViewTxStatus(props: { client: ThirdwebClient }) { + const [txHash, setTxHash] = useState(""); + const [chainId, setChainId] = useState(1); + + const isValidTxHash = txHash.length === 66 && txHash.startsWith("0x"); + const isEnabled = !!txHash && !!chainId && isValidTxHash; + + return ( +
+
+

+ View transaction status +

+

+ Select chain and enter the transaction hash to view the transaction + status. +

+ +
+
+ + +
+
+ + setTxHash(e.target.value)} + placeholder="0x.." + /> + {txHash && !isValidTxHash && ( +

+ Invalid transaction hash +

+ )} +
+
+
+ +
+ +
+
+ ); +}