-
Notifications
You must be signed in to change notification settings - Fork 539
[Dashboard] Add token request route to dashboard #7112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { Spinner } from "@/components/ui/Spinner/Spinner"; | ||
import { Button } from "@/components/ui/button"; | ||
import { cn } from "@/lib/utils"; | ||
import type React from "react"; | ||
|
||
export function RouteDiscoveryCard( | ||
props: React.PropsWithChildren<{ | ||
bottomText: React.ReactNode; | ||
header?: { | ||
description: string | undefined; | ||
title: string; | ||
}; | ||
errorText: string | undefined; | ||
noPermissionText: string | undefined; | ||
saveButton?: { | ||
Joe-Thirdweb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onClick?: () => void; | ||
disabled: boolean; | ||
isPending: boolean; | ||
type?: "submit"; | ||
variant?: | ||
| "ghost" | ||
| "default" | ||
| "primary" | ||
| "destructive" | ||
| "outline" | ||
| "secondary"; | ||
className?: string; | ||
label?: string; | ||
}; | ||
}>, | ||
) { | ||
return ( | ||
<div className="relative rounded-lg border border-border bg-card"> | ||
<div | ||
className={cn( | ||
"relative border-border border-b px-4 py-6 lg:px-6", | ||
props.noPermissionText && "cursor-not-allowed", | ||
)} | ||
> | ||
{props.header && ( | ||
<> | ||
<h3 className="font-semibold text-xl tracking-tight"> | ||
{props.header.title} | ||
</h3> | ||
{props.header.description && ( | ||
<p className="mt-1.5 mb-4 text-foreground text-sm"> | ||
{props.header.description} | ||
</p> | ||
)} | ||
</> | ||
)} | ||
|
||
{props.children} | ||
</div> | ||
|
||
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6"> | ||
{props.noPermissionText ? ( | ||
<p className="text-muted-foreground text-sm"> | ||
{props.noPermissionText} | ||
</p> | ||
) : props.errorText ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't need these if you make the error and success messages toasts There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to toasts . There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it pushed? Code looks the same |
||
<p className="text-destructive-text text-sm">{props.errorText}</p> | ||
) : ( | ||
<p className="text-muted-foreground text-sm">{props.bottomText}</p> | ||
)} | ||
|
||
{props.saveButton && !props.noPermissionText && ( | ||
<Button | ||
size="sm" | ||
className={cn("gap-2", props.saveButton.className)} | ||
onClick={props.saveButton.onClick} | ||
disabled={props.saveButton.disabled || props.saveButton.isPending} | ||
variant={props.saveButton.variant || "outline"} | ||
type={props.saveButton.type} | ||
> | ||
{props.saveButton.isPending && <Spinner className="size-3" />} | ||
{props.saveButton.label || | ||
(props.saveButton.isPending ? "Submit" : "Submit Token")} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Submit vs Submit Token? Is there any difference? |
||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
"use client"; | ||
import { addUniversalBridgeTokenRoute } from "@/api/universal-bridge/tokens"; // Adjust the import path | ||
import { RouteDiscoveryCard } from "@/components/blocks/RouteDiscoveryCard"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormLabel, | ||
} from "@/components/ui/form"; | ||
import { Input } from "@/components/ui/input"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useMutation } from "@tanstack/react-query"; | ||
import type { ProjectResponse } from "@thirdweb-dev/service-utils"; | ||
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton"; | ||
import { | ||
type RouteDiscoveryValidationSchema, | ||
routeDiscoveryValidationSchema, | ||
} from "components/settings/ApiKeys/validations"; | ||
import { useTrack } from "hooks/analytics/useTrack"; | ||
import { useForm } from "react-hook-form"; | ||
import { toast } from "sonner"; | ||
|
||
const TRACKING_CATEGORY = "token_discovery"; | ||
|
||
export const RouteDiscovery = ({ project }: { project: ProjectResponse }) => { | ||
const form = useForm<RouteDiscoveryValidationSchema>({ | ||
resolver: zodResolver(routeDiscoveryValidationSchema), | ||
defaultValues: { | ||
chainId: 1, | ||
tokenAddress: undefined, | ||
}, | ||
}); | ||
|
||
const trackEvent = useTrack(); | ||
|
||
const submitDiscoveryMutation = useMutation({ | ||
mutationFn: async (values: { | ||
chainId: number; | ||
tokenAddress: string; | ||
}) => { | ||
// Call the API to add the route | ||
const result = await addUniversalBridgeTokenRoute({ | ||
chainId: values.chainId, | ||
tokenAddress: values.tokenAddress, | ||
project, | ||
}); | ||
|
||
return result; | ||
}, | ||
}); | ||
|
||
const handleSubmit = form.handleSubmit( | ||
({ chainId, tokenAddress }) => { | ||
submitDiscoveryMutation.mutate( | ||
{ | ||
chainId, | ||
tokenAddress, | ||
}, | ||
{ | ||
onSuccess: (data) => { | ||
toast.success("Token submitted successfully!", { | ||
description: | ||
"Thank you for your submission. Contact support if your token doesn't appear after some time.", | ||
}); | ||
trackEvent({ | ||
category: TRACKING_CATEGORY, | ||
action: "token-discovery-submit", | ||
label: "success", | ||
data: { | ||
tokenAddress, | ||
tokenCount: data?.length || 0, | ||
}, | ||
}); | ||
}, | ||
onError: () => { | ||
toast.error("Token submission failed!", { | ||
description: | ||
"Please double check the network and token address. If issues persist, please reach out to our support team.", | ||
}); | ||
|
||
// Get appropriate error message | ||
const errorMessage = "An unknown error occurred"; | ||
|
||
trackEvent({ | ||
category: TRACKING_CATEGORY, | ||
action: "token-discovery-submit", | ||
label: "error", | ||
error: errorMessage, | ||
}); | ||
}, | ||
}, | ||
); | ||
}, | ||
() => { | ||
toast.error("Please fix the form errors before submitting"); | ||
}, | ||
); | ||
|
||
return ( | ||
<Form {...form}> | ||
<form onSubmit={handleSubmit} autoComplete="off"> | ||
<RouteDiscoveryCard | ||
bottomText="" | ||
errorText={form.getFieldState("tokenAddress").error?.message} | ||
saveButton={ | ||
// Only show the submit button in the default state | ||
{ | ||
type: "submit", | ||
disabled: !form.formState.isDirty, | ||
isPending: submitDiscoveryMutation.isPending, | ||
variant: "outline", | ||
} | ||
} | ||
noPermissionText={undefined} | ||
> | ||
<div> | ||
<h3 className="font-semibold text-xl tracking-tight"> | ||
Don't see your token listed? | ||
</h3> | ||
<p className="mt-1.5 mb-4 text-foreground text-sm"> | ||
Select your chain and input the token address to automatically | ||
kick-off the token route discovery process. Please check back on | ||
this page within 20-40 minutes of submitting this form. | ||
</p> | ||
|
||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> | ||
<FormField | ||
control={form.control} | ||
name="chainId" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Blockchain</FormLabel> | ||
<FormControl> | ||
<NetworkSelectorButton | ||
onSwitchChain={(chain) => { | ||
// Update the form field value | ||
field.onChange(chain.chainId); | ||
}} | ||
/> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
<FormField | ||
control={form.control} | ||
name="tokenAddress" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormLabel>Token Address</FormLabel> | ||
<FormControl> | ||
<div className="flex items-center gap-2"> | ||
<Input {...field} placeholder="0x..." /> | ||
</div> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
</div> | ||
</div> | ||
</RouteDiscoveryCard> | ||
</form> | ||
</Form> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Verify the response structure matches TokenMetadata array.
The function assumes the API response will match the expected structure and casts it to
Array<TokenMetadata>
. Consider adding a type guard or validation if the API response structure might vary.🏁 Script executed:
Length of output: 1138
Validate API Response Structure
The route helper in
apps/dashboard/src/@/api/universal-bridge/tokens.ts
(lines 66–68) currently does:That cast assumes the external API will always return the exact shape of
TokenMetadata[]
. To guard against unexpected payloads (which could break downstream code inRouteDiscovery.tsx
), add a runtime check or schema validation. For example:json.data
truly matchesTokenMetadata[]
.This will ensure any API‐side changes surface immediately and can be handled gracefully.
🤖 Prompt for AI Agents