Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
928704ad5c | ||
|
|
acbbf2039f | ||
|
|
4375e7331f | ||
|
|
22d736cb24 | ||
|
|
bb89561a1b | ||
|
|
c92081b056 | ||
|
|
dc57b850d3 | ||
|
|
6429a238c1 | ||
|
|
769019dd0b | ||
|
|
488a3af154 | ||
|
|
8a29e36934 | ||
|
|
ea98087d41 | ||
|
|
030349bb92 | ||
|
|
17d40afda3 | ||
|
|
bc6309ddc7 | ||
|
|
f47eda6842 | ||
|
|
2328985699 | ||
|
|
18b7f9104f | ||
|
|
82be4fa53a | ||
|
|
dbb2614306 | ||
|
|
b8f496603c | ||
|
|
4c782ad64d | ||
|
|
af6a306955 | ||
|
|
4b319c353d | ||
|
|
aadaf209b5 | ||
|
|
df6f8e2646 | ||
|
|
5b871ac281 | ||
|
|
7eed8e61db | ||
|
|
6ef2f3b37a | ||
|
|
596aaa5495 | ||
|
|
89eeac1f8a | ||
|
|
c331d189bb | ||
|
|
f867799301 | ||
|
|
597431ec1d | ||
|
|
d5ff9f2f5d | ||
|
|
76694f0cec | ||
|
|
65a8c39ce0 | ||
|
|
ac3a95ebbb | ||
|
|
00ffe3e46e | ||
|
|
50c6371e1e | ||
|
|
93532ecbdc | ||
|
|
f3ad6efcf8 | ||
|
|
67616c22b4 | ||
|
|
51f7418d1f | ||
|
|
dc3df4bc35 | ||
|
|
0853872d8d | ||
|
|
44e6e115d9 | ||
|
|
76cfecb305 | ||
|
|
f33d8f16e2 | ||
|
|
5c08c7c17d | ||
|
|
e20b1dd883 | ||
|
|
55a4728766 | ||
|
|
c5ef55e4f6 | ||
|
|
afd1150984 | ||
|
|
f79672ebbf | ||
|
|
3a289e80d1 | ||
|
|
e9efebaec4 | ||
|
|
d4a44f5b5f | ||
|
|
e651d0ae91 | ||
|
|
34239ea2cb | ||
|
|
8828df0a58 | ||
|
|
c6150be299 | ||
|
|
27f50a863c | ||
|
|
157f4b9a19 | ||
|
|
fbe1aa2e53 | ||
|
|
bdcbe0e25b | ||
|
|
30e217870d | ||
|
|
44fdfeef1a | ||
|
|
4cda30a87a | ||
|
|
98fab93e8b | ||
|
|
6caba380b2 | ||
|
|
68a07ed106 | ||
|
|
b075ff55a1 | ||
|
|
2d12249719 | ||
|
|
301b835b60 | ||
|
|
a02b55744b | ||
|
|
8b6deadc35 |
@@ -172,6 +172,8 @@ See our [open source page](https://thirdweb.com/open-source) for more informatio
|
||||
- [Documentation](https://portal.thirdweb.com/)
|
||||
- [Templates](https://thirdweb.com/templates)
|
||||
- [YouTube](https://www.youtube.com/c/thirdweb)
|
||||
- [X/Twitter](https://x.com/thirdweb)
|
||||
- [Telegram](https://t.me/officialthirdweb)
|
||||
|
||||
## Support
|
||||
|
||||
|
||||
@@ -95,6 +95,30 @@ module.exports = {
|
||||
'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.',
|
||||
name: "posthog-js",
|
||||
},
|
||||
{
|
||||
importNames: ["useSendTransaction"],
|
||||
message:
|
||||
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead',
|
||||
name: "thirdweb/react",
|
||||
},
|
||||
{
|
||||
importNames: ["useSendAndConfirmTransaction"],
|
||||
message:
|
||||
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead',
|
||||
name: "thirdweb/react",
|
||||
},
|
||||
{
|
||||
importNames: ["sendTransaction"],
|
||||
message:
|
||||
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead if used in react component',
|
||||
name: "thirdweb",
|
||||
},
|
||||
{
|
||||
importNames: ["sendAndConfirmTransaction"],
|
||||
message:
|
||||
'Use `import { useSendAndConfirmTx } from "@/hooks/useSendTx";` instead if used in react component',
|
||||
name: "thirdweb",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { Preview } from "@storybook/nextjs";
|
||||
import "../src/global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
// biome-ignore lint/style/useImportType: <explanation>
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
// biome-ignore lint/style/useImportType: ok
|
||||
import React, { useEffect } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { Button } from "../src/@/components/ui/button";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -69,9 +68,7 @@ const preview: Preview = {
|
||||
|
||||
export default preview;
|
||||
|
||||
function StoryLayout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
function StoryLayout(props: { children: React.ReactNode }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
],
|
||||
"ignoreBinaries": ["only-allow"],
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/api",
|
||||
"@thirdweb-dev/service-utils",
|
||||
"@thirdweb-dev/vault-sdk",
|
||||
"thirdweb",
|
||||
"@types/color",
|
||||
"fast-xml-parser",
|
||||
"@workspace/ui",
|
||||
"tailwindcss-animate"
|
||||
"tailwindcss-animate",
|
||||
"@radix-ui/react-tooltip",
|
||||
"shiki",
|
||||
"react-children-utilities",
|
||||
"react-markdown",
|
||||
"remark-gfm"
|
||||
],
|
||||
"next": true,
|
||||
"project": ["src/**"]
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@scalar/api-reference-react": "0.7.25",
|
||||
"@sentry/nextjs": "9.34.0",
|
||||
"@shazow/whatsabi": "0.22.2",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@thirdweb-dev/api": "workspace:*",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.2.2",
|
||||
@@ -47,7 +47,6 @@
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
"posthog-node": "^5.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
@@ -62,7 +61,7 @@
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"stripe": "17.7.0",
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
let _posthogClient: PostHog | null = null;
|
||||
|
||||
function getPostHogServer(): PostHog | null {
|
||||
if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
_posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: "https://us.i.posthog.com",
|
||||
});
|
||||
}
|
||||
return _posthogClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag is enabled for a specific user
|
||||
* @param flagKey - The feature flag key
|
||||
* @param userEmail - The user's email address for filtering
|
||||
*/
|
||||
export const isFeatureFlagEnabled = unstable_cache(
|
||||
async (params: {
|
||||
flagKey: string;
|
||||
accountId: string;
|
||||
email: string | undefined;
|
||||
}): Promise<boolean> => {
|
||||
const posthogClient = getPostHogServer();
|
||||
if (!posthogClient) {
|
||||
console.warn("Posthog client not set");
|
||||
return true;
|
||||
}
|
||||
|
||||
const { flagKey, accountId, email } = params;
|
||||
|
||||
try {
|
||||
if (posthogClient && accountId) {
|
||||
const isEnabled = await posthogClient.isFeatureEnabled(
|
||||
flagKey,
|
||||
accountId,
|
||||
{
|
||||
personProperties: email ? { email } : undefined,
|
||||
},
|
||||
);
|
||||
if (isEnabled !== undefined) {
|
||||
return isEnabled;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking feature flag ${flagKey}:`, error);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
["is-feature-flag-enabled"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { ANALYTICS_SERVICE_URL } from "@/constants/server-envs";
|
||||
import { normalizeTime } from "@/lib/time";
|
||||
import type {
|
||||
AIUsageStats,
|
||||
AnalyticsQueryParams,
|
||||
EcosystemWalletStats,
|
||||
EngineCloudStats,
|
||||
@@ -14,7 +15,6 @@ import type {
|
||||
UserOpStats,
|
||||
WalletStats,
|
||||
WebhookLatencyStats,
|
||||
WebhookRequestStats,
|
||||
WebhookSummaryStats,
|
||||
} from "@/types/analytics";
|
||||
import { getChains } from "./chain";
|
||||
@@ -205,6 +205,41 @@ export function getInAppWalletUsage(
|
||||
return cached_getInAppWalletUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getAiUsage = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams,
|
||||
authToken: string,
|
||||
): Promise<AIUsageStats[]> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics({
|
||||
authToken,
|
||||
url: `v2/nebula/usage?${searchParams.toString()}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch AI usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as AIUsageStats[];
|
||||
},
|
||||
["getAiUsage"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export function getAiUsage(params: AnalyticsQueryParams, authToken: string) {
|
||||
return cached_getAiUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getUserOpUsage = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams,
|
||||
@@ -660,7 +695,7 @@ export function getEngineCloudMethodUsage(
|
||||
return cached_getEngineCloudMethodUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getWebhookSummary = unstable_cache(
|
||||
const _cached_getWebhookSummary = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams & { webhookId: string },
|
||||
authToken: string,
|
||||
@@ -688,51 +723,7 @@ const cached_getWebhookSummary = unstable_cache(
|
||||
},
|
||||
);
|
||||
|
||||
export function getWebhookSummary(
|
||||
params: AnalyticsQueryParams & { webhookId: string },
|
||||
authToken: string,
|
||||
) {
|
||||
return cached_getWebhookSummary(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getWebhookRequests = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams & { webhookId?: string },
|
||||
authToken: string,
|
||||
): Promise<{ data: WebhookRequestStats[] } | { error: string }> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
if (params.webhookId) {
|
||||
searchParams.append("webhookId", params.webhookId);
|
||||
}
|
||||
|
||||
const res = await fetchAnalytics({
|
||||
authToken,
|
||||
url: `v2/webhook/requests?${searchParams.toString()}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const reason = await res.text();
|
||||
return { error: reason };
|
||||
}
|
||||
|
||||
return (await res.json()) as { data: WebhookRequestStats[] };
|
||||
},
|
||||
["getWebhookRequests"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export function getWebhookRequests(
|
||||
params: AnalyticsQueryParams & { webhookId?: string },
|
||||
authToken: string,
|
||||
) {
|
||||
return cached_getWebhookRequests(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getWebhookLatency = unstable_cache(
|
||||
const _cached_getWebhookLatency = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams & { webhookId?: string },
|
||||
authToken: string,
|
||||
@@ -762,13 +753,6 @@ const cached_getWebhookLatency = unstable_cache(
|
||||
},
|
||||
);
|
||||
|
||||
export function getWebhookLatency(
|
||||
params: AnalyticsQueryParams & { webhookId?: string },
|
||||
authToken: string,
|
||||
) {
|
||||
return cached_getWebhookLatency(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getInsightChainUsage = unstable_cache(
|
||||
async (
|
||||
params: AnalyticsQueryParams,
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
|
||||
export interface WebhookConfig {
|
||||
id: string;
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
destinationUrl: string;
|
||||
description: string;
|
||||
pausedAt: string | null;
|
||||
webhookSecret: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
topics: {
|
||||
id: string;
|
||||
serviceName: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
filters: object | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
type WebhookConfigsResponse =
|
||||
| {
|
||||
data: WebhookConfig[];
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
interface CreateWebhookConfigRequest {
|
||||
topicIdsWithFilters: { id: string; filters: object | null }[];
|
||||
destinationUrl: string;
|
||||
description: string;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
type CreateWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export interface Topic {
|
||||
id: string;
|
||||
serviceName: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
type TopicsResponse =
|
||||
| {
|
||||
data: Topic[];
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
interface UpdateWebhookConfigRequest {
|
||||
destinationUrl?: string;
|
||||
topicIdsWithFilters?: { id: string; filters: object | null }[];
|
||||
description?: string;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
type UpdateWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
type DeleteWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export async function getWebhookConfigs(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
}): Promise<WebhookConfigsResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function createWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
config: CreateWebhookConfigRequest;
|
||||
}): Promise<CreateWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
|
||||
{
|
||||
body: JSON.stringify(props.config),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAvailableTopics(): Promise<TopicsResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/webhook-topics`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
webhookConfigId: string;
|
||||
config: UpdateWebhookConfigRequest;
|
||||
}): Promise<UpdateWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
|
||||
{
|
||||
body: JSON.stringify(props.config),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
webhookConfigId: string;
|
||||
}): Promise<DeleteWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
type TestDestinationUrlResponse =
|
||||
| {
|
||||
result: {
|
||||
httpStatusCode: number;
|
||||
httpResponseBody: string;
|
||||
};
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export async function testDestinationUrl(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
destinationUrl: string;
|
||||
}): Promise<TestDestinationUrlResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const resp = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/test-destination-url`,
|
||||
{
|
||||
body: JSON.stringify({ destinationUrl: props.destinationUrl }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
return {
|
||||
body: await resp.text(),
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -297,38 +297,53 @@ export type PaymentsResponse = {
|
||||
totalCount: number;
|
||||
};
|
||||
};
|
||||
|
||||
type BridgePaymentType = "buy" | "sell" | "transfer";
|
||||
type OnrampPaymentType = "onramp";
|
||||
|
||||
export type Payment = {
|
||||
// common
|
||||
id: string;
|
||||
blockNumber?: bigint;
|
||||
transactionId: string;
|
||||
createdAt: string;
|
||||
clientId: string;
|
||||
sender: string;
|
||||
receiver: string;
|
||||
developerFeeRecipient: string;
|
||||
developerFeeBps: number;
|
||||
transactions: Array<{
|
||||
chainId: number;
|
||||
transactionHash: string;
|
||||
}>;
|
||||
status: "PENDING" | "COMPLETED" | "FAILED" | "NOT_FOUND";
|
||||
type: "buy" | "sell" | "transfer";
|
||||
originAmount: bigint;
|
||||
destinationAmount: bigint;
|
||||
purchaseData: unknown;
|
||||
originToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
|
||||
destinationAmount: string;
|
||||
destinationToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
createdAt: string;
|
||||
};
|
||||
purchaseData: unknown;
|
||||
} & (
|
||||
| {
|
||||
type: BridgePaymentType;
|
||||
transactionId: string;
|
||||
blockNumber?: string;
|
||||
sender: string;
|
||||
developerFeeRecipient: string;
|
||||
developerFeeBps: number;
|
||||
originAmount: string;
|
||||
originToken: {
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
onrampId: string;
|
||||
type: OnrampPaymentType;
|
||||
}
|
||||
);
|
||||
|
||||
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
|
||||
|
||||
export async function getPayments(props: {
|
||||
clientId: string;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
import type { TokenMetadata } from "./types";
|
||||
|
||||
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");
|
||||
url.searchParams.append("includePrices", "false");
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
@@ -1,48 +1,8 @@
|
||||
"use server";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
export type TokenMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
iconUri?: string;
|
||||
};
|
||||
|
||||
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(), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} as Record<string, string>,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
import type { TokenMetadata } from "./types";
|
||||
|
||||
export async function addUniversalBridgeTokenRoute(props: {
|
||||
chainId: number;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export type TokenMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
iconUri?: string;
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export function ResponsiveTimeFilters(props: { defaultRange: DurationId }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-3 flex-col lg:flex-row">
|
||||
<div className="flex gap-3 flex-row">
|
||||
<DateRangeSelector
|
||||
className="rounded-full"
|
||||
range={range}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toast } from "sonner";
|
||||
import { reSubscribePlan } from "@/actions/billing";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { pollWithTimeout } from "@/utils/pollWithTimeout";
|
||||
import { tryCatch } from "@/utils/try-catch";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { DynamicHeight } from "../ui/DynamicHeight";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DownloadIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function ExportToCSVButton(props: {
|
||||
|
||||
@@ -1,87 +1,3 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
> & {
|
||||
skeleton?: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
src: string | undefined;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function Img(props: imgElementProps) {
|
||||
const [_status, setStatus] = useState<"pending" | "fallback" | "loaded">(
|
||||
"pending",
|
||||
);
|
||||
const status =
|
||||
props.src === undefined
|
||||
? "pending"
|
||||
: props.src === ""
|
||||
? "fallback"
|
||||
: _status;
|
||||
const { className, fallback, skeleton, containerClassName, ...restProps } =
|
||||
props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-accent" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const imgEl = imgRef.current;
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
if (imgEl.complete) {
|
||||
setStatus("loaded");
|
||||
} else {
|
||||
function handleLoad() {
|
||||
setStatus("loaded");
|
||||
}
|
||||
imgEl.addEventListener("load", handleLoad);
|
||||
return () => {
|
||||
imgEl.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("relative shrink-0", containerClassName)}>
|
||||
<img
|
||||
{...restProps}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
"fade-in-0 object-cover transition-opacity duration-300",
|
||||
className,
|
||||
)}
|
||||
decoding="async"
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
ref={imgRef}
|
||||
src={restProps.src || undefined}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
/>
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
)}
|
||||
style={restProps.style}
|
||||
>
|
||||
{status === "pending" && (skeleton || defaultSkeleton)}
|
||||
{status === "fallback" && (fallback || defaultFallback)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { Img } from "@workspace/ui/components/img";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function RouteDiscoveryCard(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SettingsCard(
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -5,16 +5,16 @@ import {
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import type { TokenMetadata } from "@/api/universal-bridge/tokens";
|
||||
import type { TokenMetadata } from "@/api/universal-bridge/types";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { fallbackChainIcon } from "@/constants/chain";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useTokensData } from "@/hooks/tokens";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo } from "react";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
@@ -6,11 +7,14 @@ import { TabButtons } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type CodeEnvironment =
|
||||
| "api"
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "react"
|
||||
| "react-native"
|
||||
| "unity";
|
||||
| "dotnet"
|
||||
| "unity"
|
||||
| "curl";
|
||||
|
||||
type SupportedEnvironment = {
|
||||
environment: CodeEnvironment;
|
||||
@@ -20,6 +24,10 @@ type SupportedEnvironment = {
|
||||
type CodeSnippet = Partial<Record<CodeEnvironment, string>>;
|
||||
|
||||
const Environments: SupportedEnvironment[] = [
|
||||
{
|
||||
environment: "api",
|
||||
title: "API",
|
||||
},
|
||||
{
|
||||
environment: "javascript",
|
||||
title: "JavaScript",
|
||||
@@ -36,10 +44,18 @@ const Environments: SupportedEnvironment[] = [
|
||||
environment: "react-native",
|
||||
title: "React Native",
|
||||
},
|
||||
{
|
||||
environment: "dotnet",
|
||||
title: ".NET",
|
||||
},
|
||||
{
|
||||
environment: "unity",
|
||||
title: "Unity",
|
||||
},
|
||||
{
|
||||
environment: "curl",
|
||||
title: "cURL",
|
||||
},
|
||||
];
|
||||
|
||||
interface CodeSegmentProps {
|
||||
@@ -122,9 +138,14 @@ export const CodeSegment: React.FC<CodeSegmentProps> = ({
|
||||
: activeEnvironment === "react" ||
|
||||
activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: activeEnvironment === "unity"
|
||||
: activeEnvironment === "unity" ||
|
||||
activeEnvironment === "dotnet"
|
||||
? "cpp"
|
||||
: activeEnvironment
|
||||
: activeEnvironment === "api"
|
||||
? "javascript"
|
||||
: activeEnvironment === "curl"
|
||||
? "bash"
|
||||
: activeEnvironment
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
|
||||
export function UnexpectedValueErrorMessage(props: {
|
||||
value: unknown;
|
||||
|
||||
@@ -91,7 +91,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
<main className="flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,225 +1 @@
|
||||
import { onlyText } from "react-children-utilities";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const MarkdownRenderer: React.FC<{
|
||||
markdownText: string;
|
||||
className?: string;
|
||||
code?: {
|
||||
disableCodeHighlight?: boolean;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
inlineCode?: {
|
||||
className?: string;
|
||||
};
|
||||
p?: {
|
||||
className?: string;
|
||||
};
|
||||
li?: {
|
||||
className?: string;
|
||||
};
|
||||
skipHtml?: boolean;
|
||||
}> = (markdownProps) => {
|
||||
const { markdownText, className, code } = markdownProps;
|
||||
const commonHeadingClassName = "mb-2 leading-5 font-semibold tracking-tight";
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: (props) => (
|
||||
<UnderlineLink
|
||||
href={props.href ?? "#"}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...cleanedProps(props)}
|
||||
className="mt-4"
|
||||
/>
|
||||
),
|
||||
|
||||
code: ({ ...props }) => {
|
||||
const codeStr = onlyText(props.children);
|
||||
|
||||
if (props?.className || codeStr.length > 100) {
|
||||
if (code?.disableCodeHighlight || !props.className) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
{/* @ts-expect-error - TODO: fix this */}
|
||||
<PlainTextCodeBlock
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const language = props.className.replace("language-", "");
|
||||
return (
|
||||
<div className="my-4">
|
||||
<CodeClient
|
||||
// @ts-expect-error - TODO: fix this
|
||||
lang={language}
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
ignoreFormattingErrors={code?.ignoreFormattingErrors}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineCode
|
||||
className={markdownProps.inlineCode?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
h1: (props) => (
|
||||
<h2
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mb-3 border-dashed border-b pb-2 text-xl md:text-2xl",
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
|
||||
h2: (props) => (
|
||||
<h3
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-8 mb-3 border-dashed border-b pb-2 text-lg md:text-xl",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h3: (props) => (
|
||||
<h4
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-4 text-base md:text-lg",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h4: (props) => (
|
||||
<h5
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h5: (props) => (
|
||||
<h6
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h6: (props) => (
|
||||
<p
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
hr: (props) => (
|
||||
<hr {...cleanedProps(props)} className="my-5 bg-border" />
|
||||
),
|
||||
li: ({ children: c, ...props }) => (
|
||||
<li
|
||||
className={cn(
|
||||
"text-muted-foreground leading-relaxed [&>p]:m-0",
|
||||
markdownProps.li?.className,
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
>
|
||||
{c}
|
||||
</li>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol
|
||||
className="mb-4 list-outside list-decimal pl-4 space-y-2 [&>li]:first:mt-2"
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
|
||||
p: (props) => (
|
||||
<p
|
||||
className={cn(
|
||||
"mb-3 text-muted-foreground leading-7",
|
||||
markdownProps.p?.className,
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
strong(props) {
|
||||
return <strong className="font-medium" {...cleanedProps(props)} />;
|
||||
},
|
||||
|
||||
table: (props) => (
|
||||
<div className="mb-6">
|
||||
<TableContainer>
|
||||
<Table {...cleanedProps(props)} />
|
||||
</TableContainer>
|
||||
</div>
|
||||
),
|
||||
tbody: (props) => <TableBody {...cleanedProps(props)} />,
|
||||
|
||||
td: (props) => (
|
||||
<TableCell {...cleanedProps(props)} className="text-left" />
|
||||
),
|
||||
|
||||
th: ({ children: c, ...props }) => (
|
||||
<TableHead
|
||||
{...cleanedProps(props)}
|
||||
className="text-left text-muted-foreground"
|
||||
>
|
||||
{c}
|
||||
</TableHead>
|
||||
),
|
||||
thead: (props) => <TableHeader {...cleanedProps(props)} />,
|
||||
tr: (props) => <TableRow {...cleanedProps(props)} />,
|
||||
ul: (props) => {
|
||||
return (
|
||||
<ul
|
||||
className="mb-4 list-outside list-disc pl-4 space-y-2 [&>li]:first:mt-2"
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={markdownProps.skipHtml}
|
||||
>
|
||||
{markdownText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function cleanedProps<T extends object & { node?: unknown }>(
|
||||
props: T,
|
||||
): Omit<T, "node"> {
|
||||
// biome-ignore lint/correctness/noUnusedVariables: EXPECTED
|
||||
const { node, ...rest } = props;
|
||||
return rest;
|
||||
}
|
||||
export { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useShowMore } from "@/hooks/useShowMore";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "../../ui/DynamicHeight";
|
||||
import { Spinner } from "../../ui/Spinner/Spinner";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
|
||||
export type MultiStepState<T extends string> = {
|
||||
id: T;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
ChevronDownIcon,
|
||||
CodeIcon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
type LinkType = "api" | "docs" | "playground" | "webhooks";
|
||||
|
||||
const linkTypeToLabel: Record<LinkType, string> = {
|
||||
api: "API Reference",
|
||||
docs: "Documentation",
|
||||
playground: "Playground",
|
||||
webhooks: "Webhooks",
|
||||
};
|
||||
|
||||
const linkTypeToOrder: Record<LinkType, number> = {
|
||||
docs: 0,
|
||||
playground: 1,
|
||||
api: 3,
|
||||
webhooks: 4,
|
||||
};
|
||||
|
||||
const linkTypeToIcon: Record<LinkType, React.FC<{ className?: string }>> = {
|
||||
api: CodeIcon,
|
||||
docs: BookTextIcon,
|
||||
playground: BoxIcon,
|
||||
webhooks: WebhookIcon,
|
||||
};
|
||||
|
||||
function orderLinks(links: ActionLink[]) {
|
||||
return links.slice().sort((a, b) => {
|
||||
const aIndex = linkTypeToOrder[a.type];
|
||||
const bIndex = linkTypeToOrder[b.type];
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}
|
||||
|
||||
export type ActionLink = {
|
||||
type: LinkType;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function LinkGroup(props: { links: ActionLink[] }) {
|
||||
const orderedLinks = useMemo(() => orderLinks(props.links), [props.links]);
|
||||
|
||||
if (orderedLinks.length === 1 && orderedLinks[0]) {
|
||||
const link = orderedLinks[0];
|
||||
const Icon = linkTypeToIcon[link.type];
|
||||
return (
|
||||
<Link
|
||||
href={link.href}
|
||||
target={link.href.startsWith("http") ? "_blank" : undefined}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border gap-2 bg-card"
|
||||
>
|
||||
<Icon className="size-3.5 text-muted-foreground" />
|
||||
{linkTypeToLabel[link.type]}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border gap-2 bg-card [&[data-state=open]>svg]:rotate-180"
|
||||
>
|
||||
Resources
|
||||
<ChevronDownIcon className="size-4 transition-transform duration-200 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="gap-1 flex flex-col w-48 rounded-xl"
|
||||
sideOffset={10}
|
||||
>
|
||||
{orderedLinks.map((link) => {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
const Icon = linkTypeToIcon[link.type];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={link.type}
|
||||
asChild
|
||||
className="flex flex-row items-center gap-2 cursor-pointer py-1.5"
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
{linkTypeToLabel[link.type]}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
type FooterCardProps,
|
||||
FooterLinksSection,
|
||||
} from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/footer/FooterLinksSection";
|
||||
|
||||
export type ProjectPageFooterProps = FooterCardProps;
|
||||
|
||||
export function ProjectPageFooter(props: ProjectPageFooterProps) {
|
||||
return (
|
||||
<footer className="container max-w-7xl">
|
||||
<FooterLinksSection {...props} />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { ArrowUpRightIcon, Settings2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type ActionLink, LinkGroup } from "./header/link-group";
|
||||
|
||||
type Action =
|
||||
| ({
|
||||
label: string;
|
||||
href: string;
|
||||
} & (
|
||||
| {
|
||||
external: true;
|
||||
icon?: never;
|
||||
}
|
||||
| {
|
||||
icon?: React.ReactNode;
|
||||
external?: false;
|
||||
}
|
||||
))
|
||||
| {
|
||||
component: React.ReactNode;
|
||||
};
|
||||
|
||||
function Action(props: { action: Action; variant?: "default" | "secondary" }) {
|
||||
const action = props.action;
|
||||
|
||||
return "component" in action ? (
|
||||
action.component
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
props.variant === "secondary" && "border border-border",
|
||||
)}
|
||||
size="sm"
|
||||
variant={props.variant}
|
||||
>
|
||||
<Link
|
||||
href={action.href}
|
||||
target={action.external ? "_blank" : undefined}
|
||||
rel={action.external ? "noopener noreferrer" : undefined}
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
{action.external && <ArrowUpRightIcon className="size-4" />}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export type ProjectPageHeaderProps = {
|
||||
client: ThirdwebClient;
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
imageUrl?: string | null;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
isProjectIcon?: boolean;
|
||||
actions: {
|
||||
primary: Action;
|
||||
secondary?: Action;
|
||||
} | null;
|
||||
|
||||
links?: ActionLink[];
|
||||
settings?: {
|
||||
href: string;
|
||||
};
|
||||
|
||||
// TODO: add task card component
|
||||
task?: never;
|
||||
};
|
||||
|
||||
export function ProjectPageHeader(props: ProjectPageHeaderProps) {
|
||||
return (
|
||||
<header className="container max-w-7xl py-6 relative">
|
||||
{/* top row */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
{/* left - icon */}
|
||||
<div className="flex">
|
||||
{props.isProjectIcon ? (
|
||||
<props.icon />
|
||||
) : (
|
||||
<div className="border rounded-full p-2.5 bg-card">
|
||||
<props.icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* right - buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{props.links && props.links.length > 0 && (
|
||||
<LinkGroup links={props.links} />
|
||||
)}
|
||||
|
||||
{props.settings && (
|
||||
<Link href={props.settings.href}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full gap-2 bg-card"
|
||||
>
|
||||
<Settings2Icon className="size-4 text-muted-foreground" />
|
||||
Settings
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* hide on mobile */}
|
||||
{props.actions && (
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
{props.actions.secondary && (
|
||||
<Action action={props.actions.secondary} variant="secondary" />
|
||||
)}
|
||||
|
||||
{props.actions.primary && (
|
||||
<Action action={props.actions.primary} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* mid row */}
|
||||
<div className="space-y-1 max-w-3xl">
|
||||
<h2 className="text-3xl font-semibold tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
{/* description */}
|
||||
<p className="text-sm text-muted-foreground line-clamp-3 md:line-clamp-2">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* bottom row - hidden on desktop */}
|
||||
{props.actions && (
|
||||
<div className="flex items-center gap-3 lg:hidden">
|
||||
{props.actions?.primary && (
|
||||
<Action action={props.actions.primary} />
|
||||
)}
|
||||
|
||||
{props.actions?.secondary && (
|
||||
<Action action={props.actions.secondary} variant="secondary" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type TabPathLink, TabPathLinks } from "../../ui/tabs";
|
||||
import {
|
||||
ProjectPageFooter,
|
||||
type ProjectPageFooterProps,
|
||||
} from "./project-page-footer";
|
||||
import {
|
||||
ProjectPageHeader,
|
||||
type ProjectPageHeaderProps,
|
||||
} from "./project-page-header";
|
||||
|
||||
type ProjectPageProps = {
|
||||
header: ProjectPageHeaderProps;
|
||||
footer?: ProjectPageFooterProps;
|
||||
/**
|
||||
* @deprecated only for legacy pages where we still need internal tabs for the moment, currently:
|
||||
* - /webhooks
|
||||
*/
|
||||
tabs?: TabPathLink[];
|
||||
};
|
||||
|
||||
export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
<div className={cn(!props.tabs && "border-b")}>
|
||||
<ProjectPageHeader {...props.header} />
|
||||
{props.tabs && (
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
links={props.tabs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<main className="container max-w-7xl pt-6">{props.children}</main>
|
||||
<div className="h-20" />
|
||||
{props.footer && (
|
||||
<div className="border-t mt-auto">
|
||||
<ProjectPageFooter {...props.footer} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { useShowMore } from "@/hooks/useShowMore";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function GenericLoadingPage({ className }: { className?: string }) {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function WalletAddress(props: {
|
||||
return (
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<div className="flex size-6 items-center justify-center rounded-full border bg-background">
|
||||
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
Invalid Address
|
||||
@@ -62,7 +62,7 @@ export function WalletAddress(props: {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<CircleSlashIcon
|
||||
className={cn("size-6 text-muted-foreground/70", props.iconClassName)}
|
||||
className={cn("size-5 text-muted-foreground/70", props.iconClassName)}
|
||||
/>
|
||||
<span
|
||||
className={cn("cursor-pointer font-mono text-sm", props.className)}
|
||||
@@ -208,6 +208,13 @@ function WalletAvatar(props: {
|
||||
<Img
|
||||
className={cn("size-5 object-cover", props.iconClassName)}
|
||||
src={resolvedAvatarSrc}
|
||||
fallback={
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
className={props.iconClassName}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : props.fallbackIcon ? (
|
||||
props.fallbackIcon
|
||||
@@ -215,7 +222,7 @@ function WalletAvatar(props: {
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
className={props.iconClassName}
|
||||
size={24}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "../../../app/(app)/components/ThirdwebMiniLogo";
|
||||
import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm";
|
||||
@@ -413,15 +413,15 @@ function StyledMarkdownRenderer(props: {
|
||||
}) {
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
className="[&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0"
|
||||
className="text-sm text-foreground [&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0 leading-relaxed"
|
||||
code={{
|
||||
className: "bg-card",
|
||||
ignoreFormattingErrors: true,
|
||||
}}
|
||||
inlineCode={{ className: "border-none" }}
|
||||
li={{ className: "text-foreground leading-relaxed" }}
|
||||
markdownText={props.text}
|
||||
p={{
|
||||
className: props.type === "assistant" ? "" : "leading-normal",
|
||||
className: "text-foreground leading-relaxed",
|
||||
}}
|
||||
skipHtml
|
||||
/>
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { fetchTopContracts, type TrendingContract } from "@/lib/search";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -65,6 +65,23 @@ export default function Component() {
|
||||
events: [preparedEvent]
|
||||
});
|
||||
}`,
|
||||
unity: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "{{contract_address}}",
|
||||
chainId: {{chainId}},
|
||||
);
|
||||
|
||||
// Listen to contract events
|
||||
var events = await contract.Events("{{function}}");`,
|
||||
dotnet: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebContract.Create(client, "{{contract_address}}", {{chainId}});
|
||||
|
||||
// Listen to contract events
|
||||
var events = await contract.Events("{{function}}");`,
|
||||
},
|
||||
install: {
|
||||
javascript: "npm i thirdweb",
|
||||
@@ -73,6 +90,9 @@ export default function Component() {
|
||||
unity: `// Download the .unitypackage from the latest release:
|
||||
// https://github.com/thirdweb-dev/unity-sdk/releases
|
||||
// and drag it into your project`,
|
||||
dotnet: `// Install the thirdweb SDK via NuGet:
|
||||
// dotnet add package Thirdweb
|
||||
// Or search for "Thirdweb" in your NuGet package manager`,
|
||||
},
|
||||
read: {
|
||||
javascript: `import { readContract } from "thirdweb";
|
||||
@@ -100,6 +120,33 @@ export default function Component() {
|
||||
params: [{{args}}]
|
||||
});
|
||||
}`,
|
||||
unity: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "{{contract_address}}",
|
||||
chainId: {{chainId}}
|
||||
);
|
||||
|
||||
// Read from the contract
|
||||
var result = await contract.Read<T>(
|
||||
"{{function}}",
|
||||
{{args}}
|
||||
);`,
|
||||
dotnet: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebContract.Create(
|
||||
client,
|
||||
"{{contract_address}}",
|
||||
{{chainId}}
|
||||
);
|
||||
|
||||
// Read from the contract
|
||||
var result = await contract.Read<T>(
|
||||
"{{function}}",
|
||||
{{args}}
|
||||
);`,
|
||||
},
|
||||
setup: {
|
||||
javascript: `import { createThirdwebClient, getContract } from "thirdweb";
|
||||
@@ -165,11 +212,19 @@ function App() {
|
||||
`,
|
||||
unity: `using Thirdweb;
|
||||
|
||||
// Reference the SDK
|
||||
var sdk = ThirdwebManager.Instance.SDK;
|
||||
// Get your contract
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "{{contract_address}}",
|
||||
chainId: {{chainId}}
|
||||
);`,
|
||||
dotnet: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = sdk.GetContract("{{contract_address}}");`,
|
||||
var contract = await ThirdwebContract.Create(
|
||||
client,
|
||||
"{{contract_address}}",
|
||||
{{chainId}}
|
||||
);`,
|
||||
},
|
||||
write: {
|
||||
javascript: `import { prepareContractCall, sendTransaction } from "thirdweb";
|
||||
@@ -213,6 +268,123 @@ export default function Component() {
|
||||
sendTransaction(transaction);
|
||||
}
|
||||
}`,
|
||||
unity: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebManager.Instance.GetContract(
|
||||
address: "{{contract_address}}",
|
||||
chainId: {{chainId}}
|
||||
);
|
||||
|
||||
// Write to the contract
|
||||
var transactionReceipt = await contract.Write(
|
||||
wallet,
|
||||
contract,
|
||||
"{{function}}",
|
||||
weiValue,
|
||||
{{args}}
|
||||
);`,
|
||||
dotnet: `using Thirdweb;
|
||||
|
||||
// Get your contract
|
||||
var contract = await ThirdwebContract.Create(
|
||||
client,
|
||||
"{{contract_address}}",
|
||||
{{chainId}}
|
||||
);
|
||||
|
||||
// Write to the contract
|
||||
var transactionReceipt = await contract.Write(
|
||||
wallet,
|
||||
contract,
|
||||
"{{function}}",
|
||||
weiValue,
|
||||
{{args}}
|
||||
);`,
|
||||
},
|
||||
api: {
|
||||
read: `const response = await fetch('https://api.thirdweb.com/v1/contracts/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-secret-key': '<YOUR_SECRET_KEY>'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
calls: [
|
||||
{
|
||||
contractAddress: "{{contract_address}}",
|
||||
method: "{{function}}",
|
||||
params: [{{args}}]
|
||||
}
|
||||
],
|
||||
chainId: {{chainId}}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();`,
|
||||
write: `const response = await fetch('https://api.thirdweb.com/v1/contracts/write', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-secret-key': '<YOUR_SECRET_KEY>'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
calls: [
|
||||
{
|
||||
contractAddress: "{{contract_address}}",
|
||||
method: "{{function}}",
|
||||
params: [{{args}}]
|
||||
}
|
||||
],
|
||||
chainId: {{chainId}},
|
||||
from: "<YOUR_WALLET_ADDRESS>"
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();`,
|
||||
events: `const response = await fetch('https://api.thirdweb.com/v1/contracts/{{chainId}}/{{contract_address}}/events?eventSignature={{function}}', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-secret-key': '<YOUR_SECRET_KEY>'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();`,
|
||||
},
|
||||
curl: {
|
||||
read: `curl https://api.thirdweb.com/v1/contracts/read \\
|
||||
--request POST \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--header 'x-secret-key: <YOUR_SECRET_KEY>' \\
|
||||
--data '{
|
||||
"calls": [
|
||||
{
|
||||
"contractAddress": "{{contract_address}}",
|
||||
"method": "{{function}}",
|
||||
"params": [{{args}}]
|
||||
}
|
||||
],
|
||||
"chainId": {{chainId}}
|
||||
}'`,
|
||||
write: `curl -X POST https://api.thirdweb.com/v1/contracts/write \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "x-secret-key: <YOUR_SECRET_KEY>" \\
|
||||
-d '{
|
||||
"calls": [
|
||||
{
|
||||
"contractAddress": "{{contract_address}}",
|
||||
"method": "{{function}}",
|
||||
"params": [{{args}}]
|
||||
}
|
||||
],
|
||||
"chainId": {{chainId}},
|
||||
"from": "<YOUR_WALLET_ADDRESS>"
|
||||
}'`,
|
||||
events: `curl https://api.thirdweb.com/v1/contracts/{{chainId}}/{{contract_address}}/events?eventSignature={{function}} \\
|
||||
--request GET \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--header 'x-secret-key: <YOUR_SECRET_KEY>'`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -269,20 +441,17 @@ return (
|
||||
|
||||
public async void ConnectWallet()
|
||||
{
|
||||
// Reference to your Thirdweb SDK
|
||||
var sdk = ThirdwebManager.Instance.SDK;
|
||||
|
||||
// Configure the connection
|
||||
var connection = new WalletConnection(
|
||||
provider: WalletProvider.SmartWallet, // The wallet provider you want to connect to (Required)
|
||||
chainId: 1, // The chain you want to connect to (Required)
|
||||
password: "myEpicPassword", // If using a local wallet as personal wallet (Optional)
|
||||
email: "[email protected]", // If using an email wallet as personal wallet (Optional)
|
||||
personalWallet: WalletProvider.LocalWallet // The personal wallet you want to use with your Account (Optional)
|
||||
var wallet = await ConnectWallet(
|
||||
new WalletOptions(
|
||||
provider: WalletProvider.InAppWallet,
|
||||
chainId: {{chainId}},
|
||||
inAppWalletOptions: new InAppWalletOptions(
|
||||
authprovider: AuthProvider.Google,
|
||||
executionMode: ExecutionMode.EIP7702Sponsored
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Connect the wallet
|
||||
string address = await sdk.wallet.Connect(connection);
|
||||
string address = await wallet.GetAddress();
|
||||
}`,
|
||||
},
|
||||
},
|
||||
@@ -490,6 +659,9 @@ export function formatSnippet(
|
||||
: "event",
|
||||
});
|
||||
break;
|
||||
case "api":
|
||||
// For API, we don't need special extension handling, just use the standard templates
|
||||
break;
|
||||
}
|
||||
}
|
||||
// end hacks on hacks on hacks -- now just hacks on hacks from here on out
|
||||
@@ -533,7 +705,7 @@ export function CodeOverview(props: {
|
||||
| undefined;
|
||||
|
||||
const [environment, setEnvironment] = useState<CodeEnvironment>(
|
||||
defaultEnvironment || "javascript",
|
||||
defaultEnvironment || "api",
|
||||
);
|
||||
|
||||
const [tab, setTab] = useState("write");
|
||||
@@ -677,7 +849,13 @@ export function CodeOverview(props: {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{environment === "react-native" || environment === "unity" ? (
|
||||
{environment === "api" || environment === "curl" ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No SDK install required for API/cURL. Call the HTTP endpoints
|
||||
directly. Keep your secret key server-side; use client IDs in
|
||||
frontends.
|
||||
</p>
|
||||
) : environment === "react-native" || environment === "unity" ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Install the latest version of the SDK.{" "}
|
||||
<UnderlineLink
|
||||
@@ -800,32 +978,76 @@ export function CodeOverview(props: {
|
||||
<CodeSegment
|
||||
environment={environment}
|
||||
setEnvironment={setEnvironment}
|
||||
snippet={formatSnippet(
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
COMMANDS[tab as keyof typeof COMMANDS] as any,
|
||||
{
|
||||
snippet={(() => {
|
||||
const selectedFn =
|
||||
tab === "read" ? read : tab === "write" ? write : event;
|
||||
const commandsKey = tab as "read" | "write" | "events";
|
||||
|
||||
const baseSnippet = formatSnippet(COMMANDS[commandsKey], {
|
||||
args:
|
||||
abi
|
||||
?.filter(
|
||||
(f) => f.type === "function" || f.type === "event",
|
||||
)
|
||||
?.find(
|
||||
(f) =>
|
||||
f.name ===
|
||||
(tab === "read"
|
||||
? read?.name
|
||||
: tab === "write"
|
||||
? write?.name
|
||||
: event?.name),
|
||||
)
|
||||
?.find((f) => f.name === selectedFn?.name)
|
||||
?.inputs.map((i) => i.name || "") || [],
|
||||
|
||||
chainId,
|
||||
contractAddress,
|
||||
extensionNamespace,
|
||||
fn: tab === "read" ? read : tab === "write" ? write : event,
|
||||
},
|
||||
)}
|
||||
fn: selectedFn,
|
||||
});
|
||||
|
||||
// Add API snippet if it exists
|
||||
const apiSnippet = COMMANDS.api[commandsKey]
|
||||
? formatSnippet(
|
||||
{ api: COMMANDS.api[commandsKey] },
|
||||
{
|
||||
args:
|
||||
abi
|
||||
?.filter(
|
||||
(f) =>
|
||||
f.type === "function" || f.type === "event",
|
||||
)
|
||||
?.find((f) => f.name === selectedFn?.name)
|
||||
?.inputs.map((i) => i.name || "") || [],
|
||||
|
||||
chainId,
|
||||
contractAddress,
|
||||
extensionNamespace,
|
||||
fn: selectedFn,
|
||||
},
|
||||
)
|
||||
: {};
|
||||
|
||||
// Add cURL snippet if it exists
|
||||
const curlSnippet = COMMANDS.curl[commandsKey]
|
||||
? formatSnippet(
|
||||
{ curl: COMMANDS.curl[commandsKey] },
|
||||
{
|
||||
args:
|
||||
abi
|
||||
?.filter(
|
||||
(f) =>
|
||||
f.type === "function" || f.type === "event",
|
||||
)
|
||||
?.find((f) => f.name === selectedFn?.name)
|
||||
?.inputs.map((i) => i.name || "") || [],
|
||||
|
||||
chainId,
|
||||
contractAddress,
|
||||
extensionNamespace,
|
||||
fn: selectedFn,
|
||||
},
|
||||
)
|
||||
: {};
|
||||
|
||||
return {
|
||||
...baseSnippet,
|
||||
...apiSnippet,
|
||||
...curlSnippet,
|
||||
};
|
||||
})()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ const ContractFunction: React.FC<{
|
||||
|
||||
function ContractFunctionInner(props: ContractFunctionProps) {
|
||||
const { contract, fn } = props;
|
||||
const [environment, setEnvironment] = useState<CodeEnvironment>("javascript");
|
||||
const [environment, setEnvironment] = useState<CodeEnvironment>("api");
|
||||
const functionSelectorQuery = useContractFunctionSelectors(contract);
|
||||
const functionSelectors = functionSelectorQuery.data || [];
|
||||
const isERC721Query = useReadContract(ERC721Ext.isERC721, { contract });
|
||||
@@ -112,7 +112,7 @@ function ContractFunctionInner(props: ContractFunctionProps) {
|
||||
: "write"
|
||||
: ("events" as const);
|
||||
|
||||
const codeSnippet = formatSnippet(COMMANDS[commandsKey], {
|
||||
const baseSnippet = formatSnippet(COMMANDS[commandsKey], {
|
||||
args: fn.inputs?.map((i) => i.name || ""),
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
@@ -120,6 +120,40 @@ function ContractFunctionInner(props: ContractFunctionProps) {
|
||||
fn,
|
||||
});
|
||||
|
||||
// Safety check: Only include API snippet if the command exists in COMMANDS.api
|
||||
const apiSnippet = COMMANDS.api[commandsKey]
|
||||
? formatSnippet(
|
||||
{ api: COMMANDS.api[commandsKey] },
|
||||
{
|
||||
args: fn.inputs?.map((i) => i.name || ""),
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
extensionNamespace,
|
||||
fn,
|
||||
},
|
||||
)
|
||||
: {};
|
||||
|
||||
// Safety check: Only include cURL snippet if the command exists in COMMANDS.curl
|
||||
const curlSnippet = COMMANDS.curl[commandsKey]
|
||||
? formatSnippet(
|
||||
{ curl: COMMANDS.curl[commandsKey] },
|
||||
{
|
||||
args: fn.inputs?.map((i) => i.name || ""),
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
extensionNamespace,
|
||||
fn,
|
||||
},
|
||||
)
|
||||
: {};
|
||||
|
||||
const codeSnippet = {
|
||||
...baseSnippet,
|
||||
...apiSnippet,
|
||||
...curlSnippet,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center flex-wrap gap-2 border-b pb-3 mb-3">
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
toSerializableTransaction,
|
||||
toWei,
|
||||
} from "thirdweb";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { parseAbiParams, stringify, toFunctionSelector } from "thirdweb/utils";
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { SolidityInput } from "@/components/solidity-inputs";
|
||||
@@ -31,9 +31,10 @@ import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
|
||||
function formatResponseData(data: unknown): {
|
||||
type: "json" | "text";
|
||||
@@ -264,7 +265,7 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
data: mutationData,
|
||||
error: mutationError,
|
||||
isPending: mutationLoading,
|
||||
} = useSendAndConfirmTransaction();
|
||||
} = useSendAndConfirmTx();
|
||||
|
||||
const {
|
||||
mutate: readFn,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useChainSlug } from "@/hooks/chains/chainSlug";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
@@ -231,13 +231,13 @@ function ImportForm(props: {
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="gap-2" type="submit">
|
||||
{addContractToProject.isPending ? (
|
||||
{form.formState.isSubmitting ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<ArrowDownToLineIcon className="size-4" />
|
||||
)}
|
||||
|
||||
{addContractToProject.isPending ? "Importing" : "Import"}
|
||||
{form.formState.isSubmitting ? "Importing" : "Import"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SearchIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import type { WalletUser } from "thirdweb/wallets";
|
||||
import { TWTable } from "@/components/blocks/TWTable";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
ToolTipLabel,
|
||||
Tooltip,
|
||||
|
||||
@@ -1,7 +1,79 @@
|
||||
import type {
|
||||
ListUserWalletsData,
|
||||
ListUserWalletsResponses,
|
||||
} from "@thirdweb-dev/api";
|
||||
import { configure, listUserWallets } from "@thirdweb-dev/api";
|
||||
import type { WalletUser } from "thirdweb/wallets";
|
||||
import { THIRDWEB_EWS_API_HOST } from "@/constants/urls";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
// Configure the API client to use the correct base URL
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
// Extract types from the generated API
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
| { email: string; [key: string]: string }
|
||||
| { phone: string; [key: string]: string }
|
||||
| { address: string; [key: string]: string }
|
||||
| { id: string; [key: string]: string };
|
||||
|
||||
const profileId = getProfileId(profile);
|
||||
|
||||
if ("email" in profile && profile.email) {
|
||||
details = { email: profile.email, id: profileId };
|
||||
} else if ("phone" in profile && profile.phone) {
|
||||
details = { phone: profile.phone, id: profileId };
|
||||
} else if ("walletAddress" in profile && profile.walletAddress) {
|
||||
details = { address: profile.walletAddress, id: profileId };
|
||||
} else {
|
||||
details = { id: profileId };
|
||||
}
|
||||
|
||||
return {
|
||||
type: profile.type,
|
||||
details,
|
||||
};
|
||||
}),
|
||||
wallets: apiWallet.address
|
||||
? [
|
||||
{
|
||||
address: apiWallet.address,
|
||||
createdAt: apiWallet.createdAt || new Date().toISOString(),
|
||||
type: "enclave" as const,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to safely get ID from any profile type
|
||||
function getProfileId(profile: APIProfile | undefined): string {
|
||||
if (!profile) return "";
|
||||
|
||||
if ("id" in profile) {
|
||||
return profile.id;
|
||||
} else if ("credentialId" in profile) {
|
||||
return profile.credentialId;
|
||||
} else if ("identifier" in profile) {
|
||||
return profile.identifier;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function searchUsers(
|
||||
authToken: string,
|
||||
clientId: string | undefined,
|
||||
@@ -10,50 +82,57 @@ export async function searchUsers(
|
||||
searchType: SearchType,
|
||||
query: string,
|
||||
): Promise<WalletUser[]> {
|
||||
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
|
||||
try {
|
||||
// Prepare query parameters
|
||||
const queryParams: ListUserWalletsData["query"] = {
|
||||
limit: 50,
|
||||
};
|
||||
|
||||
// Add clientId or ecosystemSlug parameter
|
||||
if (ecosystemSlug) {
|
||||
url.searchParams.append("ecosystemSlug", `ecosystem.${ecosystemSlug}`);
|
||||
} else if (clientId) {
|
||||
url.searchParams.append("clientId", clientId);
|
||||
// Add search parameter based on search type
|
||||
switch (searchType) {
|
||||
case "email":
|
||||
queryParams.email = query;
|
||||
break;
|
||||
case "phone":
|
||||
queryParams.phone = query;
|
||||
break;
|
||||
case "id":
|
||||
queryParams.id = query;
|
||||
break;
|
||||
case "address":
|
||||
queryParams.address = query;
|
||||
break;
|
||||
case "externalWallet":
|
||||
queryParams.externalWalletAddress = query;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the generated API function with Bearer authentication
|
||||
const response = await listUserWallets({
|
||||
query: queryParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-thirdweb-team-id": teamId,
|
||||
...(clientId && { "x-client-id": clientId }),
|
||||
...(ecosystemSlug && {
|
||||
"x-ecosystem-id": `ecosystem.${ecosystemSlug}`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Handle response
|
||||
if (response.error || !response.data) {
|
||||
console.error(
|
||||
"Error searching users:",
|
||||
response.error || "No data returned",
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.data.result.wallets.map(transformToWalletUser);
|
||||
} catch (error) {
|
||||
console.error("Error searching users:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Add search parameter based on search type
|
||||
switch (searchType) {
|
||||
case "email":
|
||||
url.searchParams.append("email", query);
|
||||
break;
|
||||
case "phone":
|
||||
url.searchParams.append("phone", query);
|
||||
break;
|
||||
case "id":
|
||||
url.searchParams.append("id", query);
|
||||
break;
|
||||
case "address":
|
||||
url.searchParams.append("address", query);
|
||||
break;
|
||||
case "externalWallet":
|
||||
url.searchParams.append("externalWalletAddress", query);
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-thirdweb-team-id": teamId,
|
||||
...(clientId && { "x-client-id": clientId }),
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to search users: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.users as WalletUser[];
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { SettingsIcon } from "lucide-react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { UseNetworkSwitcherModalOptions } from "thirdweb/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { OPSponsoredChains } from "@/constants/chains";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -32,7 +31,6 @@ export const CustomChainRenderer = ({
|
||||
const { idToChain } = useAllChainsData();
|
||||
const storedChain = idToChain.get(chain.id);
|
||||
const isDeprecated = storedChain?.status === "deprecated";
|
||||
const isSponsored = OPSponsoredChains.includes(chain.id);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[48px] w-full cursor-pointer justify-start rounded-lg px-2 py-1 hover:bg-accent">
|
||||
@@ -70,16 +68,6 @@ export const CustomChainRenderer = ({
|
||||
Deprecated
|
||||
</div>
|
||||
)}
|
||||
{isSponsored && (
|
||||
<div
|
||||
className="flex shrink-0 cursor-not-allowed items-center overflow-hidden rounded-xl px-2 py-1 font-medium text-xs"
|
||||
style={{
|
||||
background: "linear-gradient(to right, #701953, #5454B2)",
|
||||
}}
|
||||
>
|
||||
Sponsored
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{switching && (
|
||||
<div className="flex items-center gap-1 font-medium text-link-foreground text-xs">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { FileInput } from "@/components/blocks/FileInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
|
||||
|
||||
export const IconUpload: React.FC<{
|
||||
|
||||
@@ -56,7 +56,8 @@ export function NotificationsButton(props: { accountId: string }) {
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="max-h-[90vh] min-h-[500px] w-[400px] max-w-md p-0"
|
||||
sideOffset={8}
|
||||
className="max-h-[90vh] min-h-[550px] w-[450px] max-w-md p-0 flex flex-col rounded-xl overflow-hidden"
|
||||
>
|
||||
<NotificationList {...manager} />
|
||||
</PopoverContent>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseISO,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import { ArchiveIcon } from "lucide-react";
|
||||
import { ArchiveIcon, ArrowUpRightIcon, BellIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import type { Notification } from "@/api/notifications";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -42,39 +42,48 @@ export function NotificationEntry({
|
||||
}, [notification.createdAt]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row py-1.5">
|
||||
{onMarkAsRead && (
|
||||
<div className="min-h-full w-1 shrink-0 rounded-r-lg bg-primary" />
|
||||
)}
|
||||
<div className="flex w-full flex-row justify-between gap-2 border-b px-4 py-2 transition-colors last:border-b-0">
|
||||
<div className="flex items-start gap-3 p-4 border-b group hover:bg-card last:border-b-0">
|
||||
<div className="p-2.5 rounded-full border bg-card">
|
||||
<BellIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-between gap-2 transition-colors">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<p className="text-sm">{notification.description}</p>
|
||||
{timeAgo && (
|
||||
<p className="text-muted-foreground text-xs">{timeAgo}</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between gap-2 pt-1">
|
||||
<Button asChild className="px-0" size="sm" variant="link">
|
||||
<div className="flex flex-row justify-between gap-2 pt-2">
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-full bg-card gap-2"
|
||||
>
|
||||
<a
|
||||
href={notification.ctaUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{notification.ctaText}
|
||||
<ArrowUpRightIcon className="size-4 text-muted-foreground" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{onMarkAsRead && (
|
||||
<Button
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onMarkAsRead(notification.id)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="p-2 h-auto opacity-0 group-hover:opacity-100 rounded-full text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onMarkAsRead(notification.id)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Archive notification"
|
||||
>
|
||||
<ArchiveIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,15 +21,15 @@ export function NotificationList(props: ReturnType<typeof useNotifications>) {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
<div className="flex flex-col overflow-hidden grow">
|
||||
<TabButtons
|
||||
tabClassName="!text-sm hover:!bg-transparent !px-0 !py-1"
|
||||
tabContainerClassName="space-x-6 px-4 pt-2"
|
||||
tabClassName="hover:bg-transparent"
|
||||
tabContainerClassName="pt-1.5 px-4"
|
||||
tabs={[
|
||||
{
|
||||
isActive: activeTab === "inbox",
|
||||
name: (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
Inbox
|
||||
{props.unreadNotificationsCount > 0 && (
|
||||
<Badge variant="secondary">
|
||||
@@ -49,7 +49,7 @@ export function NotificationList(props: ReturnType<typeof useNotifications>) {
|
||||
/>
|
||||
|
||||
<div
|
||||
className="h-full w-full flex-1 overflow-y-auto"
|
||||
className="flex-1 overflow-y-auto overscroll-contain flex flex-col grow"
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
{activeTab === "inbox" ? (
|
||||
@@ -93,9 +93,9 @@ function InboxTab(
|
||||
},
|
||||
) {
|
||||
return props.unreadNotifications.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
|
||||
<BellIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex grow flex-col items-center justify-center gap-4 p-4 text-center">
|
||||
<div className="p-2 bg-card rounded-full border">
|
||||
<BellIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">No new notifications</p>
|
||||
</div>
|
||||
@@ -131,9 +131,9 @@ function ArchiveTab(
|
||||
},
|
||||
) {
|
||||
return props.archivedNotifications.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
|
||||
<ArchiveIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex grow flex-col items-center justify-center gap-4 p-4 text-center">
|
||||
<div className="p-2 bg-card rounded-full border">
|
||||
<ArchiveIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">No archived notifications</p>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { createProjectClient } from "@/hooks/useApi";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { isAddress, isValidENSName } from "thirdweb/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useEns } from "@/hooks/contract-hooks";
|
||||
import type { SolidityInputProps } from ".";
|
||||
import { validateAddress } from "./helpers";
|
||||
|
||||
@@ -12,6 +12,7 @@ import { forwardRef, useCallback, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
prepareTransaction,
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
sendTransaction,
|
||||
type ThirdwebClient,
|
||||
toWei,
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { LOCAL_NODE_PKEY } from "@/constants/misc";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { ScrollShadow } from "@workspace/ui/components/scroll-shadow";
|
||||
@@ -1,36 +0,0 @@
|
||||
.scrollShadowY {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollShadowX {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollShadowTop {
|
||||
top: 0;
|
||||
background: linear-gradient(to bottom, var(--shadow), transparent);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.scrollShadowBottom {
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, var(--shadow), transparent);
|
||||
}
|
||||
|
||||
.scrollShadowLeft {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, var(--shadow), transparent);
|
||||
}
|
||||
|
||||
.scrollShadowRight {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, var(--shadow), transparent);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import styles from "./ScrollShadow.module.css";
|
||||
|
||||
export function ScrollShadow(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
disableTopShadow?: boolean;
|
||||
shadowColor?: string;
|
||||
shadowClassName?: string;
|
||||
}) {
|
||||
const scrollableEl = useRef<HTMLDivElement>(null);
|
||||
const shadowTopEl = useRef<HTMLDivElement>(null);
|
||||
const shadowBottomEl = useRef<HTMLDivElement>(null);
|
||||
const shadowLeftEl = useRef<HTMLDivElement>(null);
|
||||
const shadowRightEl = useRef<HTMLDivElement>(null);
|
||||
const wrapperEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const content = scrollableEl.current;
|
||||
const shadowTop = shadowTopEl.current;
|
||||
const shadowBottom = shadowBottomEl.current;
|
||||
const wrapper = wrapperEl.current;
|
||||
const shadowLeft = shadowLeftEl.current;
|
||||
const shadowRight = shadowRightEl.current;
|
||||
|
||||
if (!content || !shadowTop || !shadowBottom || !wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (
|
||||
!content ||
|
||||
!shadowTop ||
|
||||
!shadowBottom ||
|
||||
!shadowLeft ||
|
||||
!shadowRight ||
|
||||
!wrapper
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScrollHeight = content.scrollHeight - wrapper.offsetHeight;
|
||||
const contentScrollWidth = content.scrollWidth - wrapper.offsetWidth;
|
||||
|
||||
if (contentScrollHeight > 10) {
|
||||
const currentScroll = content.scrollTop / contentScrollHeight;
|
||||
shadowTop.style.opacity = `${currentScroll}`;
|
||||
shadowBottom.style.opacity = `${1 - currentScroll}`;
|
||||
} else {
|
||||
shadowTop.style.opacity = "0";
|
||||
shadowBottom.style.opacity = "0";
|
||||
}
|
||||
|
||||
if (contentScrollWidth > 10) {
|
||||
const currentScrollX = content.scrollLeft / contentScrollWidth;
|
||||
shadowLeft.style.opacity = `${currentScrollX}`;
|
||||
shadowRight.style.opacity = `${1 - currentScrollX}`;
|
||||
} else {
|
||||
shadowLeft.style.opacity = "0";
|
||||
shadowRight.style.opacity = "0";
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
content.addEventListener("scroll", handleScroll);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
resizeObserver.observe(content);
|
||||
return () => {
|
||||
content.removeEventListener("scroll", handleScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(props.className, "relative overflow-hidden")}
|
||||
ref={wrapperEl}
|
||||
style={
|
||||
{
|
||||
"--shadow": props.shadowColor || "hsl(var(--muted))",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowTop,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowTopEl}
|
||||
style={{
|
||||
display: props.disableTopShadow ? "none" : "block",
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowBottom,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowBottomEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowLeft,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowLeftEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowRight,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowRightEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn("no-scrollbar overflow-auto", props.scrollableClassName)}
|
||||
data-scrollable
|
||||
ref={scrollableEl}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Spinner } from "@workspace/ui/components/spinner";
|
||||
@@ -1,37 +0,0 @@
|
||||
.loader {
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.loader circle {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #fff;
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type LinkProps = React.ComponentProps<typeof Link>;
|
||||
|
||||
export function UnderlineLink(props: LinkProps) {
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(
|
||||
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
|
||||
@@ -1,39 +1,2 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 leading-4",
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary/20 text-primary",
|
||||
destructive:
|
||||
"border-transparent dark:bg-red-950 dark:text-red-400 bg-red-500/20 text-red-800",
|
||||
outline: "text-foreground",
|
||||
secondary:
|
||||
"border-transparent bg-accent text-accent-foreground hover:bg-accent/80",
|
||||
success:
|
||||
"border-transparent dark:bg-green-950/50 dark:text-green-400 bg-green-200 text-green-950",
|
||||
warning:
|
||||
"border-transparent dark:bg-yellow-600/20 dark:text-yellow-500 bg-yellow-500/20 text-yellow-900",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export type { BadgeProps } from "@workspace/ui/components/badge";
|
||||
export { Badge, badgeVariants } from "@workspace/ui/components/badge";
|
||||
|
||||
@@ -1,119 +1,9 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav aria-label="breadcrumb" ref={ref} {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: required
|
||||
<span
|
||||
aria-current="page"
|
||||
aria-disabled="true"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
ref={ref}
|
||||
role="link"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
} from "@workspace/ui/components/breadcrumb";
|
||||
|
||||
@@ -1,71 +1,3 @@
|
||||
"use client";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
import { PlainTextCodeBlock } from "./plaintext-code";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
keepPreviousDataOnCodeChange?: boolean;
|
||||
copyButtonClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
onCopy?: (code: string) => void;
|
||||
};
|
||||
|
||||
export const CodeClient: React.FC<CodeProps> = ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
scrollableClassName,
|
||||
keepPreviousDataOnCodeChange = false,
|
||||
copyButtonClassName,
|
||||
ignoreFormattingErrors,
|
||||
scrollableContainerClassName,
|
||||
shadowColor,
|
||||
onCopy,
|
||||
}) => {
|
||||
const codeQuery = useQuery({
|
||||
placeholderData: keepPreviousDataOnCodeChange
|
||||
? keepPreviousData
|
||||
: undefined,
|
||||
queryFn: () =>
|
||||
getCodeHtml(code, lang, {
|
||||
ignoreFormattingErrors: ignoreFormattingErrors,
|
||||
}),
|
||||
queryKey: ["html", code],
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (!codeQuery.data) {
|
||||
return (
|
||||
<PlainTextCodeBlock
|
||||
className={className}
|
||||
code={code}
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
onCopy={onCopy}
|
||||
scrollableClassName={scrollableClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCode
|
||||
className={className}
|
||||
code={codeQuery.data.formattedCode}
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
html={codeQuery.data.html}
|
||||
onCopy={onCopy}
|
||||
scrollableClassName={scrollableClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
};
|
||||
|
||||
export const CodeServer: React.FC<CodeProps> = async ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
ignoreFormattingErrors,
|
||||
}) => {
|
||||
const { html, formattedCode } = await getCodeHtml(code, lang, {
|
||||
ignoreFormattingErrors,
|
||||
});
|
||||
return <RenderCode className={className} code={formattedCode} html={html} />;
|
||||
};
|
||||
export {
|
||||
type CodeProps,
|
||||
CodeServer,
|
||||
} from "@workspace/ui/components/code/code.server";
|
||||
|
||||
@@ -1,29 +1 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CodeBlockContainer } from "./CodeBlockContainer";
|
||||
|
||||
export function PlainTextCodeBlock(props: {
|
||||
code: string;
|
||||
copyButtonClassName?: string;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
codeClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
onCopy?: (code: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<CodeBlockContainer
|
||||
className={props.className}
|
||||
codeToCopy={props.code}
|
||||
copyButtonClassName={props.copyButtonClassName}
|
||||
onCopy={props.onCopy}
|
||||
scrollableClassName={props.scrollableClassName}
|
||||
scrollableContainerClassName={props.scrollableContainerClassName}
|
||||
shadowColor={props.shadowColor}
|
||||
>
|
||||
<code className={cn("block whitespace-pre", props.codeClassName)}>
|
||||
{props.code}
|
||||
</code>
|
||||
</CodeBlockContainer>
|
||||
);
|
||||
}
|
||||
export { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
|
||||
|
||||
@@ -1,25 +1 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-inverted selection:text-inverted-foreground file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
export { Input } from "@workspace/ui/components/input";
|
||||
|
||||
@@ -1,156 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<table
|
||||
className={cn(
|
||||
"w-full caption-bottom border-collapse align-top text-sm lining-nums tabular-nums",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
className={cn("relative border-b bg-background", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
className={cn(
|
||||
"border-border border-t bg-card font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
/**
|
||||
* Contain the absolutely position elements inside the row with position:relative + transform:translate(0)
|
||||
* transform:translate(0) is required because position:relative on tr element does not work on webkit
|
||||
*/
|
||||
linkBox?: boolean;
|
||||
}
|
||||
>(({ className, linkBox, ...props }, ref) => (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-border border-b last:border-0 data-[state=selected]:bg-muted",
|
||||
linkBox && "relative translate-x-0 translate-y-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
className={cn(
|
||||
"px-6 py-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
className={cn(
|
||||
"px-6 py-4 text-start align-middle text-sm [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
className={cn("py-4 text-muted-foreground text-sm", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
function TableContainer(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ScrollShadow
|
||||
className={cn(
|
||||
"relative whitespace-nowrap rounded-lg border border-border bg-card",
|
||||
props.className,
|
||||
)}
|
||||
disableTopShadow
|
||||
scrollableClassName={props.scrollableContainerClassName}
|
||||
shadowClassName="z-30"
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
{props.children}
|
||||
</ScrollShadow>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
};
|
||||
} from "@workspace/ui/components/table";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "./ScrollShadow";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export type TabLink = {
|
||||
@@ -216,14 +216,16 @@ function useUnderline<El extends HTMLElement>() {
|
||||
return { activeTabRef, containerRef, lineRef };
|
||||
}
|
||||
|
||||
export type TabPathLink = {
|
||||
name: React.ReactNode;
|
||||
path: string;
|
||||
exactMatch?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
export function TabPathLinks(props: {
|
||||
links: {
|
||||
name: React.ReactNode;
|
||||
path: string;
|
||||
exactMatch?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isActive?: (pathname: string) => boolean;
|
||||
}[];
|
||||
links: TabPathLink[];
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
|
||||
@@ -1,70 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
className={cn(
|
||||
"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
export function ToolTipLabel(props: {
|
||||
children: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
hoverable?: boolean;
|
||||
contentClassName?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
if (!props.label) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100} disableHoverableContent={!props.hoverable}>
|
||||
<TooltipTrigger asChild className="!pointer-events-auto" tabIndex={-1}>
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align={props.align}
|
||||
className={cn(
|
||||
"max-w-[400px] whitespace-normal p-0 leading-relaxed",
|
||||
props.contentClassName,
|
||||
)}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 p-4 text-sm">
|
||||
{props.leftIcon}
|
||||
{props.label}
|
||||
{props.rightIcon}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
export {
|
||||
ToolTipLabel,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@workspace/ui/components/tooltip";
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// TODO: move to API
|
||||
export const OPSponsoredChains = [
|
||||
// Optimism
|
||||
10,
|
||||
// Base
|
||||
8453,
|
||||
// Zora
|
||||
7777777,
|
||||
// Mode
|
||||
34443,
|
||||
// Frax
|
||||
252,
|
||||
// Cyber
|
||||
7560,
|
||||
// Redstone
|
||||
690,
|
||||
// Ancient8
|
||||
888888888,
|
||||
//Donatuz
|
||||
42026,
|
||||
//Mantle
|
||||
5000,
|
||||
];
|
||||
@@ -3,3 +3,4 @@ export const COOKIE_PREFIX_TOKEN = "tw_token_";
|
||||
|
||||
export const LAST_USED_PROJECT_ID = "last-used-project-id";
|
||||
export const LAST_USED_TEAM_ID = "last-used-team-id";
|
||||
export const HAS_USED_DASHBOARD = "has-used-dashboard";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const THIRDWEB_EWS_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_EWS_API_HOST ||
|
||||
"https://in-app-wallet.thirdweb.com";
|
||||
export const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb-dev.com";
|
||||
|
||||
export const THIRDWEB_PAY_DOMAIN =
|
||||
process.env.NEXT_PUBLIC_PAY_URL || "pay.thirdweb-dev.com";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { Chain, ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
@@ -27,6 +27,17 @@ export function useV5DashboardChain(
|
||||
}, [chainId, idToChain]);
|
||||
}
|
||||
|
||||
export function useGetV5DashboardChain() {
|
||||
const { idToChain } = useAllChainsData();
|
||||
return useCallback(
|
||||
(chainId: number) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return defineDashboardChain(chainId, idToChain.get(chainId));
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* same behavior as v4 `useChain()` but for v5
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUniversalBridgeTokens } from "@/api/universal-bridge/tokens";
|
||||
import { getUniversalBridgeTokens } from "@/api/universal-bridge/token-list";
|
||||
|
||||
export function useTokensData({
|
||||
chainId,
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
ListUserWalletsData,
|
||||
ListUserWalletsResponses,
|
||||
} from "@thirdweb-dev/api";
|
||||
import { configure, listUserWallets } from "@thirdweb-dev/api";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import type { WalletUser } from "thirdweb/wallets";
|
||||
import { THIRDWEB_EWS_API_HOST } from "@/constants/urls";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
import { embeddedWalletsKeys } from "../query-keys/cache-keys";
|
||||
|
||||
// Configure the API client to use the correct base URL
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
// Extract types from the generated API
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
const fetchAccountList = ({
|
||||
jwt,
|
||||
clientId,
|
||||
@@ -18,37 +34,104 @@ const fetchAccountList = ({
|
||||
pageNumber: number;
|
||||
}) => {
|
||||
return async () => {
|
||||
const url = new URL(`${THIRDWEB_EWS_API_HOST}/api/2024-05-05/account/list`);
|
||||
try {
|
||||
// Prepare query parameters for the new API
|
||||
const queryParams: ListUserWalletsData["query"] = {
|
||||
page: pageNumber,
|
||||
limit: 50, // Keep the same page size
|
||||
};
|
||||
|
||||
// Add clientId or ecosystemSlug parameter
|
||||
if (ecosystemSlug) {
|
||||
url.searchParams.append("ecosystemSlug", `ecosystem.${ecosystemSlug}`);
|
||||
} else if (clientId) {
|
||||
url.searchParams.append("clientId", clientId);
|
||||
// Use the generated API function with Bearer authentication
|
||||
const response = await listUserWallets({
|
||||
query: queryParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-thirdweb-team-id": teamId,
|
||||
...(clientId && { "x-client-id": clientId }),
|
||||
...(ecosystemSlug && {
|
||||
"x-ecosystem-id": `ecosystem.${ecosystemSlug}`,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Handle response
|
||||
if (response.error || !response.data) {
|
||||
const errorMessage =
|
||||
typeof response.error === "string"
|
||||
? response.error
|
||||
: "No data returned";
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Transform the response to match the expected format
|
||||
return {
|
||||
users: response.data.result.wallets.map(transformToWalletUser),
|
||||
hasMore: response.data.result.pagination.hasMore ?? false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch wallets:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
url.searchParams.append("page", pageNumber.toString());
|
||||
|
||||
const res = await fetch(url.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-thirdweb-team-id": teamId,
|
||||
...(clientId && { "x-client-id": clientId }),
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch wallets: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json as {
|
||||
users: WalletUser[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
| { email: string; [key: string]: string }
|
||||
| { phone: string; [key: string]: string }
|
||||
| { address: string; [key: string]: string }
|
||||
| { id: string; [key: string]: string };
|
||||
|
||||
const profileId = getProfileId(profile);
|
||||
|
||||
if ("email" in profile && profile.email) {
|
||||
details = { email: profile.email, id: profileId };
|
||||
} else if ("phone" in profile && profile.phone) {
|
||||
details = { phone: profile.phone, id: profileId };
|
||||
} else if ("walletAddress" in profile && profile.walletAddress) {
|
||||
details = { address: profile.walletAddress, id: profileId };
|
||||
} else {
|
||||
details = { id: profileId };
|
||||
}
|
||||
|
||||
return {
|
||||
type: profile.type,
|
||||
details,
|
||||
};
|
||||
}),
|
||||
wallets: apiWallet.address
|
||||
? [
|
||||
{
|
||||
address: apiWallet.address,
|
||||
createdAt: apiWallet.createdAt || new Date().toISOString(),
|
||||
type: "enclave" as const,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to safely get ID from any profile type
|
||||
function getProfileId(profile: APIProfile | undefined): string {
|
||||
if (!profile) return "";
|
||||
|
||||
if ("id" in profile) {
|
||||
return profile.id;
|
||||
} else if ("credentialId" in profile) {
|
||||
return profile.credentialId;
|
||||
} else if ("identifier" in profile) {
|
||||
return profile.identifier;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function useEmbeddedWallets(params: {
|
||||
clientId?: string;
|
||||
ecosystemSlug?: string;
|
||||
@@ -98,6 +181,7 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
|
||||
while (true) {
|
||||
const res = await queryClient.fetchQuery<{
|
||||
users: WalletUser[];
|
||||
hasMore: boolean;
|
||||
}>({
|
||||
queryFn: fetchAccountList({
|
||||
clientId,
|
||||
@@ -113,12 +197,13 @@ export function useAllEmbeddedWallets(params: { authToken: string }) {
|
||||
),
|
||||
});
|
||||
|
||||
if (res.users.length === 0) {
|
||||
responses.push(...res.users);
|
||||
|
||||
if (!res.hasMore) {
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
responses.push(...res.users);
|
||||
}
|
||||
|
||||
return responses;
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { EngineBackendWalletType } from "@/constants/engine";
|
||||
import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../query-keys/cache-keys";
|
||||
|
||||
// Engine instances
|
||||
@@ -1662,7 +1662,7 @@ export function useEngineSystemMetrics(
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "GET",
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/${projectSlug}/engine/dedicated/${engineId}/metrics`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/${projectSlug}/engine/${engineId}/metrics`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
type SendTransactionConfig,
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
|
||||
export function useSendAndConfirmTx(config?: SendTransactionConfig) {
|
||||
const { theme } = useTheme();
|
||||
const sendAndConfirmTransaction = useSendAndConfirmTransaction({
|
||||
payModal: {
|
||||
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
return sendAndConfirmTransaction;
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
} from "thirdweb";
|
||||
@@ -20,6 +20,7 @@ import { getWalletBalance } from "thirdweb/wallets";
|
||||
import invariant from "tiny-invariant";
|
||||
import { parseError } from "../utils/errorParser";
|
||||
import { tryCatch } from "../utils/try-catch";
|
||||
import { useSendAndConfirmTx } from "./useSendTx";
|
||||
|
||||
function getTokenBalancesQuery(params: {
|
||||
ownerAddress: string;
|
||||
@@ -78,6 +79,7 @@ export function useOwnedTokenBalances(params: {
|
||||
export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
const account = useActiveAccount();
|
||||
const queryClient = useQueryClient();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
const params = {
|
||||
ownerAddress: contract.address, // because we want to fetch the balance of split contract
|
||||
@@ -98,16 +100,15 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
.filter((token) => token.value !== 0n)
|
||||
.map(async (currency) => {
|
||||
const transaction =
|
||||
currency.name === "Native Token"
|
||||
getAddress(currency.tokenAddress) ===
|
||||
getAddress(NATIVE_TOKEN_ADDRESS)
|
||||
? distribute({ contract })
|
||||
: distributeByToken({
|
||||
contract,
|
||||
tokenAddress: currency.tokenAddress,
|
||||
});
|
||||
const promise = sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction,
|
||||
});
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction);
|
||||
|
||||
toast.promise(promise, {
|
||||
error: (err) => ({
|
||||
message: `Error distributing ${currency.name}`,
|
||||
@@ -116,6 +117,8 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
loading: `Distributing ${currency.name}`,
|
||||
success: `Successfully distributed ${currency.name}`,
|
||||
});
|
||||
|
||||
await promise;
|
||||
});
|
||||
|
||||
return await Promise.all(distributions);
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
} from "thirdweb";
|
||||
import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as VoteExt from "thirdweb/extensions/vote";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import invariant from "tiny-invariant";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
|
||||
export async function tokensDelegated(
|
||||
options: BaseTransactionOptions<{ account: Account | undefined }>,
|
||||
@@ -81,7 +81,7 @@ export async function votingTokenDecimals(options: BaseTransactionOptions) {
|
||||
}
|
||||
|
||||
export function useDelegateMutation() {
|
||||
const { mutateAsync } = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (contract: ThirdwebContract) => {
|
||||
@@ -97,7 +97,7 @@ export function useDelegateMutation() {
|
||||
contract: tokenContract,
|
||||
delegatee: contract.address,
|
||||
});
|
||||
return await mutateAsync(transaction);
|
||||
return await sendAndConfirmTx.mutateAsync(transaction);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
rpc: {
|
||||
enabled: true,
|
||||
rateLimit: 1000,
|
||||
websockets: {
|
||||
enabled: false,
|
||||
reasonCode: "enterprise_plan_required",
|
||||
maxConnections: 0,
|
||||
maxSubscriptions: 0,
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
download: {
|
||||
|
||||
@@ -60,13 +60,6 @@ export interface UniversalBridgeWalletStats {
|
||||
developerFeeUsdCents: number;
|
||||
}
|
||||
|
||||
export interface WebhookRequestStats {
|
||||
date: string;
|
||||
webhookId: string;
|
||||
httpStatusCode: number;
|
||||
totalRequests: number;
|
||||
}
|
||||
|
||||
export interface WebhookLatencyStats {
|
||||
date: string;
|
||||
webhookId: string;
|
||||
@@ -85,6 +78,14 @@ export interface WebhookSummaryStats {
|
||||
errorBreakdown: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface AIUsageStats {
|
||||
date: string;
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalSessions: number;
|
||||
totalRequests: number;
|
||||
}
|
||||
|
||||
export interface AnalyticsQueryParams {
|
||||
teamId: string;
|
||||
projectId?: string;
|
||||
|
||||
+6
-6
@@ -17,7 +17,6 @@ import type { Chain, ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletChain,
|
||||
useSendTransaction,
|
||||
useSwitchActiveWalletChain,
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
@@ -43,11 +42,12 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
} from "@/constants/public-envs";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
|
||||
@@ -310,7 +310,7 @@ function SendFundsToFaucetModalContent(props: {
|
||||
const account = useActiveAccount();
|
||||
const activeChain = useActiveWalletChain();
|
||||
const switchActiveWalletChain = useSwitchActiveWalletChain();
|
||||
const sendTxMutation = useSendTransaction({
|
||||
const sendAndConfirmTx = useSendAndConfirmTx({
|
||||
payModal: false,
|
||||
});
|
||||
const switchChainMutation = useMutation({
|
||||
@@ -334,7 +334,7 @@ function SendFundsToFaucetModalContent(props: {
|
||||
value: toWei(values.amount.toString()),
|
||||
});
|
||||
|
||||
const promise = sendTxMutation.mutateAsync(sendNativeTokenTx);
|
||||
const promise = sendAndConfirmTx.mutateAsync(sendNativeTokenTx);
|
||||
|
||||
toast.promise(promise, {
|
||||
error: `Failed to send ${values.amount} ${props.chainMeta.nativeCurrency.symbol} to faucet`,
|
||||
@@ -404,11 +404,11 @@ function SendFundsToFaucetModalContent(props: {
|
||||
{activeChain.id === props.chain.id ? (
|
||||
<Button
|
||||
className="mt-4 w-full gap-2"
|
||||
disabled={sendTxMutation.isPending}
|
||||
disabled={sendAndConfirmTx.isPending}
|
||||
key="submit"
|
||||
type="submit"
|
||||
>
|
||||
{sendTxMutation.isPending && <Spinner className="size-4" />}
|
||||
{sendAndConfirmTx.isPending && <Spinner className="size-4" />}
|
||||
Send funds to faucet
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useCustomConnectModal } from "@/components/connect-wallet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
|
||||
type AddChainToWalletProps = {
|
||||
|
||||
+4
-4
@@ -2,8 +2,8 @@
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { cancelAuction, cancelListing } from "thirdweb/extensions/marketplace";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
|
||||
interface CancelTabProps {
|
||||
id: string;
|
||||
@@ -21,15 +21,15 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
const transaction = isAuction
|
||||
? cancelAuction({ auctionId: BigInt(id), contract })
|
||||
: cancelListing({ contract, listingId: BigInt(id) });
|
||||
const cancelQuery = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
return (
|
||||
<TransactionButton
|
||||
className="self-end"
|
||||
client={contract.client}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPending={cancelQuery.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
onClick={() => {
|
||||
const promise = cancelQuery.mutateAsync(transaction, {
|
||||
const promise = sendAndConfirmTx.mutateAsync(transaction, {
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
|
||||
+3
-2
@@ -28,7 +28,7 @@ import type {
|
||||
CreateListingParams,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { createAuction, createListing } from "thirdweb/extensions/marketplace";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { CurrencySelector } from "@/components/blocks/CurrencySelector";
|
||||
import { NFTMediaWithEmptyState } from "@/components/blocks/nft-media";
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardOwnedNFTs } from "@/hooks/useDashboardOwnedNFTs";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import { useOwnedNFTsInsight } from "@/hooks/useWalletNFTs";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -122,7 +123,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
chain: contract.chain,
|
||||
});
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const listingNotifications = useTxNotifications(
|
||||
"NFT listed Successfully",
|
||||
"Failed to list NFT",
|
||||
|
||||
+43
-3
@@ -1,4 +1,6 @@
|
||||
import { UploadIcon } from "lucide-react";
|
||||
import { ArrowDownToLineIcon, UploadIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { handleDownload } from "@/components/blocks/download-file-button";
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -9,6 +11,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SnapshotEntry } from "../../legacy-zod-schema";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
|
||||
/**
|
||||
@@ -118,10 +121,10 @@ export const ClaimerSelection = () => {
|
||||
|
||||
{/* Edit or See Snapshot */}
|
||||
{snapshot ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
|
||||
<Button
|
||||
className="gap-2 rounded-md"
|
||||
className="gap-2"
|
||||
disabled={disabledSnapshotButton}
|
||||
onClick={() => setOpenIndex(phaseIndex)}
|
||||
size="sm"
|
||||
@@ -130,6 +133,21 @@ export const ClaimerSelection = () => {
|
||||
<UploadIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
{snapshot && snapshot.length > 0 && (
|
||||
<Button
|
||||
className="gap-2 rounded bg-background"
|
||||
variant="outline"
|
||||
disabled={disabledSnapshotButton}
|
||||
onClick={() => {
|
||||
downloadSnapshotAsCSV(snapshot);
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
<ArrowDownToLineIcon className="size-4" />
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center",
|
||||
@@ -151,3 +169,25 @@ export const ClaimerSelection = () => {
|
||||
</FormFieldSetup>
|
||||
);
|
||||
};
|
||||
|
||||
function downloadSnapshotAsCSV(snapshot: SnapshotEntry[]) {
|
||||
const csvData = snapshot.map((entry) => ({
|
||||
address: entry.address,
|
||||
maxClaimable:
|
||||
entry.maxClaimable === "0" ? "" : entry.maxClaimable?.toString() || "",
|
||||
price: entry.price?.toString() || "",
|
||||
currencyAddress: entry.currencyAddress || "",
|
||||
}));
|
||||
|
||||
const csvContent = Papa.unparse(csvData, {
|
||||
header: true,
|
||||
delimiter: ",",
|
||||
quotes: false,
|
||||
});
|
||||
|
||||
handleDownload({
|
||||
fileContent: csvContent,
|
||||
fileNameWithExtension: "snapshot.csv",
|
||||
fileFormat: "text/csv",
|
||||
});
|
||||
}
|
||||
|
||||
+11
-19
@@ -1,11 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowDownToLineIcon,
|
||||
CircleAlertIcon,
|
||||
CircleHelpIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { CircleAlertIcon, CircleHelpIcon, PlusIcon } from "lucide-react";
|
||||
import {
|
||||
createContext,
|
||||
Fragment,
|
||||
@@ -28,11 +23,7 @@ import {
|
||||
ZERO_ADDRESS,
|
||||
} from "thirdweb";
|
||||
import { decimals } from "thirdweb/extensions/erc20";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useReadContract,
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import * as z from "zod";
|
||||
import { ZodError } from "zod";
|
||||
@@ -46,9 +37,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useIsAdmin } from "@/hooks/useContractRoles";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import {
|
||||
type ClaimConditionInput,
|
||||
@@ -222,7 +214,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
Record<number, SnapshotEntry[] | undefined>
|
||||
>({});
|
||||
|
||||
const sendTx = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
const tokenDecimals = useReadContract(decimals, {
|
||||
contract,
|
||||
@@ -305,7 +297,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
|
||||
const isFetchingData =
|
||||
claimConditionsQuery.isFetching ||
|
||||
sendTx.isPending ||
|
||||
sendAndConfirmTx.isPending ||
|
||||
// Need to make sure the tokenDecimals.data is present when interacting with ERC20 claim conditions
|
||||
(isErc20 && tokenDecimals.isLoading);
|
||||
|
||||
@@ -428,7 +420,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
},
|
||||
phasesWithSnapshots,
|
||||
);
|
||||
await sendTx.mutateAsync(tx);
|
||||
await sendAndConfirmTx.mutateAsync(tx);
|
||||
|
||||
saveClaimPhaseNotification.onSuccess();
|
||||
} catch (error) {
|
||||
@@ -559,7 +551,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
>
|
||||
<ClaimConditionsPhase
|
||||
contract={contract}
|
||||
isPending={sendTx.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
onRemove={() => {
|
||||
formFields.remove(index);
|
||||
// Clean up snapshot when phase is removed
|
||||
@@ -594,7 +586,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
disabled={
|
||||
sendTx.isPending || (!isMultiPhase && phases?.length > 0)
|
||||
sendAndConfirmTx.isPending ||
|
||||
(!isMultiPhase && phases?.length > 0)
|
||||
}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -649,12 +642,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
client={contract.client}
|
||||
disabled={claimConditionsQuery.isPending}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPending={sendTx.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
transactionCount={undefined}
|
||||
txChainID={contract.chain.id}
|
||||
type="submit"
|
||||
>
|
||||
<ArrowDownToLineIcon className="size-3.5" />
|
||||
{claimConditionsQuery.isPending
|
||||
? "Saving Phases"
|
||||
: "Save Phases"}
|
||||
|
||||
+5
-5
@@ -5,9 +5,9 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
|
||||
export function ResetClaimEligibility({
|
||||
@@ -23,7 +23,7 @@ export function ResetClaimEligibility({
|
||||
isLoggedIn: boolean;
|
||||
isMultiphase: boolean;
|
||||
}) {
|
||||
const sendTxMutation = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
const txNotification = useTxNotifications(
|
||||
"Successfully reset claim eligibility",
|
||||
@@ -52,7 +52,7 @@ export function ResetClaimEligibility({
|
||||
}
|
||||
})();
|
||||
|
||||
sendTxMutation.mutate(tx, {
|
||||
sendAndConfirmTx.mutate(tx, {
|
||||
onError: (error) => {
|
||||
txNotification.onError(error);
|
||||
},
|
||||
@@ -70,7 +70,7 @@ export function ResetClaimEligibility({
|
||||
<TransactionButton
|
||||
client={contract.client}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isPending={sendTxMutation.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
onClick={handleResetClaimEligibility}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -79,7 +79,7 @@ export function ResetClaimEligibility({
|
||||
type="button"
|
||||
className="text-destructive-text bg-card"
|
||||
>
|
||||
{sendTxMutation.isPending ? (
|
||||
{sendAndConfirmTx.isPending ? (
|
||||
"Resetting Eligibility"
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { DropZone } from "@/components/blocks/drop-zone/drop-zone";
|
||||
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
|
||||
export function LoadingPage() {
|
||||
return (
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export function ContractPageLayout(props: {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col grow">
|
||||
<div className="border-border border-b py-8">
|
||||
<div className="container flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import type { EVMContractInfo } from "@/hooks/useActiveChainId";
|
||||
|
||||
+4
-4
@@ -5,9 +5,9 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { prepareTransaction, type ThirdwebClient, toWei } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
|
||||
export function DepositNative(props: {
|
||||
@@ -17,7 +17,7 @@ export function DepositNative(props: {
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const sendTransaction = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const [amount, setAmount] = useState("");
|
||||
|
||||
return (
|
||||
@@ -36,7 +36,7 @@ export function DepositNative(props: {
|
||||
disabled={amount.length === 0 || Number.parseFloat(amount) <= 0}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
className="border-l-0 rounded-l-none px-6"
|
||||
isPending={sendTransaction.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
onClick={() => {
|
||||
const transaction = prepareTransaction({
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
@@ -45,7 +45,7 @@ export function DepositNative(props: {
|
||||
to: props.address,
|
||||
value: toWei(amount),
|
||||
});
|
||||
sendTransaction.mutate(transaction, {
|
||||
sendAndConfirmTx.mutate(transaction, {
|
||||
onSuccess: () => {
|
||||
toast.success("Deposit successful");
|
||||
setAmount("");
|
||||
|
||||
+5
-8
@@ -7,20 +7,17 @@ import {
|
||||
getAccountsOfSigner,
|
||||
isAccountDeployed,
|
||||
} from "thirdweb/extensions/erc4337";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useReadContract,
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
|
||||
export function CreateAccountButton(props: {
|
||||
contract: ThirdwebContract;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const sendTxMutation = useSendAndConfirmTransaction();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
@@ -63,14 +60,14 @@ export function CreateAccountButton(props: {
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
variant="default"
|
||||
size="sm"
|
||||
isPending={sendTxMutation.isPending}
|
||||
isPending={sendAndConfirmTx.isPending}
|
||||
onClick={() => {
|
||||
const tx = createAccount({
|
||||
admin: address,
|
||||
contract: props.contract,
|
||||
data: "0x",
|
||||
});
|
||||
sendTxMutation.mutate(tx);
|
||||
sendAndConfirmTx.mutate(tx);
|
||||
}}
|
||||
transactionCount={undefined}
|
||||
txChainID={props.contract.chain.id}
|
||||
|
||||
+5
-9
@@ -18,7 +18,6 @@ import {
|
||||
getRpcClient,
|
||||
prepareContractCall,
|
||||
prepareTransaction,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type {
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import type { ProjectMeta } from "../../../../../team/[team_slug]/[project_slug]/contract/[chainIdOrSlug]/[contractAddress]/types";
|
||||
import {
|
||||
@@ -178,6 +178,8 @@ export function DataTable({
|
||||
return Array.from(chainMap.values());
|
||||
}, [data, customChainData]);
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
|
||||
const form = useForm<FormSchema>({
|
||||
defaultValues: {
|
||||
amounts: {
|
||||
@@ -216,10 +218,7 @@ export function DataTable({
|
||||
],
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: activeAccount,
|
||||
transaction: sendErc20Tx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(sendErc20Tx);
|
||||
};
|
||||
|
||||
const crossChainTransferNotifications = useTxNotifications(
|
||||
@@ -368,10 +367,7 @@ export function DataTable({
|
||||
to: "0x4e59b44847b379578588920cA78FbF26c0B4956C",
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: activeAccount,
|
||||
transaction: tx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(tx);
|
||||
|
||||
const code = await eth_getCode(
|
||||
getRpcClient({
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import { getInstalledModules, owner } from "thirdweb/modules";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { InstalledModulesTable } from "./components/InstalledModulesTable";
|
||||
import { InstallModuleForm } from "./components/ModuleForm";
|
||||
|
||||
|
||||
+10
-7
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { sendAndConfirmTransaction, type ThirdwebClient } from "thirdweb";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { BatchMetadataERC721, BatchMetadataERC1155 } from "thirdweb/modules";
|
||||
import { z } from "zod";
|
||||
import { TransactionButton } from "@/components/tx-button";
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import { parseAttributes } from "@/utils/parseAttributes";
|
||||
import { fileBufferOrStringSchema } from "../zod-schemas";
|
||||
@@ -63,7 +64,7 @@ export type UploadMetadataFormValues = z.infer<typeof uploadMetadataFormSchema>;
|
||||
|
||||
function BatchMetadataModule(props: ModuleInstanceProps) {
|
||||
const { contract, ownerAccount } = props;
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const uploadMetadata = useCallback(
|
||||
async (values: UploadMetadataFormValues) => {
|
||||
if (!ownerAccount) {
|
||||
@@ -80,12 +81,14 @@ function BatchMetadataModule(props: ModuleInstanceProps) {
|
||||
metadatas: [nft],
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: uploadMetadataTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(uploadMetadataTx);
|
||||
},
|
||||
[contract, ownerAccount, props.contractInfo.name],
|
||||
[
|
||||
contract,
|
||||
ownerAccount,
|
||||
props.contractInfo.name,
|
||||
sendAndConfirmTx.mutateAsync,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+18
-17
@@ -15,7 +15,6 @@ import {
|
||||
getContract,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type PreparedTransaction,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
toTokens,
|
||||
ZERO_ADDRESS,
|
||||
@@ -53,6 +52,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import { addressSchema } from "../zod-schemas";
|
||||
import { CurrencySelector } from "./CurrencySelector";
|
||||
@@ -76,7 +76,7 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
const { contract, ownerAccount } = props;
|
||||
const account = useActiveAccount();
|
||||
const [tokenId, setTokenId] = useState<string>("");
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const isValidTokenId = positiveIntegerRegex.test(tokenId);
|
||||
|
||||
const primarySaleRecipientQuery = useReadContract(
|
||||
@@ -189,12 +189,9 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
throw new Error("Invalid tokenId");
|
||||
}
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: account,
|
||||
transaction: mintTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(mintTx);
|
||||
},
|
||||
[contract, account, props.contractInfo.name],
|
||||
[contract, account, props.contractInfo.name, sendAndConfirmTx.mutateAsync],
|
||||
);
|
||||
|
||||
const setClaimCondition = useCallback(
|
||||
@@ -236,12 +233,14 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
throw new Error("Invalid tokenId");
|
||||
}
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: setClaimConditionTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(setClaimConditionTx);
|
||||
},
|
||||
[contract, ownerAccount, props.contractInfo.name],
|
||||
[
|
||||
contract,
|
||||
ownerAccount,
|
||||
props.contractInfo.name,
|
||||
sendAndConfirmTx.mutateAsync,
|
||||
],
|
||||
);
|
||||
|
||||
const setPrimarySaleRecipient = useCallback(
|
||||
@@ -260,12 +259,14 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
primarySaleRecipient: values.primarySaleRecipient,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: setSaleConfigTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(setSaleConfigTx);
|
||||
},
|
||||
[contract, ownerAccount, props.contractInfo.name],
|
||||
[
|
||||
contract,
|
||||
ownerAccount,
|
||||
props.contractInfo.name,
|
||||
sendAndConfirmTx.mutateAsync,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { CircleSlashIcon } from "lucide-react";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { ModuleCard } from "./module-card";
|
||||
import { useAllModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
|
||||
+19
-20
@@ -4,11 +4,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
type PreparedTransaction,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type { PreparedTransaction, ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
grantRoles,
|
||||
hasAllRoles,
|
||||
@@ -40,6 +36,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useSendAndConfirmTx } from "@/hooks/useSendTx";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
import type { NFTMetadataInputLimited } from "@/types/modified-types";
|
||||
import { parseAttributes } from "@/utils/parseAttributes";
|
||||
@@ -73,7 +70,7 @@ const MINTER_ROLE = 1n;
|
||||
|
||||
function MintableModule(props: ModuleInstanceProps) {
|
||||
const { contract, ownerAccount } = props;
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const isErc721 = props.contractInfo.name === "MintableERC721";
|
||||
|
||||
const primarySaleRecipientQuery = useReadContract(
|
||||
@@ -106,10 +103,7 @@ function MintableModule(props: ModuleInstanceProps) {
|
||||
user: ownerAccount.address,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: grantRoleTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(grantRoleTx);
|
||||
}
|
||||
|
||||
let mintTx: PreparedTransaction;
|
||||
@@ -137,12 +131,15 @@ function MintableModule(props: ModuleInstanceProps) {
|
||||
throw new Error("Invalid token ID");
|
||||
}
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: mintTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(mintTx);
|
||||
},
|
||||
[contract, ownerAccount, hasMinterRole.data, props.contractInfo.name],
|
||||
[
|
||||
contract,
|
||||
ownerAccount,
|
||||
hasMinterRole.data,
|
||||
props.contractInfo.name,
|
||||
sendAndConfirmTx.mutateAsync,
|
||||
],
|
||||
);
|
||||
|
||||
const update = useCallback(
|
||||
@@ -163,12 +160,14 @@ function MintableModule(props: ModuleInstanceProps) {
|
||||
primarySaleRecipient: values.primarySaleRecipient,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
account: ownerAccount,
|
||||
transaction: setSaleConfigTx,
|
||||
});
|
||||
await sendAndConfirmTx.mutateAsync(setSaleConfigTx);
|
||||
},
|
||||
[contract, ownerAccount, props.contractInfo.name],
|
||||
[
|
||||
contract,
|
||||
ownerAccount,
|
||||
props.contractInfo.name,
|
||||
sendAndConfirmTx.mutateAsync,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user