Skip to content

Commit 4b0124f

Browse files
committed
wip: payment link creation
1 parent d16c991 commit 4b0124f

File tree

17 files changed

+822
-437
lines changed

17 files changed

+822
-437
lines changed

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"use server";
2+
import type { Address } from "thirdweb";
23
import { getAuthToken } from "@/api/auth-token";
34
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
45

@@ -96,6 +97,112 @@ export async function deleteWebhook(props: {
9697
return;
9798
}
9899

100+
export type PaymentLink = {
101+
id: string;
102+
link: string;
103+
title: string;
104+
imageUrl: string;
105+
createdAt: string;
106+
updatedAt: string;
107+
destinationToken: {
108+
chainId: number;
109+
address: Address;
110+
symbol: string;
111+
name: string;
112+
decimals: number;
113+
iconUri: string;
114+
};
115+
receiver: Address;
116+
amount: bigint;
117+
};
118+
119+
export async function getPaymentLinks(props: {
120+
clientId: string;
121+
teamId: string;
122+
}): Promise<Array<PaymentLink>> {
123+
const authToken = await getAuthToken();
124+
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
125+
headers: {
126+
Authorization: `Bearer ${authToken}`,
127+
"Content-Type": "application/json",
128+
"x-client-id": props.clientId,
129+
"x-team-id": props.teamId,
130+
},
131+
method: "GET",
132+
});
133+
134+
if (!res.ok) {
135+
const text = await res.text();
136+
throw new Error(text);
137+
}
138+
139+
const json = (await res.json()) as {
140+
data: Array<PaymentLink & { amount: string }>;
141+
};
142+
return json.data.map((link) => ({
143+
id: link.id,
144+
link: link.link,
145+
title: link.title,
146+
imageUrl: link.imageUrl,
147+
createdAt: link.createdAt,
148+
updatedAt: link.updatedAt,
149+
destinationToken: {
150+
chainId: link.destinationToken.chainId,
151+
address: link.destinationToken.address,
152+
symbol: link.destinationToken.symbol,
153+
name: link.destinationToken.name,
154+
decimals: link.destinationToken.decimals,
155+
iconUri: link.destinationToken.iconUri,
156+
},
157+
receiver: link.receiver,
158+
amount: BigInt(link.amount),
159+
}));
160+
}
161+
162+
export async function createPaymentLink(props: {
163+
clientId: string;
164+
teamId: string;
165+
title: string;
166+
imageUrl: string;
167+
intent: {
168+
destinationChainId: number;
169+
destinationTokenAddress: Address;
170+
receiver: Address;
171+
amount: bigint;
172+
purchaseData?: unknown;
173+
};
174+
}) {
175+
const authToken = await getAuthToken();
176+
177+
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
178+
body: JSON.stringify({
179+
title: props.title,
180+
imageUrl: props.imageUrl,
181+
intent: {
182+
destinationChainId: props.intent.destinationChainId,
183+
destinationTokenAddress: props.intent.destinationTokenAddress,
184+
receiver: props.intent.receiver,
185+
amount: props.intent.amount.toString(),
186+
purchaseData: props.intent.purchaseData,
187+
},
188+
}),
189+
headers: {
190+
Authorization: `Bearer ${authToken}`,
191+
"Content-Type": "application/json",
192+
"x-client-id": props.clientId,
193+
"x-team-id": props.teamId,
194+
},
195+
method: "POST",
196+
});
197+
198+
if (!res.ok) {
199+
const text = await res.text();
200+
throw new Error(text);
201+
}
202+
203+
return;
204+
}
205+
99206
export type Fee = {
100207
feeRecipient: string;
101208
feeBps: number;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ export type TokenMetadata = {
1313
iconUri?: string;
1414
};
1515

16-
export async function getUniversalBridgeTokens(props: { chainId?: number }) {
16+
export async function getUniversalBridgeTokens(props: {
17+
chainId?: number;
18+
address?: string;
19+
}) {
1720
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
1821

1922
if (props.chainId) {
2023
url.searchParams.append("chainId", String(props.chainId));
2124
}
25+
if (props.address) {
26+
url.searchParams.append("tokenAddress", props.address);
27+
}
2228
url.searchParams.append("limit", "1000");
2329

2430
const res = await fetch(url.toString(), {

apps/dashboard/src/@/components/blocks/TokenSelector.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useTokensData } from "@/hooks/tokens";
1414
import { replaceIpfsUrl } from "@/lib/sdk";
1515
import { cn } from "@/lib/utils";
1616
import { fallbackChainIcon } from "@/utils/chain-icons";
17+
import { Spinner } from "../ui/Spinner/Spinner";
1718

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

@@ -186,9 +187,14 @@ export function TokenSelector(props: {
186187
options={options}
187188
overrideSearchFn={searchFn}
188189
placeholder={
189-
tokensQuery.isPending
190-
? "Loading Tokens"
191-
: props.placeholder || "Select Token"
190+
tokensQuery.isPending ? (
191+
<div className="flex items-center gap-2">
192+
<Spinner className="size-4" />
193+
<span>Loading Tokens</span>
194+
</div>
195+
) : (
196+
props.placeholder || "Select Token"
197+
)
192198
}
193199
popoverContentClassName={props.popoverContentClassName}
194200
renderOption={renderOption}

apps/dashboard/src/@/components/blocks/select-with-search.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface SelectWithSearchProps
2222
}[];
2323
value: string | undefined;
2424
onValueChange: (value: string) => void;
25-
placeholder: string;
25+
placeholder: string | React.ReactNode;
2626
searchPlaceholder?: string;
2727
className?: string;
2828
overrideSearchFn?: (

apps/dashboard/src/@/components/ui/tabs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function TabLinks(props: {
5656
<Link
5757
aria-disabled={tab.isDisabled}
5858
className={cn(
59-
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm hover:bg-accent lg:text-sm",
59+
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm lg:text-sm",
6060
!tab.isActive && !tab.isDisabled && "hover:text-foreground",
6161
tab.isDisabled && "pointer-events-none",
6262
tab.isActive && "!text-foreground",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Card } from "@/components/ui/card";
2+
3+
export function EmptyState(props: {
4+
icon: React.FC<{ className?: string }>;
5+
title: string;
6+
description: string;
7+
buttons: Array<React.ReactNode>;
8+
}) {
9+
return (
10+
<Card className="flex flex-col p-16 gap-8 items-center justify-center">
11+
<div className="bg-violet-800/25 text-muted-foreground rounded-full size-16 flex items-center justify-center">
12+
<props.icon className="size-8 text-violet-500" />
13+
</div>
14+
<div className="flex flex-col gap-1 items-center text-center">
15+
<h3 className="text-foreground font-medium text-xl">{props.title}</h3>
16+
<p className="text-muted-foreground text-sm max-w-md">
17+
{props.description}
18+
</p>
19+
</div>
20+
<div className="flex gap-4">{props.buttons.map((button) => button)}</div>
21+
</Card>
22+
);
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { OctagonAlertIcon } from "lucide-react";
2+
import { Card } from "@/components/ui/card";
3+
4+
export function ErrorState(props: {
5+
title: string;
6+
description: string;
7+
buttons: Array<React.ReactNode>;
8+
}) {
9+
return (
10+
<Card className="flex flex-col p-16 gap-8 items-center justify-center">
11+
<OctagonAlertIcon className="size-8 text-red-500" />
12+
<div className="flex flex-col gap-1 items-center text-center">
13+
<h3 className="text-foreground font-medium text-xl">{props.title}</h3>
14+
<p className="text-muted-foreground text-sm max-w-md">
15+
{props.description}
16+
</p>
17+
</div>
18+
{props.buttons && (
19+
<div className="flex gap-4">
20+
{props.buttons.map((button) => button)}
21+
</div>
22+
)}
23+
</Card>
24+
);
25+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PayAnalytics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getUniversalBridgeWalletUsage,
66
} from "@/api/analytics";
77
import type { Range } from "@/components/analytics/date-range-selector";
8+
import { Card } from "@/components/ui/card";
89
import { CodeServer } from "@/components/ui/code/code.server";
910
import { Skeleton } from "@/components/ui/skeleton";
1011
import { BuyWidgetFTUX } from "./BuyWidgetFTUX";
@@ -17,7 +18,6 @@ import { PayNewCustomers } from "./PayNewCustomers";
1718
import { Payouts } from "./Payouts";
1819
import { TotalPayVolume } from "./TotalPayVolume";
1920
import { TotalVolumePieChart } from "./TotalVolumePieChart";
20-
import { Card } from "@/components/ui/card";
2121

2222
export async function PayAnalytics(props: {
2323
projectClientId: string;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PaymentHistory.client.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
1212
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
1313
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
14-
import { TableRow, SkeletonTableRow } from "./PaymentsTableRow";
15-
import { CardHeading, TableHeading, TableHeadingRow } from "./common";
14+
import { Skeleton } from "@/components/ui/skeleton";
15+
import { TableData, TableHeading, TableHeadingRow } from "./common";
1616
import { formatTokenAmount } from "./format";
17+
import { TableRow } from "./PaymentsTableRow";
1718

1819
const pageSize = 50;
1920

@@ -158,3 +159,28 @@ function getCSVData(data: Payment[]) {
158159

159160
return { header, rows };
160161
}
162+
163+
function SkeletonTableRow() {
164+
return (
165+
<tr className="border-border border-b">
166+
<TableData>
167+
<Skeleton className="h-7 w-20" />
168+
</TableData>
169+
<TableData>
170+
<Skeleton className="h-7 w-20" />
171+
</TableData>
172+
<TableData>
173+
<Skeleton className="h-7 w-20 rounded-2xl" />
174+
</TableData>
175+
<TableData>
176+
<Skeleton className="h-7 w-20 rounded-2xl" />
177+
</TableData>
178+
<TableData>
179+
<Skeleton className="h-7 w-[140px]" />
180+
</TableData>
181+
<TableData>
182+
<Skeleton className="h-7 w-[200px]" />
183+
</TableData>
184+
</tr>
185+
);
186+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/payments/components/PaymentsTableRow.tsx

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { type ThirdwebClient, toTokens } from "thirdweb";
33
import type { Payment } from "@/api/universal-bridge/developer";
44
import { WalletAddress } from "@/components/blocks/wallet-address";
55
import { Badge } from "@/components/ui/badge";
6-
import { Skeleton } from "@/components/ui/skeleton";
76
import { cn } from "@/lib/utils";
87
import { TableData } from "./common";
98
import { formatTokenAmount } from "./format";
@@ -86,28 +85,3 @@ export function TableRow(props: { purchase: Payment; client: ThirdwebClient }) {
8685
</tr>
8786
);
8887
}
89-
90-
export function SkeletonTableRow() {
91-
return (
92-
<tr className="border-border border-b">
93-
<TableData>
94-
<Skeleton className="h-7 w-20" />
95-
</TableData>
96-
<TableData>
97-
<Skeleton className="h-7 w-20" />
98-
</TableData>
99-
<TableData>
100-
<Skeleton className="h-7 w-20 rounded-2xl" />
101-
</TableData>
102-
<TableData>
103-
<Skeleton className="h-7 w-20 rounded-2xl" />
104-
</TableData>
105-
<TableData>
106-
<Skeleton className="h-7 w-[140px]" />
107-
</TableData>
108-
<TableData>
109-
<Skeleton className="h-7 w-[200px]" />
110-
</TableData>
111-
</tr>
112-
);
113-
}

0 commit comments

Comments
 (0)