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
150 changes: 141 additions & 9 deletions apps/dashboard/src/@/api/universal-bridge/developer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use server";
import type { Address } from "thirdweb";
import { getAuthToken } from "@/api/auth-token";
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";

Expand Down Expand Up @@ -96,6 +97,139 @@ export async function deleteWebhook(props: {
return;
}

type PaymentLink = {
id: string;
link: string;
title: string;
imageUrl: string;
createdAt: string;
updatedAt: string;
destinationToken: {
chainId: number;
address: Address;
symbol: string;
name: string;
decimals: number;
iconUri: string;
};
receiver: Address;
amount: bigint;
};

export async function getPaymentLinks(props: {
clientId: string;
teamId: string;
}): Promise<Array<PaymentLink>> {
const authToken = await getAuthToken();
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "GET",
});

if (!res.ok) {
const text = await res.text();
throw new Error(text);
}

const json = (await res.json()) as {
data: Array<PaymentLink & { amount: string }>;
};
return json.data.map((link) => ({
id: link.id,
link: link.link,
title: link.title,
imageUrl: link.imageUrl,
createdAt: link.createdAt,
updatedAt: link.updatedAt,
destinationToken: {
chainId: link.destinationToken.chainId,
address: link.destinationToken.address,
symbol: link.destinationToken.symbol,
name: link.destinationToken.name,
decimals: link.destinationToken.decimals,
iconUri: link.destinationToken.iconUri,
},
receiver: link.receiver,
amount: BigInt(link.amount),
}));
}

export async function createPaymentLink(props: {
clientId: string;
teamId: string;
title: string;
imageUrl?: string;
intent: {
destinationChainId: number;
destinationTokenAddress: Address;
receiver: Address;
amount: bigint;
purchaseData?: unknown;
};
}) {
const authToken = await getAuthToken();

const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
body: JSON.stringify({
title: props.title,
imageUrl: props.imageUrl,
intent: {
destinationChainId: props.intent.destinationChainId,
destinationTokenAddress: props.intent.destinationTokenAddress,
receiver: props.intent.receiver,
amount: props.intent.amount.toString(),
purchaseData: props.intent.purchaseData,
},
}),
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "POST",
});

if (!res.ok) {
const text = await res.text();
throw new Error(text);
}

return;
}

export async function deletePaymentLink(props: {
clientId: string;
teamId: string;
paymentLinkId: string;
}) {
const authToken = await getAuthToken();
const res = await fetch(
`${UB_BASE_URL}/v1/developer/links/${props.paymentLinkId}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"x-client-id": props.clientId,
"x-team-id": props.teamId,
},
method: "DELETE",
},
);

if (!res.ok) {
const text = await res.text();
throw new Error(text);
}

return;
}

export type Fee = {
feeRecipient: string;
feeBps: number;
Expand Down Expand Up @@ -195,30 +329,28 @@ export type Payment = {
export async function getPayments(props: {
clientId: string;
teamId: string;
paymentLinkId?: string;
limit?: number;
offset?: number;
}) {
const authToken = await getAuthToken();

// Build URL with query parameters if provided
let url = `${UB_BASE_URL}/v1/developer/payments`;
const queryParams = new URLSearchParams();
const url = new URL(`${UB_BASE_URL}/v1/developer/payments`);

if (props.limit) {
queryParams.append("limit", props.limit.toString());
url.searchParams.append("limit", props.limit.toString());
}

if (props.offset) {
queryParams.append("offset", props.offset.toString());
url.searchParams.append("offset", props.offset.toString());
}

// Append query params to URL if any exist
const queryString = queryParams.toString();
if (queryString) {
url = `${url}?${queryString}`;
if (props.paymentLinkId) {
url.searchParams.append("paymentLinkId", props.paymentLinkId);
}

const res = await fetch(url, {
const res = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
Expand Down
8 changes: 7 additions & 1 deletion apps/dashboard/src/@/api/universal-bridge/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ export type TokenMetadata = {
iconUri?: string;
};

export async function getUniversalBridgeTokens(props: { chainId?: number }) {
export async function getUniversalBridgeTokens(props: {
chainId?: number;
address?: string;
}) {
const url = new URL(`${UB_BASE_URL}/v1/tokens`);

if (props.chainId) {
url.searchParams.append("chainId", String(props.chainId));
}
if (props.address) {
url.searchParams.append("tokenAddress", props.address);
}
url.searchParams.append("limit", "1000");

const res = await fetch(url.toString(), {
Expand Down
12 changes: 9 additions & 3 deletions apps/dashboard/src/@/components/blocks/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useTokensData } from "@/hooks/tokens";
import { replaceIpfsUrl } from "@/lib/sdk";
import { cn } from "@/lib/utils";
import { fallbackChainIcon } from "@/utils/chain-icons";
import { Spinner } from "../ui/Spinner/Spinner";

type Option = { label: string; value: string };

Expand Down Expand Up @@ -186,9 +187,14 @@ export function TokenSelector(props: {
options={options}
overrideSearchFn={searchFn}
placeholder={
tokensQuery.isPending
? "Loading Tokens"
: props.placeholder || "Select Token"
tokensQuery.isPending ? (
<div className="flex items-center gap-2">
<Spinner className="size-4" />
<span>Loading tokens</span>
</div>
) : (
props.placeholder || "Select Token"
)
}
popoverContentClassName={props.popoverContentClassName}
renderOption={renderOption}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface SelectWithSearchProps
}[];
value: string | undefined;
onValueChange: (value: string) => void;
placeholder: string;
placeholder: string | React.ReactNode;
searchPlaceholder?: string;
className?: string;
overrideSearchFn?: (
Expand Down
53 changes: 33 additions & 20 deletions apps/dashboard/src/@/components/ui/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,42 @@ import { ToolTipLabel } from "./tooltip";

export function CopyButton(props: {
text: string;
label?: string;
className?: string;
iconClassName?: string;
variant?: "ghost" | "primary" | "secondary";
tooltip?: boolean;
variant?: "ghost" | "primary" | "secondary" | "default" | "outline";
}) {
const { hasCopied, onCopy } = useClipboard(props.text, 1000);
return (
<ToolTipLabel label="Copy">
<Button
aria-label="Copy"
className={cn("h-auto w-auto p-1", props.className)}
onClick={onCopy}
variant={props.variant || "ghost"}
>
{hasCopied ? (
<CheckIcon
className={cn("size-4 text-green-500", props.iconClassName)}
/>
) : (
<CopyIcon
className={cn("size-4 text-muted-foreground", props.iconClassName)}
/>
)}
</Button>
</ToolTipLabel>
const showTooltip = props.tooltip ?? true;

const button = (
<Button
aria-label="Copy"
className={cn(
"h-auto w-auto flex items-center gap-2 text-muted-foreground",
props.label ? "p-2" : "p-1",
props.className,
)}
onClick={onCopy}
variant={props.variant || "ghost"}
>
{hasCopied ? (
<CheckIcon
className={cn("size-4 text-green-500", props.iconClassName)}
/>
) : (
<CopyIcon
className={cn("size-4 text-muted-foreground", props.iconClassName)}
/>
)}
{props.label}
</Button>
);

if (!showTooltip) {
return button;
}

return <ToolTipLabel label="Copy">{button}</ToolTipLabel>;
}
2 changes: 1 addition & 1 deletion apps/dashboard/src/@/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function TabLinks(props: {
<Link
aria-disabled={tab.isDisabled}
className={cn(
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm hover:bg-accent lg:text-sm",
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm lg:text-sm",
!tab.isActive && !tab.isDisabled && "hover:text-foreground",
tab.isDisabled && "pointer-events-none",
tab.isActive && "!text-foreground",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Card } from "@/components/ui/card";

export function EmptyState(props: {
icon: React.FC<{ className?: string }>;
title: string;
description: string;
buttons: Array<React.ReactNode>;
}) {
return (
<Card className="flex flex-col p-16 gap-8 items-center justify-center">
<div className="bg-violet-800/25 text-muted-foreground rounded-full size-16 flex items-center justify-center">
<props.icon className="size-8 text-violet-500" />
</div>
<div className="flex flex-col gap-1 items-center text-center">
<h3 className="text-foreground font-medium text-xl">{props.title}</h3>
<p className="text-muted-foreground text-sm max-w-md">
{props.description}
</p>
</div>
<div className="flex gap-4">{props.buttons.map((button) => button)}</div>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { OctagonAlertIcon } from "lucide-react";
import { Card } from "@/components/ui/card";

export function ErrorState(props: {
title: string;
description: string;
buttons: Array<React.ReactNode>;
}) {
return (
<Card className="flex flex-col p-16 gap-8 items-center justify-center">
<OctagonAlertIcon className="size-8 text-red-500" />
<div className="flex flex-col gap-1 items-center text-center">
<h3 className="text-foreground font-medium text-xl">{props.title}</h3>
<p className="text-muted-foreground text-sm max-w-md">
{props.description}
</p>
</div>
{props.buttons && (
<div className="flex gap-4">
{props.buttons.map((button) => button)}
</div>
)}
</Card>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ export function FeatureCard(props: {
<div className="flex-1 flex flex-col items-start gap-6 w-full">
<div className="relative w-full">
<div
className={`${props.color === "green" ? "bg-green-700/25" : "bg-violet-700/25"} rounded-lg size-9 flex items-center justify-center`}
className={
"border bg-background rounded-lg size-9 flex items-center justify-center"
}
>
<props.icon
className={`size-5 ${props.color === "green" ? "text-green-500" : "text-violet-500"}`}
/>
<props.icon className={`size-5 text-foreground`} />
</div>
{props.badge && (
<Badge
Expand Down
Loading
Loading