Skip to content

Commit 376bdb2

Browse files
gregfromstlClaude
and
Claude
authored
[Dashboard] Feature: Setup payment link UI (#7121)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent dcd6b99 commit 376bdb2

File tree

29 files changed

+289
-107
lines changed

29 files changed

+289
-107
lines changed

.changeset/tired-rice-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Payment link support in PayEmbed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import "server-only";
2+
3+
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
4+
import { UB_BASE_URL } from "./constants";
5+
6+
type PaymentLink = {
7+
clientId: string;
8+
label?: string;
9+
receiver: string;
10+
destinationToken: {
11+
address: string;
12+
symbol: string;
13+
decimals: number;
14+
chainId: number;
15+
};
16+
amount: bigint | undefined;
17+
purchaseData: unknown;
18+
};
19+
20+
export async function getPaymentLink(props: {
21+
paymentId: string;
22+
}) {
23+
const res = await fetch(`${UB_BASE_URL}/v1/links/${props.paymentId}`, {
24+
method: "GET",
25+
headers: {
26+
"Content-Type": "application/json",
27+
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
28+
},
29+
});
30+
31+
if (!res.ok) {
32+
const text = await res.text();
33+
throw new Error(text);
34+
}
35+
36+
const { data } = await res.json();
37+
return {
38+
clientId: data.clientId,
39+
label: data.label,
40+
receiver: data.receiver,
41+
destinationToken: data.destinationToken,
42+
amount: data.amount ? BigInt(data.amount) : undefined,
43+
purchaseData: data.purchaseData,
44+
} as PaymentLink;
45+
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/TotalSponsoredCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ export async function TotalSponsoredChartCardUI({
118118
className={className}
119119
// Get the trend from the last two COMPLETE periods
120120
trendFn={(data, key) =>
121-
data.filter((d) => (d[key] as number) > 0).length >= 3
121+
data.filter((d) => (d[key] as number) > 0).length >= 2
122122
? ((data[data.length - 2]?.[key] as number) ?? 0) /
123-
((data[data.length - 3]?.[key] as number) ?? 0) -
123+
((data[0]?.[key] as number) ?? 0) -
124124
1
125125
: undefined
126126
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/_components/TransactionsCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ export async function TransactionsChartCardUI({
119119
className={className}
120120
// Get the trend from the last two COMPLETE periods
121121
trendFn={(data, key) =>
122-
data.filter((d) => (d[key] as number) > 0).length >= 3
122+
data.filter((d) => (d[key] as number) > 0).length >= 2
123123
? ((data[data.length - 2]?.[key] as number) ?? 0) /
124-
((data[data.length - 3]?.[key] as number) ?? 0) -
124+
((data[0]?.[key] as number) ?? 0) -
125125
1
126126
: undefined
127127
}

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/analytics/page.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,16 @@ function AppHighlightsCard({
320320
"totalVolume"
321321
}
322322
data={timeSeriesData}
323-
aggregateFn={(_data, key) =>
324-
timeSeriesData.reduce((acc, curr) => acc + curr[key], 0)
325-
}
326-
// Get the trend from the last two COMPLETE periods
323+
aggregateFn={(_data, key) => {
324+
if (key === "activeUsers") {
325+
return Math.max(...timeSeriesData.map((d) => d[key]));
326+
}
327+
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
328+
}}
327329
trendFn={(data, key) =>
328-
data.filter((d) => (d[key] as number) > 0).length >= 3
330+
data.filter((d) => (d[key] as number) > 0).length >= 2
329331
? ((data[data.length - 2]?.[key] as number) ?? 0) /
330-
((data[data.length - 3]?.[key] as number) ?? 0) -
332+
((data[0]?.[key] as number) ?? 0) -
331333
1
332334
: undefined
333335
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/page.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -376,17 +376,16 @@ function AppHighlightsCard({
376376
activeChart={chartKey}
377377
queryKey="appHighlights"
378378
data={timeSeriesData}
379-
aggregateFn={(_data, key) =>
380-
// If there is only one data point, use that one, otherwise use the previous
381-
timeSeriesData.filter((d) => (d[key] as number) > 0).length >= 2
382-
? timeSeriesData[timeSeriesData.length - 2]?.[key]
383-
: timeSeriesData[timeSeriesData.length - 1]?.[key]
384-
}
385-
// Get the trend from the last two COMPLETE periods
379+
aggregateFn={(_data, key) => {
380+
if (key === "activeUsers") {
381+
return Math.max(...timeSeriesData.map((d) => d[key]));
382+
}
383+
return timeSeriesData.reduce((acc, curr) => acc + curr[key], 0);
384+
}}
386385
trendFn={(data, key) =>
387-
data.filter((d) => (d[key] as number) > 0).length >= 3
386+
data.filter((d) => (d[key] as number) > 0).length >= 2
388387
? ((data[data.length - 2]?.[key] as number) ?? 0) /
389-
((data[data.length - 3]?.[key] as number) ?? 0) -
388+
((data[0]?.[key] as number) ?? 0) -
390389
1
391390
: undefined
392391
}

apps/dashboard/src/app/nebula-app/(app)/utils/nebulaThirdwebClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
NEXT_PUBLIC_NEBULA_APP_CLIENT_ID,
44
} from "@/constants/public-envs";
55
import {
6+
THIRDWEB_BRIDGE_URL,
67
THIRDWEB_BUNDLER_DOMAIN,
78
THIRDWEB_INAPP_WALLET_DOMAIN,
89
THIRDWEB_INSIGHT_API_DOMAIN,
@@ -27,6 +28,7 @@ function getNebulaThirdwebClient() {
2728
social: THIRDWEB_SOCIAL_API_DOMAIN,
2829
bundler: THIRDWEB_BUNDLER_DOMAIN,
2930
insight: THIRDWEB_INSIGHT_API_DOMAIN,
31+
bridge: THIRDWEB_BRIDGE_URL,
3032
});
3133
}
3234

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { getPaymentLink } from "@/api/universal-bridge/links";
2+
import type { Metadata } from "next";
3+
import { defineChain, getContract } from "thirdweb";
4+
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
5+
import { checksumAddress } from "thirdweb/utils";
6+
import { getClientThirdwebClient } from "../../../@/constants/thirdweb-client.client";
7+
import { PayPageEmbed } from "../components/client/PayPageEmbed.client";
8+
9+
const title = "thirdweb Pay";
10+
const description = "Fast, secure, and simple payments.";
11+
12+
export const metadata: Metadata = {
13+
title,
14+
description,
15+
openGraph: {
16+
title,
17+
description,
18+
},
19+
};
20+
21+
export default async function PayPage({
22+
params,
23+
searchParams,
24+
}: {
25+
params: Promise<{ id: string }>;
26+
searchParams: Promise<{ redirectUri?: string; theme?: "light" | "dark" }>;
27+
}) {
28+
const { id } = await params;
29+
const { redirectUri, theme } = await searchParams;
30+
31+
const paymentLink = await getPaymentLink({
32+
paymentId: id,
33+
});
34+
35+
const tokenContract = getContract({
36+
client: getClientThirdwebClient(undefined), // for this RPC call, use the dashboard client
37+
// eslint-disable-next-line no-restricted-syntax
38+
chain: defineChain(Number(paymentLink.destinationToken.chainId)),
39+
address: paymentLink.destinationToken.address,
40+
});
41+
const {
42+
symbol,
43+
decimals,
44+
name: tokenName,
45+
} = await getCurrencyMetadata({
46+
contract: tokenContract,
47+
});
48+
const token = {
49+
symbol,
50+
decimals,
51+
name: tokenName,
52+
address: checksumAddress(paymentLink.destinationToken.address),
53+
chainId: Number(paymentLink.destinationToken.chainId),
54+
};
55+
56+
return (
57+
<PayPageEmbed
58+
redirectUri={redirectUri}
59+
paymentLinkId={id}
60+
chainId={Number(paymentLink.destinationToken.chainId)}
61+
recipientAddress={paymentLink.receiver}
62+
amount={paymentLink.amount ? BigInt(paymentLink.amount) : undefined}
63+
token={token}
64+
clientId={paymentLink.clientId}
65+
name={paymentLink.label}
66+
theme={theme}
67+
/>
68+
);
69+
}

apps/dashboard/src/app/pay/components/client/PayPageEmbed.client.tsx

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,26 @@
11
"use client";
2-
import {
3-
THIRDWEB_ANALYTICS_DOMAIN,
4-
THIRDWEB_BUNDLER_DOMAIN,
5-
THIRDWEB_INAPP_WALLET_DOMAIN,
6-
THIRDWEB_INSIGHT_API_DOMAIN,
7-
THIRDWEB_PAY_DOMAIN,
8-
THIRDWEB_RPC_DOMAIN,
9-
THIRDWEB_SOCIAL_API_DOMAIN,
10-
THIRDWEB_STORAGE_DOMAIN,
11-
} from "constants/urls";
2+
import { payAppThirdwebClient } from "app/pay/constants";
123
import { useV5DashboardChain } from "lib/v5-adapter";
13-
import { getVercelEnv } from "lib/vercel-utils";
144
import { useTheme } from "next-themes";
15-
import { useEffect, useMemo } from "react";
16-
import { NATIVE_TOKEN_ADDRESS, createThirdwebClient, toTokens } from "thirdweb";
5+
import { useEffect } from "react";
6+
import { NATIVE_TOKEN_ADDRESS, toTokens } from "thirdweb";
177
import { AutoConnect, PayEmbed } from "thirdweb/react";
18-
import { setThirdwebDomains } from "thirdweb/utils";
198

209
export function PayPageEmbed({
2110
chainId,
2211
recipientAddress,
12+
paymentLinkId,
2313
amount,
2414
token,
2515
name,
2616
image,
2717
redirectUri,
28-
clientId,
2918
theme,
3019
}: {
3120
chainId: number;
3221
recipientAddress: string;
33-
amount: bigint;
22+
paymentLinkId?: string;
23+
amount?: bigint;
3424
token: { name: string; symbol: string; address: string; decimals: number };
3525
name?: string;
3626
image?: string;
@@ -46,30 +36,15 @@ export function PayPageEmbed({
4636
setTheme(theme);
4737
}
4838
}, [theme, setTheme]);
49-
50-
const client = useMemo(() => {
51-
if (getVercelEnv() !== "production") {
52-
setThirdwebDomains({
53-
rpc: THIRDWEB_RPC_DOMAIN,
54-
pay: THIRDWEB_PAY_DOMAIN,
55-
storage: THIRDWEB_STORAGE_DOMAIN,
56-
insight: THIRDWEB_INSIGHT_API_DOMAIN,
57-
analytics: THIRDWEB_ANALYTICS_DOMAIN,
58-
inAppWallet: THIRDWEB_INAPP_WALLET_DOMAIN,
59-
bundler: THIRDWEB_BUNDLER_DOMAIN,
60-
social: THIRDWEB_SOCIAL_API_DOMAIN,
61-
});
62-
}
63-
return createThirdwebClient({ clientId });
64-
}, [clientId]);
6539
const chain = useV5DashboardChain(chainId);
6640

6741
return (
6842
<>
69-
<AutoConnect client={client} />
43+
<AutoConnect client={payAppThirdwebClient} />
7044
<PayEmbed
71-
client={client}
45+
client={payAppThirdwebClient}
7246
theme={theme ?? (browserTheme === "light" ? "light" : "dark")}
47+
paymentLinkId={paymentLinkId}
7348
payOptions={{
7449
metadata: {
7550
name,
@@ -79,7 +54,7 @@ export function PayPageEmbed({
7954
paymentInfo: {
8055
chain,
8156
sellerAddress: recipientAddress,
82-
amount: toTokens(amount, token.decimals),
57+
amount: amount ? toTokens(amount, token.decimals) : "0.01",
8358
token: token.address === NATIVE_TOKEN_ADDRESS ? undefined : token,
8459
},
8560
onPurchaseSuccess: (result) => {

0 commit comments

Comments
 (0)