Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,11 +16,17 @@ import { cn } from "@/lib/utils";
import { fetchChain } from "@/utils/fetchChain";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";

type PurchaseData = Exclude<Status, { status: "NOT_FOUND" }>["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 (
<div className="bg-card rounded-xl border relative">
Expand All @@ -39,7 +46,7 @@ export function BridgeStatus(props: {
/>
)}

<div className="px-6 lg:px-10 py-7 space-y-1.5">
<div className="px-6 lg:px-8 py-7 space-y-1.5">
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground "> Status </p>

Expand Down Expand Up @@ -80,6 +87,18 @@ export function BridgeStatus(props: {
/>
</div>
</div>

{purchaseDataString && (
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
<p className="text-sm text-muted-foreground ">Purchase Data</p>
<CodeClient
code={purchaseDataString}
lang="json"
className="[&_code]:text-xs"
scrollableClassName="p-3"
/>
</div>
)}
</div>
);
}
Expand All @@ -96,7 +115,7 @@ function TokenInfo(props: {
const chainQuery = useChainQuery(props.token.chainId);

return (
<div className="flex-1 pt-10 pb-9 px-6 lg:px-10">
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
<div className="flex justify-between items-center">
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
{props.label}
Expand All @@ -107,7 +126,12 @@ function TokenInfo(props: {
<div className="flex items-center gap-3">
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
<Link
href={`/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`}
href={
props.token.address.toLowerCase() ===
NATIVE_TOKEN_ADDRESS.toLowerCase()
? `/${chainQuery.data?.slug || props.token.chainId}`
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
}
target="_blank"
aria-label="View Token"
className="absolute inset-0 z-10"
Expand Down Expand Up @@ -263,7 +287,7 @@ function FailedBridgeStatusContent(props: {
client: ThirdwebClient;
}) {
return (
<div className="px-6 lg:px-10 py-7 space-y-1.5 border-b border-dashed">
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
<h3 className="text-base font-medium tracking-tight mb-3">
Transactions
</h3>
Expand Down Expand Up @@ -370,3 +394,11 @@ export function BridgeStatusWithPolling(props: {
/>
);
}

function getPurchaseData(purchaseData: PurchaseData) {
try {
return JSON.stringify(purchaseData, null, 2);
} catch {
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -55,19 +54,28 @@ 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,
client: serverThirdwebClient,
}).catch(() => undefined),
]);

if (!transaction.blockHash) {
notFound();
if (!transaction?.blockHash || !receipt) {
return (
<div className="flex flex-col items-center justify-center grow">
<div className="flex flex-col items-center justify-center gap-3">
<div className="p-2 rounded-full bg-background border">
<CircleAlertIcon className="size-5 text-muted-foreground" />
</div>
Transaction not found
</div>
</div>
);
}
Comment on lines +68 to 79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pending transactions will incorrectly show as "not found."

The condition !transaction?.blockHash || !receipt catches pending transactions (where blockHash is null but the transaction exists) and displays "Transaction not found," which is misleading.

Consider one of these approaches:

  • Check for pending state explicitly and show "Transaction pending" UI
  • Add a loading state while polling for confirmation
  • Distinguish between truly not-found vs. pending scenarios

Example differentiation:

- if (!transaction?.blockHash || !receipt) {
+ if (!transaction) {
    return (
      <div className="flex flex-col items-center justify-center grow">
        <div className="flex flex-col items-center justify-center gap-3">
          <div className="p-2 rounded-full bg-background border">
            <CircleAlertIcon className="size-5 text-muted-foreground" />
          </div>
          Transaction not found
        </div>
      </div>
    );
+ }
+ 
+ if (!transaction.blockHash || !receipt) {
+   return (
+     <div className="flex flex-col items-center justify-center grow">
+       <div className="flex flex-col items-center justify-center gap-3">
+         <div className="p-2 rounded-full bg-background border">
+           <Clock4Icon className="size-5 text-muted-foreground" />
+         </div>
+         Transaction pending confirmation
+       </div>
+     </div>
+   );
  }

Optional: Add semantic HTML for accessibility.

The plain text "Transaction not found" lacks semantic structure. Consider wrapping in a heading or paragraph tag with appropriate ARIA attributes for better screen reader support.

Example:

  <div className="flex flex-col items-center justify-center gap-3">
    <div className="p-2 rounded-full bg-background border">
      <CircleAlertIcon className="size-5 text-muted-foreground" />
    </div>
-   Transaction not found
+   <p className="text-center">Transaction not found</p>
  </div>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/tx/[txHash]/page.tsx
around lines 68-79, the current guard treats transactions with no blockHash as
"not found," which incorrectly labels pending transactions; update the logic to
distinguish three states: not found (transaction null/undefined), pending
(transaction exists but blockHash is null and receipt is null), and confirmed
(transaction has blockHash and receipt exists). Render a clear pending UI (or
loading spinner) when pending, render the existing "not found" UI only when
transaction is truly missing, and keep the confirmed path unchanged; also wrap
the message text in semantic HTML (e.g., an h2 or p with appropriate ARIA role)
to improve accessibility.


const block = await eth_getBlockByHash(rpcRequest, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,7 +22,6 @@ import {
type RouteDiscoveryValidationSchema,
routeDiscoveryValidationSchema,
} from "@/schema/validations";
import { RouteDiscoveryCard } from "./RouteDiscoveryCard";

export const RouteDiscovery = ({
project,
Expand Down Expand Up @@ -77,33 +79,21 @@ export const RouteDiscovery = ({
},
);

const errorText = form.getFieldState("tokenAddress").error?.message;

return (
<Form {...form}>
<form autoComplete="off" onSubmit={handleSubmit}>
<RouteDiscoveryCard
bottomText=""
errorText={form.getFieldState("tokenAddress").error?.message}
noPermissionText={undefined}
saveButton={
// Only show the submit button in the default state
{
disabled: !form.formState.isDirty,
isPending: submitDiscoveryMutation.isPending,
type: "submit",
variant: "outline",
}
}
>
<div>
<h3 className="font-semibold text-xl tracking-tight">
<div className="relative rounded-lg border border-border bg-card">
<div className="relative border-dashed border-b px-4 py-6 lg:px-6">
<h3 className="font-semibold text-xl tracking-tight mb-1">
Add a token to Bridge
</h3>
<p className="mt-1.5 mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
<p className="mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
Select your chain and input the token address to automatically
kick-off the token route discovery process. <br /> This may take
up to 20-40 minutes to complete.
</p>

<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
<FormField
control={form.control}
Expand Down Expand Up @@ -142,7 +132,33 @@ export const RouteDiscovery = ({
/>
</div>
</div>
</RouteDiscoveryCard>
<div>
<div className="flex items-center justify-between gap-2 px-4 py-4 lg:px-6">
{errorText ? (
<p className="text-destructive-text text-sm">{errorText}</p>
) : (
<div />
)}

<Button
className="gap-1.5 rounded-full"
disabled={
!form.formState.isDirty || submitDiscoveryMutation.isPending
}
size="sm"
type="submit"
variant={"outline"}
>
{submitDiscoveryMutation.isPending ? (
<Spinner className="size-3" />
) : (
<PlusIcon className="size-4 text-muted-foreground" />
)}
Add Token
</Button>
</div>
</div>
</div>
</form>
</Form>
);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -84,7 +85,7 @@ export default async function Page(props: {
],
}}
>
<div className="flex flex-col gap-12">
<div className="flex flex-col gap-6">
<ResponsiveSearchParamsProvider value={searchParams}>
<PayAnalytics
client={client}
Expand All @@ -99,12 +100,16 @@ export default async function Page(props: {

<RouteDiscovery client={client} project={project} />

<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
<ViewTxStatus client={client} />

<div className="pt-4">
<QuickStartSection
projectSlug={params.project_slug}
teamSlug={params.team_slug}
clientId={project.publishableKey}
teamId={project.teamId}
/>
</div>
</div>
</ProjectPage>
);
Expand Down
Loading
Loading