Skip to content

Commit e14a134

Browse files
committed
[Dashboard] Add token request route to dashboard (#7112)
TOOL-4443 TOOL-4559 How to Test 1. Log into dashboard 2. Select a project 3. Click on Universal Bridge on left hand side 4. Click on Settings 5. Scroll down to add token section - Failure input Chain: Ethereum Token Address: 0x7613c48e0cd50e42dd9bf0f6c235063145f6f8db -Success input Chain: Ethereum Token Address:0x7613c48e0cd50e42dd9bf0f6c235063145f6f8dc <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a new validation schema for route discovery, enhances the UI for token submission, and integrates a new `RouteDiscovery` component to facilitate token discovery in the dashboard. ### Detailed summary - Added `routeDiscoveryValidationSchema` in `validations.ts` for validating token discovery inputs. - Created `RouteDiscovery` component to handle token submission. - Updated `page.tsx` to include `RouteDiscovery` below `PayConfig`. - Implemented `addUniversalBridgeTokenRoute` function in `tokens.ts` for API token submission. - Added `RouteDiscoveryCard` component for better UI presentation of token discovery. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a Route Discovery workflow, allowing users to submit tokens for route discovery on selected blockchain networks. - Added a new card component for displaying route discovery information and actions. - Integrated Route Discovery into the Universal Bridge settings page for easier access. - **Enhancements** - Added validation for token address and chain selection to ensure proper input. - Provided user feedback through success and error messages, including toast notifications. - **Style** - Minor cleanup of extra blank lines in the Project General Settings page for improved readability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent acd8b9e commit e14a134

File tree

6 files changed

+310
-14
lines changed

6 files changed

+310
-14
lines changed

apps/dashboard/src/@/api/universal-bridge/tokens.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use server";
22
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
3+
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
4+
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
35
import { UB_BASE_URL } from "./constants";
46

57
export type TokenMetadata = {
@@ -37,3 +39,33 @@ export async function getUniversalBridgeTokens(props: {
3739
const json = await res.json();
3840
return json.data as Array<TokenMetadata>;
3941
}
42+
43+
export async function addUniversalBridgeTokenRoute(props: {
44+
chainId: number;
45+
tokenAddress: string;
46+
project: ProjectResponse;
47+
}) {
48+
const authToken = await getAuthToken();
49+
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
50+
51+
const res = await fetch(url.toString(), {
52+
method: "POST",
53+
headers: {
54+
"Content-Type": "application/json",
55+
Authorization: `Bearer ${authToken}`,
56+
"x-client-id": props.project.publishableKey,
57+
} as Record<string, string>,
58+
body: JSON.stringify({
59+
chainId: props.chainId,
60+
tokenAddress: props.tokenAddress,
61+
}),
62+
});
63+
64+
if (!res.ok) {
65+
const text = await res.text();
66+
throw new Error(text);
67+
}
68+
69+
const json = await res.json();
70+
return json.data as Array<TokenMetadata>;
71+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Spinner } from "@/components/ui/Spinner/Spinner";
2+
import { Button } from "@/components/ui/button";
3+
import { cn } from "@/lib/utils";
4+
import type React from "react";
5+
6+
export function RouteDiscoveryCard(
7+
props: React.PropsWithChildren<{
8+
bottomText: React.ReactNode;
9+
header?: {
10+
description: string | undefined;
11+
title: string;
12+
};
13+
errorText: string | undefined;
14+
noPermissionText: string | undefined;
15+
saveButton?: {
16+
onClick?: () => void;
17+
disabled: boolean;
18+
isPending: boolean;
19+
type?: "submit";
20+
variant?:
21+
| "ghost"
22+
| "default"
23+
| "primary"
24+
| "destructive"
25+
| "outline"
26+
| "secondary";
27+
className?: string;
28+
label?: string;
29+
};
30+
}>,
31+
) {
32+
return (
33+
<div className="relative rounded-lg border border-border bg-card">
34+
<div
35+
className={cn(
36+
"relative border-border border-b px-4 py-6 lg:px-6",
37+
props.noPermissionText && "cursor-not-allowed",
38+
)}
39+
>
40+
{props.header && (
41+
<>
42+
<h3 className="font-semibold text-xl tracking-tight">
43+
{props.header.title}
44+
</h3>
45+
{props.header.description && (
46+
<p className="mt-1.5 mb-4 text-foreground text-sm">
47+
{props.header.description}
48+
</p>
49+
)}
50+
</>
51+
)}
52+
53+
{props.children}
54+
</div>
55+
56+
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
57+
{props.noPermissionText ? (
58+
<p className="text-muted-foreground text-sm">
59+
{props.noPermissionText}
60+
</p>
61+
) : props.errorText ? (
62+
<p className="text-destructive-text text-sm">{props.errorText}</p>
63+
) : (
64+
<p className="text-muted-foreground text-sm">{props.bottomText}</p>
65+
)}
66+
67+
{props.saveButton && !props.noPermissionText && (
68+
<Button
69+
size="sm"
70+
className={cn("gap-2", props.saveButton.className)}
71+
onClick={props.saveButton.onClick}
72+
disabled={props.saveButton.disabled || props.saveButton.isPending}
73+
variant={props.saveButton.variant || "outline"}
74+
type={props.saveButton.type}
75+
>
76+
{props.saveButton.isPending && <Spinner className="size-3" />}
77+
{props.saveButton.label ||
78+
(props.saveButton.isPending ? "Submit" : "Submit Token")}
79+
</Button>
80+
)}
81+
</div>
82+
</div>
83+
);
84+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/connect/universal-bridge/settings/page.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { getProject } from "@/api/projects";
22
import { getTeamBySlug } from "@/api/team";
33
import { getFees } from "@/api/universal-bridge/developer";
44
import { PayConfig } from "components/pay/PayConfig";
5+
import { RouteDiscovery } from "components/pay/RouteDiscovery";
6+
57
import { redirect } from "next/navigation";
68

79
export default async function Page(props: {
@@ -47,11 +49,17 @@ export default async function Page(props: {
4749
}
4850

4951
return (
50-
<PayConfig
51-
project={project}
52-
teamId={team.id}
53-
teamSlug={team_slug}
54-
fees={fees}
55-
/>
52+
<div className="flex flex-col p-5">
53+
<PayConfig
54+
project={project}
55+
teamId={team.id}
56+
teamSlug={team_slug}
57+
fees={fees}
58+
/>
59+
60+
<div className="flex pt-5">
61+
<RouteDiscovery project={project} />
62+
</div>
63+
</div>
5664
);
5765
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/settings/ProjectGeneralSettingsPage.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -349,40 +349,33 @@ export function ProjectGeneralSettingsPageUI(props: {
349349
isUpdatingProject={updateProject.isPending}
350350
handleSubmit={handleSubmit}
351351
/>
352-
353352
<ProjectImageSetting
354353
updateProjectImage={props.updateProjectImage}
355354
avatar={project.image || null}
356355
client={props.client}
357356
/>
358-
359357
<ProjectKeyDetails
360358
project={project}
361359
rotateSecretKey={props.rotateSecretKey}
362360
/>
363-
364361
<ProjectIdCard project={project} />
365-
366362
<AllowedDomainsSetting
367363
form={form}
368364
isUpdatingProject={updateProject.isPending}
369365
handleSubmit={handleSubmit}
370366
/>
371-
372367
<AllowedBundleIDsSetting
373368
form={form}
374369
isUpdatingProject={updateProject.isPending}
375370
handleSubmit={handleSubmit}
376371
/>
377-
378372
<EnabledServicesSetting
379373
form={form}
380374
isUpdatingProject={updateProject.isPending}
381375
handleSubmit={handleSubmit}
382376
paths={paths}
383377
showNebulaSettings={props.showNebulaSettings}
384378
/>
385-
386379
<TransferProject
387380
isOwnerAccount={props.isOwnerAccount}
388381
client={props.client}
@@ -391,7 +384,6 @@ export function ProjectGeneralSettingsPageUI(props: {
391384
currentTeamId={project.teamId}
392385
transferProject={props.transferProject}
393386
/>
394-
395387
<DeleteProject
396388
projectName={project.name}
397389
deleteProject={props.deleteProject}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"use client";
2+
import { addUniversalBridgeTokenRoute } from "@/api/universal-bridge/tokens"; // Adjust the import path
3+
import { RouteDiscoveryCard } from "@/components/blocks/RouteDiscoveryCard";
4+
import {
5+
Form,
6+
FormControl,
7+
FormField,
8+
FormItem,
9+
FormLabel,
10+
} from "@/components/ui/form";
11+
import { Input } from "@/components/ui/input";
12+
import { zodResolver } from "@hookform/resolvers/zod";
13+
import { useMutation } from "@tanstack/react-query";
14+
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
15+
import { NetworkSelectorButton } from "components/selects/NetworkSelectorButton";
16+
import {
17+
type RouteDiscoveryValidationSchema,
18+
routeDiscoveryValidationSchema,
19+
} from "components/settings/ApiKeys/validations";
20+
import { useTrack } from "hooks/analytics/useTrack";
21+
import { useForm } from "react-hook-form";
22+
import { toast } from "sonner";
23+
24+
const TRACKING_CATEGORY = "token_discovery";
25+
26+
export const RouteDiscovery = ({ project }: { project: ProjectResponse }) => {
27+
const form = useForm<RouteDiscoveryValidationSchema>({
28+
resolver: zodResolver(routeDiscoveryValidationSchema),
29+
defaultValues: {
30+
chainId: 1,
31+
tokenAddress: undefined,
32+
},
33+
});
34+
35+
const trackEvent = useTrack();
36+
37+
const submitDiscoveryMutation = useMutation({
38+
mutationFn: async (values: {
39+
chainId: number;
40+
tokenAddress: string;
41+
}) => {
42+
// Call the API to add the route
43+
const result = await addUniversalBridgeTokenRoute({
44+
chainId: values.chainId,
45+
tokenAddress: values.tokenAddress,
46+
project,
47+
});
48+
49+
return result;
50+
},
51+
});
52+
53+
const handleSubmit = form.handleSubmit(
54+
({ chainId, tokenAddress }) => {
55+
submitDiscoveryMutation.mutate(
56+
{
57+
chainId,
58+
tokenAddress,
59+
},
60+
{
61+
onSuccess: (data) => {
62+
toast.success("Token submitted successfully!", {
63+
description:
64+
"Thank you for your submission. Contact support if your token doesn't appear after some time.",
65+
});
66+
trackEvent({
67+
category: TRACKING_CATEGORY,
68+
action: "token-discovery-submit",
69+
label: "success",
70+
data: {
71+
tokenAddress,
72+
tokenCount: data?.length || 0,
73+
},
74+
});
75+
},
76+
onError: () => {
77+
toast.error("Token submission failed!", {
78+
description:
79+
"Please double check the network and token address. If issues persist, please reach out to our support team.",
80+
});
81+
82+
// Get appropriate error message
83+
const errorMessage = "An unknown error occurred";
84+
85+
trackEvent({
86+
category: TRACKING_CATEGORY,
87+
action: "token-discovery-submit",
88+
label: "error",
89+
error: errorMessage,
90+
});
91+
},
92+
},
93+
);
94+
},
95+
() => {
96+
toast.error("Please fix the form errors before submitting");
97+
},
98+
);
99+
100+
return (
101+
<Form {...form}>
102+
<form onSubmit={handleSubmit} autoComplete="off">
103+
<RouteDiscoveryCard
104+
bottomText=""
105+
errorText={form.getFieldState("tokenAddress").error?.message}
106+
saveButton={
107+
// Only show the submit button in the default state
108+
{
109+
type: "submit",
110+
disabled: !form.formState.isDirty,
111+
isPending: submitDiscoveryMutation.isPending,
112+
variant: "outline",
113+
}
114+
}
115+
noPermissionText={undefined}
116+
>
117+
<div>
118+
<h3 className="font-semibold text-xl tracking-tight">
119+
Don't see your token listed?
120+
</h3>
121+
<p className="mt-1.5 mb-4 text-foreground text-sm">
122+
Select your chain and input the token address to automatically
123+
kick-off the token route discovery process. Please check back on
124+
this page within 20-40 minutes of submitting this form.
125+
</p>
126+
127+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
128+
<FormField
129+
control={form.control}
130+
name="chainId"
131+
render={({ field }) => (
132+
<FormItem>
133+
<FormLabel>Blockchain</FormLabel>
134+
<FormControl>
135+
<NetworkSelectorButton
136+
onSwitchChain={(chain) => {
137+
// Update the form field value
138+
field.onChange(chain.chainId);
139+
}}
140+
/>
141+
</FormControl>
142+
</FormItem>
143+
)}
144+
/>
145+
<FormField
146+
control={form.control}
147+
name="tokenAddress"
148+
render={({ field }) => (
149+
<FormItem>
150+
<FormLabel>Token Address</FormLabel>
151+
<FormControl>
152+
<div className="flex items-center gap-2">
153+
<Input {...field} placeholder="0x..." />
154+
</div>
155+
</FormControl>
156+
</FormItem>
157+
)}
158+
/>
159+
</div>
160+
</div>
161+
</RouteDiscoveryCard>
162+
</form>
163+
</Form>
164+
);
165+
};

0 commit comments

Comments
 (0)