Compare commits
177
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d5595e7f | ||
|
|
d0bbbfd39d | ||
|
|
e26d81ce1d | ||
|
|
be3316ec1b | ||
|
|
872369770f | ||
|
|
3d48826928 | ||
|
|
fa640ad64b | ||
|
|
eb1a9c3c3e | ||
|
|
a54c1aba35 | ||
|
|
bea4f01742 | ||
|
|
e19f7a21ad | ||
|
|
c37af4f191 | ||
|
|
f964b8aaff | ||
|
|
424e6b8f9b | ||
|
|
763d3de5a8 | ||
|
|
f6f138ccbc | ||
|
|
f5d5e73e57 | ||
|
|
673e51b11c | ||
|
|
282edf7ccb | ||
|
|
a87e91b99c | ||
|
|
0e630c4550 | ||
|
|
11c7bf0955 | ||
|
|
c9e430d092 | ||
|
|
1f440d0c97 | ||
|
|
3a0aac4393 | ||
|
|
7b13015053 | ||
|
|
7f081618e1 | ||
|
|
881c7a96f7 | ||
|
|
ad68969559 | ||
|
|
915d955da1 | ||
|
|
7da16cff2f | ||
|
|
f0edc0f253 | ||
|
|
0a2d269782 | ||
|
|
3d996d1d23 | ||
|
|
40781b8170 | ||
|
|
d907017217 | ||
|
|
cf53c2d7f1 | ||
|
|
207cb9dbeb | ||
|
|
b4d365bdc0 | ||
|
|
ea4e2fe376 | ||
|
|
1d54cff7b4 | ||
|
|
fd04eefea9 | ||
|
|
4cc68310dc | ||
|
|
ccdce496e0 | ||
|
|
2725f337f8 | ||
|
|
2c6ea3e6dd | ||
|
|
219fc69d72 | ||
|
|
8d17685683 | ||
|
|
8ef418c543 | ||
|
|
b88f75b494 | ||
|
|
c83f1c6f7a | ||
|
|
bc0295487b | ||
|
|
90b779107d | ||
|
|
afe187de95 | ||
|
|
759b93beff | ||
|
|
8e357b3cb3 | ||
|
|
6c318f83d6 | ||
|
|
f790c9fae9 | ||
|
|
bcbb3f540a | ||
|
|
931c730a08 | ||
|
|
e0c83121c2 | ||
|
|
a82a31e5ad | ||
|
|
3d235e218a | ||
|
|
c08fb1356e | ||
|
|
68cca6f992 | ||
|
|
e0abf2b273 | ||
|
|
abf0bbaf35 | ||
|
|
23cb3bd98c | ||
|
|
dcef3f4fe7 | ||
|
|
bf46520a6b | ||
|
|
7defce7264 | ||
|
|
ae3a122cf1 | ||
|
|
2412a6958c | ||
|
|
bfd3be764c | ||
|
|
46b9d2dd82 | ||
|
|
3e9eb49787 | ||
|
|
630edadcec | ||
|
|
f757e60846 | ||
|
|
d5d593956e | ||
|
|
9a4b1cb147 | ||
|
|
85fe9491cd | ||
|
|
899051f788 | ||
|
|
5828b9bd45 | ||
|
|
2b68a32eaa | ||
|
|
66ae33ee8b | ||
|
|
e9788096d6 | ||
|
|
706d696434 | ||
|
|
c36cfbb5c7 | ||
|
|
93a9ba9121 | ||
|
|
6a7b556d75 | ||
|
|
e0ace736a8 | ||
|
|
b30369f3cc | ||
|
|
f2121e9519 | ||
|
|
29d05a50bf | ||
|
|
f00f559a12 | ||
|
|
ceba6835dd | ||
|
|
b33c1a30ac | ||
|
|
72fbb1550f | ||
|
|
1e7df5ad3d | ||
|
|
ca16ec783b | ||
|
|
b1a780832e | ||
|
|
9bb65ad5b2 | ||
|
|
db291d2d38 | ||
|
|
9dd0b17f15 | ||
|
|
12082fd5fd | ||
|
|
5ff6063961 | ||
|
|
3db3982fab | ||
|
|
bf90104330 | ||
|
|
8003e93f19 | ||
|
|
51c8318df7 | ||
|
|
f043f1eef4 | ||
|
|
d68da2239b | ||
|
|
6a6b0fba96 | ||
|
|
c934102c51 | ||
|
|
8c58c131ac | ||
|
|
2779afc15f | ||
|
|
fe50abd5ab | ||
|
|
e830109274 | ||
|
|
e7f42e6ac5 | ||
|
|
91ff13b8aa | ||
|
|
47354d5a19 | ||
|
|
8280efb991 | ||
|
|
5f11ae0cd4 | ||
|
|
c9a0c23c6c | ||
|
|
43b0680124 | ||
|
|
cccbde9a2e | ||
|
|
85a6afc377 | ||
|
|
e74aca9c90 | ||
|
|
b0cac940bd | ||
|
|
1a19e14845 | ||
|
|
464af6a54c | ||
|
|
f3c55d7139 | ||
|
|
9545b43675 | ||
|
|
869bbcfa68 | ||
|
|
35c7d21e36 | ||
|
|
edf3aad8c4 | ||
|
|
ab4190e23a | ||
|
|
9086cdba20 | ||
|
|
0082e996ca | ||
|
|
28955e0ac5 | ||
|
|
05550d045f | ||
|
|
a0c45cb25d | ||
|
|
6fc4d838ff | ||
|
|
fe940b3c66 | ||
|
|
080d7f2122 | ||
|
|
ae75073a4a | ||
|
|
781a626461 | ||
|
|
f630912b9b | ||
|
|
163ab8afe1 | ||
|
|
62cfbb7441 | ||
|
|
d7104f69f4 | ||
|
|
23545cb999 | ||
|
|
f9283f389b | ||
|
|
9fbb29dc25 | ||
|
|
71a1e58eaf | ||
|
|
68b48f4faa | ||
|
|
1c660d4712 | ||
|
|
30b5c30ba0 | ||
|
|
86461f9a07 | ||
|
|
2ebba6139a | ||
|
|
f3bacc16cb | ||
|
|
968913d55b | ||
|
|
bd70db4a5d | ||
|
|
d9a138a90a | ||
|
|
777aed1e5f | ||
|
|
fbed4be959 | ||
|
|
5a26217a43 | ||
|
|
938c1e2f60 | ||
|
|
af9a73d930 | ||
|
|
34c5d24043 | ||
|
|
e612f16a85 | ||
|
|
4a30b9c89a | ||
|
|
ef9cf3409b | ||
|
|
fd20a6cabb | ||
|
|
9095041ea7 | ||
|
|
2dbb9e2de4 | ||
|
|
00b8adf3a2 |
@@ -188,9 +188,16 @@ jobs:
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Report bundle size
|
||||
- name: Report bundle size (thirdweb)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/thirdweb
|
||||
directory: packages/thirdweb
|
||||
|
||||
- name: Report bundle size (nexus)
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
directory: packages/nexus
|
||||
@@ -2,7 +2,7 @@ name: Auto Author Assign
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review, draft]
|
||||
types: [opened, reopened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -18,7 +18,7 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
|
||||
2. Formatting & Linting
|
||||
|
||||
- Biome governs formatting and linting; its rules live in biome.json.
|
||||
- Run pnpm biome check --apply before committing.
|
||||
- Run `pnpm fix` & `pnpm lint` before committing, make sure there are no linting errors.
|
||||
- Avoid editor‑specific configs; rely on the shared settings.
|
||||
- make sure everything builds after each file change by running `pnpm build`
|
||||
|
||||
@@ -136,7 +136,7 @@ export function reportContractDeployed(properties: {
|
||||
}
|
||||
```
|
||||
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Housekeeping**: Inform **#eng-core-services** before renaming or removing an existing event.
|
||||
|
||||
⸻
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { defineConfig } from "checkly";
|
||||
import { Frequency } from "checkly/constructs";
|
||||
|
||||
export default defineConfig({
|
||||
checks: {
|
||||
activated: true,
|
||||
browserChecks: {
|
||||
frequency: Frequency.EVERY_24H,
|
||||
testMatch: "./tests/**/*.spec.ts",
|
||||
},
|
||||
checkMatch: "./**/*.check.ts",
|
||||
frequency: Frequency.EVERY_24H,
|
||||
ignoreDirectoriesMatch: [],
|
||||
locations: ["us-east-1", "eu-west-1"],
|
||||
muted: false,
|
||||
playwrightConfig: {
|
||||
use: {
|
||||
baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com",
|
||||
},
|
||||
},
|
||||
runtimeId: "2023.09",
|
||||
tags: ["website"],
|
||||
},
|
||||
cli: {
|
||||
runLocation: "eu-west-1",
|
||||
},
|
||||
logicalId: "thirdweb-www",
|
||||
projectName: "thirdweb.com",
|
||||
repoUrl: "https://github.com/thirdweb-dev/dashboard",
|
||||
});
|
||||
@@ -10,7 +10,8 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/x402",
|
||||
"/nexus",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
"/transactions",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@number-flow/react": "^0.5.10",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
@@ -47,7 +48,6 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"ogl": "^1.0.11",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
@@ -83,7 +83,6 @@
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
"@playwright/test": "1.53.2",
|
||||
"@storybook/addon-docs": "9.0.15",
|
||||
"@storybook/addon-links": "9.0.15",
|
||||
"@storybook/addon-onboarding": "9.0.15",
|
||||
@@ -100,7 +99,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"checkly": "6.0.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||
@@ -123,13 +121,11 @@
|
||||
"format": "biome format ./src --write",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"playwright": "playwright test",
|
||||
"postbuild": "next-sitemap",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"update-checkly": "npx checkly deploy"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
testDir: "./tests",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: process.env.CI
|
||||
? undefined
|
||||
: {
|
||||
command: "pnpm run start",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
url: "http://127.0.0.1:3000",
|
||||
},
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -479,6 +479,11 @@ async function redirects() {
|
||||
destination: "/tokens",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/payments/x402",
|
||||
destination: "/x402",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"use server";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import type { ProjectWalletSummary } from "@/lib/server/project-wallet";
|
||||
|
||||
interface VaultWalletListItem {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function listProjectServerWallets(params: {
|
||||
managementAccessToken: string;
|
||||
projectId: string;
|
||||
pageSize?: number;
|
||||
}): Promise<ProjectWalletSummary[]> {
|
||||
const { managementAccessToken, projectId, pageSize = 100 } = params;
|
||||
|
||||
if (!managementAccessToken || !NEXT_PUBLIC_THIRDWEB_VAULT_URL) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - Vault SDK expects snake_case pagination fields
|
||||
page_size: pageSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data?.items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[];
|
||||
|
||||
return items
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.metadata?.projectId === projectId &&
|
||||
(!item.metadata?.type || item.metadata.type === "server-wallet")
|
||||
);
|
||||
})
|
||||
.map<ProjectWalletSummary>((item) => ({
|
||||
id: item.id,
|
||||
address: item.address,
|
||||
label: item.metadata?.label ?? undefined,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to list project server wallets", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use server";
|
||||
|
||||
import { configure, sendTokens } from "@thirdweb-dev/api";
|
||||
import { THIRDWEB_API_HOST } from "@/constants/urls";
|
||||
|
||||
configure({
|
||||
override: {
|
||||
baseUrl: THIRDWEB_API_HOST,
|
||||
},
|
||||
});
|
||||
|
||||
export async function sendProjectWalletTokens(options: {
|
||||
walletAddress: string;
|
||||
recipientAddress: string;
|
||||
chainId: number;
|
||||
quantityWei: string;
|
||||
publishableKey: string;
|
||||
teamId: string;
|
||||
tokenAddress?: string;
|
||||
secretKey: string;
|
||||
vaultAccessToken?: string;
|
||||
}) {
|
||||
const {
|
||||
walletAddress,
|
||||
recipientAddress,
|
||||
chainId,
|
||||
quantityWei,
|
||||
publishableKey,
|
||||
teamId,
|
||||
tokenAddress,
|
||||
secretKey,
|
||||
vaultAccessToken,
|
||||
} = options;
|
||||
|
||||
if (!secretKey) {
|
||||
return {
|
||||
error: "A project secret key is required to send funds.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
const response = await sendTokens({
|
||||
body: {
|
||||
chainId,
|
||||
from: walletAddress,
|
||||
recipients: [
|
||||
{
|
||||
address: recipientAddress,
|
||||
quantity: quantityWei,
|
||||
},
|
||||
],
|
||||
...(tokenAddress ? { tokenAddress } : {}),
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": publishableKey,
|
||||
"x-secret-key": secretKey,
|
||||
"x-team-id": teamId,
|
||||
...(vaultAccessToken ? { "x-vault-access-token": vaultAccessToken } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error || !response.data) {
|
||||
return {
|
||||
error: response.error || "Failed to submit transfer request.",
|
||||
ok: false,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
transactionIds: response.data.result?.transactionIds ?? [],
|
||||
} as const;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import Stripe from "stripe";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import {
|
||||
@@ -88,6 +88,9 @@ export async function fetchClientSecret(team: Team) {
|
||||
throw new Error("No customer ID found");
|
||||
}
|
||||
|
||||
// try to get the gclid cookie
|
||||
const gclid = (await cookies()).get("gclid")?.value;
|
||||
|
||||
// Create Checkout Sessions from body params.
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
ui_mode: "embedded",
|
||||
@@ -117,6 +120,8 @@ export async function fetchClientSecret(team: Team) {
|
||||
missing_payment_method: "cancel",
|
||||
},
|
||||
},
|
||||
// if gclid exists, set it as a metadata field so we can attribute the conversion later
|
||||
metadata: gclid ? { gclid } : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export function reportContractDeployed(properties: {
|
||||
publisher: string | undefined;
|
||||
contractName: string | undefined;
|
||||
deploymentType?: "asset";
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("contract deployed", properties);
|
||||
}
|
||||
@@ -40,6 +41,7 @@ export function reportContractDeployed(properties: {
|
||||
export function reportContractDeployFailed(properties: {
|
||||
errorMessage: string;
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
publisher: string | undefined;
|
||||
contractName: string | undefined;
|
||||
}) {
|
||||
@@ -171,9 +173,7 @@ export function reportOnboardingCompleted() {
|
||||
*
|
||||
*/
|
||||
export function reportFaucetUsed(properties: { chainId: number }) {
|
||||
posthog.capture("faucet used", {
|
||||
chainId: properties.chainId,
|
||||
});
|
||||
posthog.capture("faucet used", properties);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
@@ -197,12 +197,7 @@ export function reportChainConfigurationAdded(properties: {
|
||||
decimals: number;
|
||||
};
|
||||
}) {
|
||||
posthog.capture("chain configuration added", {
|
||||
chainId: properties.chainId,
|
||||
chainName: properties.chainName,
|
||||
nativeCurrency: properties.nativeCurrency,
|
||||
rpcURLs: properties.rpcURLs,
|
||||
});
|
||||
posthog.capture("chain configuration added", properties);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
@@ -227,12 +222,9 @@ export function reportAssetBuySuccessful(properties: {
|
||||
chainId: number;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset buy successful", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset buy successful", properties);
|
||||
}
|
||||
|
||||
type TokenSwapParams = {
|
||||
@@ -349,16 +341,12 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
|
||||
*/
|
||||
export function reportAssetBuyFailed(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
error: string;
|
||||
}) {
|
||||
posthog.capture("asset buy failed", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
error: properties.error,
|
||||
});
|
||||
posthog.capture("asset buy failed", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,14 +359,11 @@ export function reportAssetBuyFailed(properties: {
|
||||
*/
|
||||
export function reportAssetBuyCancelled(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
}) {
|
||||
posthog.capture("asset buy cancelled", {
|
||||
assetType: properties.assetType,
|
||||
chainId: properties.chainId,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset buy cancelled", properties);
|
||||
}
|
||||
|
||||
// Assets Landing Page ----------------------------
|
||||
@@ -423,10 +408,7 @@ export function reportAssetCreationStepConfigured(
|
||||
step: "coin-info" | "token-distribution" | "launch-coin";
|
||||
},
|
||||
) {
|
||||
posthog.capture("asset creation step configured", {
|
||||
assetType: properties.assetType,
|
||||
step: properties.step,
|
||||
});
|
||||
posthog.capture("asset creation step configured", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,11 +422,10 @@ export function reportAssetCreationStepConfigured(
|
||||
export function reportAssetCreationSuccessful(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
contractType: AssetContractType;
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset creation successful", {
|
||||
assetType: properties.assetType,
|
||||
contractType: properties.contractType,
|
||||
});
|
||||
posthog.capture("asset creation successful", properties);
|
||||
}
|
||||
|
||||
type CoinCreationStep =
|
||||
@@ -466,7 +447,12 @@ type CoinCreationStep =
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportAssetCreationFailed(
|
||||
properties: { contractType: AssetContractType; error: string } & (
|
||||
properties: {
|
||||
contractType: AssetContractType;
|
||||
error: string;
|
||||
is_testnet: boolean | undefined;
|
||||
chainId: number;
|
||||
} & (
|
||||
| {
|
||||
assetType: "nft";
|
||||
step:
|
||||
@@ -481,12 +467,7 @@ export function reportAssetCreationFailed(
|
||||
}
|
||||
),
|
||||
) {
|
||||
posthog.capture("asset creation failed", {
|
||||
assetType: properties.assetType,
|
||||
contractType: properties.contractType,
|
||||
error: properties.error,
|
||||
step: properties.step,
|
||||
});
|
||||
posthog.capture("asset creation failed", properties);
|
||||
}
|
||||
|
||||
type UpsellParams = {
|
||||
@@ -582,6 +563,7 @@ export function reportPaymentLinkBuyFailed(properties: {
|
||||
export function reportAssetPageview(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("asset pageview", properties);
|
||||
}
|
||||
@@ -593,7 +575,10 @@ export function reportAssetPageview(properties: {
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportChainPageview(properties: { chainId: number }) {
|
||||
export function reportChainPageview(properties: {
|
||||
chainId: number;
|
||||
is_testnet: boolean | undefined;
|
||||
}) {
|
||||
posthog.capture("chain pageview", properties);
|
||||
}
|
||||
|
||||
@@ -686,10 +671,7 @@ export function reportProductFeedback(properties: {
|
||||
feedback: string;
|
||||
source: "desktop" | "mobile";
|
||||
}) {
|
||||
posthog.capture("product feedback submitted", {
|
||||
feedback: properties.feedback,
|
||||
source: properties.source,
|
||||
});
|
||||
posthog.capture("product feedback submitted", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -701,7 +683,22 @@ export function reportProductFeedback(properties: {
|
||||
*
|
||||
*/
|
||||
export function reportBridgePageLinkClick(params: {
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
linkType: "bridge-docs" | "trending-tokens";
|
||||
}) {
|
||||
posthog.capture("bridge page link clicked", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track which tokens are users are clicking on in the token list page
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*
|
||||
*/
|
||||
export function reportTokenRowClicked(params: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
}) {
|
||||
posthog.capture("token row clicked", params);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
WalletStats,
|
||||
WebhookLatencyStats,
|
||||
WebhookSummaryStats,
|
||||
X402QueryParams,
|
||||
X402SettlementStats,
|
||||
} from "@/types/analytics";
|
||||
import { getChains } from "./chain";
|
||||
|
||||
@@ -184,7 +186,7 @@ const cached_getInAppWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch in-app wallet usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch user wallets usage, ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -448,6 +450,7 @@ type ActiveStatus = {
|
||||
pay: boolean;
|
||||
inAppWallet: boolean;
|
||||
ecosystemWallet: boolean;
|
||||
engineCloud: boolean;
|
||||
};
|
||||
|
||||
export const isProjectActive = unstable_cache(
|
||||
@@ -479,6 +482,7 @@ export const isProjectActive = unstable_cache(
|
||||
pay: false,
|
||||
rpc: false,
|
||||
sdk: false,
|
||||
engineCloud: false,
|
||||
storage: false,
|
||||
} as ActiveStatus;
|
||||
}
|
||||
@@ -488,7 +492,7 @@ export const isProjectActive = unstable_cache(
|
||||
},
|
||||
["isProjectActive"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
revalidate: 30, // 30 seconds
|
||||
},
|
||||
);
|
||||
|
||||
@@ -904,3 +908,43 @@ export function getInsightUsage(
|
||||
) {
|
||||
return cached_getInsightUsage(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
const cached_getX402Settlements = unstable_cache(
|
||||
async (
|
||||
params: X402QueryParams,
|
||||
authToken: string,
|
||||
): Promise<X402SettlementStats[]> => {
|
||||
const searchParams = buildSearchParams(params);
|
||||
|
||||
if (params.groupBy) {
|
||||
searchParams.append("groupBy", params.groupBy);
|
||||
}
|
||||
|
||||
const res = await fetchAnalytics({
|
||||
authToken,
|
||||
url: `v2/x402/settlements?${searchParams.toString()}`,
|
||||
init: {
|
||||
method: "GET",
|
||||
},
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch x402 settlements: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as X402SettlementStats[];
|
||||
},
|
||||
["getX402Settlements"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export function getX402Settlements(params: X402QueryParams, authToken: string) {
|
||||
return cached_getX402Settlements(normalizedParams(params), authToken);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ export type AuthOption =
|
||||
| "steam"
|
||||
| "apple"
|
||||
| "coinbase"
|
||||
| "line";
|
||||
| "line"
|
||||
| "epic";
|
||||
|
||||
export type Ecosystem = {
|
||||
name: string;
|
||||
|
||||
@@ -82,7 +82,7 @@ const CHAIN_PLAN_TO_INFRA = {
|
||||
],
|
||||
};
|
||||
|
||||
export async function getChainSubscriptions(slug: string) {
|
||||
async function getChainSubscriptions(slug: string) {
|
||||
const allSubscriptions = await getTeamSubscriptions(slug);
|
||||
if (!allSubscriptions) {
|
||||
return null;
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
"use server";
|
||||
import type { Address } from "thirdweb";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST } from "@/constants/public-envs";
|
||||
|
||||
const UB_BASE_URL = NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
|
||||
type Webhook = {
|
||||
export type Webhook = {
|
||||
url: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
secret: string;
|
||||
version?: number; // TODO (UB) make this mandatory after migration
|
||||
};
|
||||
|
||||
export async function getWebhooks(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
export async function getWebhooks(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -36,28 +36,55 @@ export async function getWebhooks(props: { clientId: string; teamId: string }) {
|
||||
return json.data as Array<Webhook>;
|
||||
}
|
||||
|
||||
export async function createWebhook(props: {
|
||||
export async function getWebhookById(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
webhookId: string;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Webhook;
|
||||
}
|
||||
|
||||
export async function createWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
secret?: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
body: JSON.stringify({
|
||||
label: props.label,
|
||||
secret: props.secret,
|
||||
url: props.url,
|
||||
version: props.version,
|
||||
label: params.label,
|
||||
secret: params.secret,
|
||||
url: params.url,
|
||||
version: params.version,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
@@ -67,23 +94,23 @@ export async function createWebhook(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return (await res.json()) as Webhook;
|
||||
}
|
||||
|
||||
export async function deleteWebhook(props: {
|
||||
export async function deleteWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
webhookId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
@@ -94,7 +121,151 @@ export async function deleteWebhook(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function updateWebhook(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
webhookId: string;
|
||||
authToken: string;
|
||||
body: {
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
}) {
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${encodeURIComponent(params.webhookId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params.body),
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return json.data as Webhook;
|
||||
}
|
||||
|
||||
export type WebhookSend = {
|
||||
id: string;
|
||||
webhookId: string;
|
||||
webhookUrl: string;
|
||||
createdAt: string;
|
||||
onrampId: string | null;
|
||||
paymentId: string | null;
|
||||
response: string | null;
|
||||
responseStatus: number;
|
||||
status: "PENDING" | "COMPLETED" | "FAILED";
|
||||
success: boolean;
|
||||
transactionId: string | null;
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
type WebhookSendsResponse = {
|
||||
data: WebhookSend[];
|
||||
pagination: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getWebhookSends(options: {
|
||||
authToken: string;
|
||||
projectClientId: string;
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
webhookId: string;
|
||||
success?: boolean;
|
||||
}): Promise<WebhookSendsResponse> {
|
||||
const { limit, offset, success, webhookId, authToken } = options;
|
||||
|
||||
const url = new URL(
|
||||
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhook-sends`,
|
||||
);
|
||||
|
||||
url.searchParams.set("webhookId", webhookId);
|
||||
|
||||
if (limit !== undefined) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (success !== undefined) {
|
||||
url.searchParams.set("success", success.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-client-id": options.projectClientId,
|
||||
"x-team-id": options.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
throw new Error(errorJson.message);
|
||||
}
|
||||
|
||||
return (await response.json()) as WebhookSendsResponse;
|
||||
}
|
||||
|
||||
export async function resendWebhook(
|
||||
params: {
|
||||
authToken: string;
|
||||
projectClientId: string;
|
||||
teamId: string;
|
||||
} & (
|
||||
| {
|
||||
paymentId: string;
|
||||
}
|
||||
| {
|
||||
onrampId: string;
|
||||
}
|
||||
),
|
||||
) {
|
||||
const url = new URL(
|
||||
`${NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST}/v1/developer/webhooks/retry`,
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(
|
||||
"paymentId" in params
|
||||
? { paymentId: params.paymentId }
|
||||
: { onrampId: params.onrampId },
|
||||
),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": params.projectClientId,
|
||||
"x-team-id": params.teamId,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
throw new Error(errorJson.message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
type PaymentLink = {
|
||||
@@ -116,17 +287,17 @@ type PaymentLink = {
|
||||
amount: bigint;
|
||||
};
|
||||
|
||||
export async function getPaymentLinks(props: {
|
||||
export async function getPaymentLinks(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}): Promise<Array<PaymentLink>> {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -159,7 +330,7 @@ export async function getPaymentLinks(props: {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createPaymentLink(props: {
|
||||
export async function createPaymentLink(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
title: string;
|
||||
@@ -171,26 +342,25 @@ export async function createPaymentLink(props: {
|
||||
amount: bigint;
|
||||
purchaseData?: unknown;
|
||||
};
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/links`, {
|
||||
body: JSON.stringify({
|
||||
title: props.title,
|
||||
imageUrl: props.imageUrl,
|
||||
title: params.title,
|
||||
imageUrl: params.imageUrl,
|
||||
intent: {
|
||||
destinationChainId: props.intent.destinationChainId,
|
||||
destinationTokenAddress: props.intent.destinationTokenAddress,
|
||||
receiver: props.intent.receiver,
|
||||
amount: props.intent.amount.toString(),
|
||||
purchaseData: props.intent.purchaseData,
|
||||
destinationChainId: params.intent.destinationChainId,
|
||||
destinationTokenAddress: params.intent.destinationTokenAddress,
|
||||
receiver: params.intent.receiver,
|
||||
amount: params.intent.amount.toString(),
|
||||
purchaseData: params.intent.purchaseData,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
@@ -207,20 +377,20 @@ export async function createPaymentLink(props: {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePaymentLink(props: {
|
||||
export async function deletePaymentLink(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
paymentLinkId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/links/${props.paymentLinkId}`,
|
||||
`${UB_BASE_URL}/v1/developer/links/${encodeURIComponent(params.paymentLinkId)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
@@ -231,7 +401,7 @@ export async function deletePaymentLink(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export type Fee = {
|
||||
@@ -241,14 +411,17 @@ export type Fee = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export async function getFees(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
export async function getFees(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
authToken: string;
|
||||
}) {
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
@@ -262,23 +435,23 @@ export async function getFees(props: { clientId: string; teamId: string }) {
|
||||
return json.data as Fee;
|
||||
}
|
||||
|
||||
export async function updateFee(props: {
|
||||
export async function updateFee(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
authToken: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
body: JSON.stringify({
|
||||
feeBps: props.feeBps,
|
||||
feeRecipient: props.feeRecipient,
|
||||
feeBps: params.feeBps,
|
||||
feeRecipient: params.feeRecipient,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
@@ -288,7 +461,7 @@ export async function updateFee(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
export type PaymentsResponse = {
|
||||
@@ -345,36 +518,34 @@ export type Payment = {
|
||||
|
||||
export type BridgePayment = Extract<Payment, { type: BridgePaymentType }>;
|
||||
|
||||
export async function getPayments(props: {
|
||||
export async function getPayments(params: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
paymentLinkId?: string;
|
||||
limit?: number;
|
||||
authToken: string;
|
||||
offset?: number;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
// Build URL with query parameters if provided
|
||||
const url = new URL(`${UB_BASE_URL}/v1/developer/payments`);
|
||||
|
||||
if (props.limit) {
|
||||
url.searchParams.append("limit", props.limit.toString());
|
||||
if (params.limit) {
|
||||
url.searchParams.append("limit", params.limit.toString());
|
||||
}
|
||||
|
||||
if (props.offset) {
|
||||
url.searchParams.append("offset", props.offset.toString());
|
||||
if (params.offset) {
|
||||
url.searchParams.append("offset", params.offset.toString());
|
||||
}
|
||||
|
||||
if (props.paymentLinkId) {
|
||||
url.searchParams.append("paymentLinkId", props.paymentLinkId);
|
||||
if (params.paymentLinkId) {
|
||||
url.searchParams.append("paymentLinkId", params.paymentLinkId);
|
||||
}
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": params.clientId,
|
||||
"x-team-id": params.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function getUniversalBridgeTokens(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function addUniversalBridgeTokenRoute(props: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.project.publishableKey,
|
||||
} as Record<string, string>,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
"use client";
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const vertex = `
|
||||
attribute vec2 position;
|
||||
void main(){gl_Position=vec4(position,0.0,1.0);}
|
||||
`;
|
||||
|
||||
const fragment = `
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uHueShift;
|
||||
uniform float uNoise;
|
||||
uniform float uScan;
|
||||
uniform float uScanFreq;
|
||||
uniform float uWarp;
|
||||
uniform vec3 uBackgroundColor;
|
||||
uniform float uBackgroundOpacity;
|
||||
uniform float uPatternLightness;
|
||||
#define iTime uTime
|
||||
#define iResolution uResolution
|
||||
|
||||
vec4 buf[8];
|
||||
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
|
||||
|
||||
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
|
||||
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
|
||||
|
||||
vec3 hueShiftRGB(vec3 col,float deg){
|
||||
vec3 yiq=rgb2yiq*col;
|
||||
float rad=radians(deg);
|
||||
float cosh=cos(rad),sinh=sin(rad);
|
||||
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
|
||||
return clamp(yiq2rgb*yiqShift,0.0,1.0);
|
||||
}
|
||||
|
||||
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
|
||||
|
||||
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
|
||||
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
|
||||
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
|
||||
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
|
||||
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
|
||||
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
|
||||
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
|
||||
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
|
||||
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
|
||||
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
|
||||
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
|
||||
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
|
||||
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
|
||||
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
|
||||
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
|
||||
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
|
||||
buf[0]=sigmoid(buf[0]);
|
||||
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor,in vec2 fragCoord){
|
||||
vec2 uv=fragCoord/uResolution.xy*2.-1.;
|
||||
uv.y*=-1.;
|
||||
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
|
||||
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec4 col;mainImage(col,gl_FragCoord.xy);
|
||||
col.rgb=hueShiftRGB(col.rgb,uHueShift);
|
||||
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
|
||||
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
|
||||
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
|
||||
|
||||
// Apply lightness control to pattern
|
||||
col.rgb = col.rgb * uPatternLightness;
|
||||
|
||||
// Use background color as base, then add pattern on top
|
||||
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
|
||||
|
||||
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
hueShift?: number;
|
||||
noiseIntensity?: number;
|
||||
scanlineIntensity?: number;
|
||||
speed?: number;
|
||||
scanlineFrequency?: number;
|
||||
warpAmount?: number;
|
||||
resolutionScale?: number;
|
||||
backgroundColor?: [number, number, number];
|
||||
backgroundOpacity?: number;
|
||||
patternLightness?: number;
|
||||
};
|
||||
|
||||
export function DarkVeil({
|
||||
hueShift = 0,
|
||||
noiseIntensity = 0,
|
||||
scanlineIntensity = 0,
|
||||
speed = 0.5,
|
||||
scanlineFrequency = 0,
|
||||
warpAmount = 0,
|
||||
resolutionScale = 1,
|
||||
backgroundColor = [0, 0, 0],
|
||||
backgroundOpacity = 1,
|
||||
patternLightness = 1.0,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
useEffect(() => {
|
||||
const canvas = ref.current as HTMLCanvasElement;
|
||||
const parent = canvas.parentElement as HTMLElement;
|
||||
|
||||
const renderer = new Renderer({
|
||||
dpr: Math.min(window.devicePixelRatio, 2),
|
||||
canvas,
|
||||
});
|
||||
|
||||
const gl = renderer.gl;
|
||||
const geometry = new Triangle(gl);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vec2() },
|
||||
uHueShift: { value: hueShift },
|
||||
uNoise: { value: noiseIntensity },
|
||||
uScan: { value: scanlineIntensity },
|
||||
uScanFreq: { value: scanlineFrequency },
|
||||
uWarp: { value: warpAmount },
|
||||
uBackgroundColor: { value: backgroundColor },
|
||||
uBackgroundOpacity: { value: backgroundOpacity },
|
||||
uPatternLightness: { value: patternLightness },
|
||||
},
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const resize = () => {
|
||||
const w = parent.clientWidth,
|
||||
h = parent.clientHeight;
|
||||
renderer.setSize(w * resolutionScale, h * resolutionScale);
|
||||
program.uniforms.uResolution.value.set(w, h);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const start = performance.now();
|
||||
let frame = 0;
|
||||
|
||||
const loop = () => {
|
||||
program.uniforms.uTime.value =
|
||||
((performance.now() - start) / 1000) * speed;
|
||||
program.uniforms.uHueShift.value = hueShift;
|
||||
program.uniforms.uNoise.value = noiseIntensity;
|
||||
program.uniforms.uScan.value = scanlineIntensity;
|
||||
program.uniforms.uScanFreq.value = scanlineFrequency;
|
||||
program.uniforms.uWarp.value = warpAmount;
|
||||
program.uniforms.uBackgroundColor.value = backgroundColor;
|
||||
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
|
||||
program.uniforms.uPatternLightness.value = patternLightness;
|
||||
renderer.render({ scene: mesh });
|
||||
frame = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, [
|
||||
hueShift,
|
||||
noiseIntensity,
|
||||
scanlineIntensity,
|
||||
speed,
|
||||
scanlineFrequency,
|
||||
warpAmount,
|
||||
resolutionScale,
|
||||
backgroundColor,
|
||||
backgroundOpacity,
|
||||
patternLightness,
|
||||
]);
|
||||
return <canvas ref={ref} className="w-full h-full block bg-background" />;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ function SkeletonBarChart(props: {
|
||||
fill={`url(#${fillAreaSkeletonId})`}
|
||||
radius={8}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
</AreaChart>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { BuyWidget, SwapWidget } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import {
|
||||
reportAssetBuyCancelled,
|
||||
reportAssetBuyFailed,
|
||||
@@ -25,16 +27,46 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { appMetadata } from "../../constants/connect";
|
||||
import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
|
||||
|
||||
type PageType = "asset" | "bridge" | "chain";
|
||||
|
||||
export function BuyAndSwapEmbed(props: {
|
||||
chain: Chain;
|
||||
tokenAddress: string | undefined;
|
||||
buyAmount: string | undefined;
|
||||
export type BuyAndSwapEmbedProps = {
|
||||
buyTab:
|
||||
| {
|
||||
buyToken:
|
||||
| {
|
||||
tokenAddress: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
swapTab:
|
||||
| {
|
||||
sellToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
buyToken:
|
||||
| {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
amount?: string;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
| undefined;
|
||||
pageType: PageType;
|
||||
}) {
|
||||
wallets?: Wallet[];
|
||||
};
|
||||
|
||||
export function BuyAndSwapEmbed(props: BuyAndSwapEmbedProps) {
|
||||
const { theme } = useTheme();
|
||||
const [tab, setTab] = useState<"buy" | "swap">("swap");
|
||||
const themeObj = getSDKTheme(theme === "light" ? "light" : "dark");
|
||||
@@ -83,24 +115,39 @@ export function BuyAndSwapEmbed(props: {
|
||||
|
||||
{tab === "buy" && (
|
||||
<BuyWidget
|
||||
amount={props.buyAmount || "1"}
|
||||
chain={props.chain}
|
||||
amount={props.buyTab?.buyToken?.amount || "1"}
|
||||
chain={
|
||||
props.buyTab?.buyToken?.chainId
|
||||
? defineChain(props.buyTab.buyToken.chainId)
|
||||
: undefined
|
||||
}
|
||||
tokenAddress={
|
||||
props.buyTab?.buyToken?.tokenAddress as `0x${string}` | undefined
|
||||
}
|
||||
className="!rounded-2xl !border-none"
|
||||
title=""
|
||||
client={client}
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
onError={(e, quote) => {
|
||||
const errorMessage = parseError(e);
|
||||
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyFailed({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -113,20 +160,27 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyFailed({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
error: errorMessage,
|
||||
contractType: undefined,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCancel={(quote) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuyCancelled({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
@@ -139,23 +193,30 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyCancelled({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSuccess={(quote) => {
|
||||
onSuccess={({ quote }) => {
|
||||
const buyChainId =
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined;
|
||||
|
||||
if (!buyChainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
reportTokenBuySuccessful({
|
||||
buyTokenChainId:
|
||||
quote.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenChainId: buyChainId,
|
||||
buyTokenAddress:
|
||||
quote.type === "buy"
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote.type === "onramp"
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
@@ -164,13 +225,13 @@ export function BuyAndSwapEmbed(props: {
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuySuccessful({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
chainId: buyChainId,
|
||||
contractType: undefined,
|
||||
is_testnet: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
theme={themeObj}
|
||||
tokenAddress={props.tokenAddress as `0x${string}`}
|
||||
paymentMethods={["card"]}
|
||||
/>
|
||||
)}
|
||||
@@ -180,18 +241,14 @@ export function BuyAndSwapEmbed(props: {
|
||||
client={client}
|
||||
theme={themeObj}
|
||||
className="!rounded-2xl !border-none"
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
wallets: props.wallets,
|
||||
appMetadata: appMetadata,
|
||||
}}
|
||||
prefill={{
|
||||
// buy this token by default
|
||||
buyToken: {
|
||||
chainId: props.chain.id,
|
||||
tokenAddress: props.tokenAddress,
|
||||
},
|
||||
// sell the native token by default (but if buytoken is a native token, don't set)
|
||||
sellToken: props.tokenAddress
|
||||
? {
|
||||
chainId: props.chain.id,
|
||||
}
|
||||
: undefined,
|
||||
buyToken: props.swapTab?.buyToken,
|
||||
sellToken: props.swapTab?.sellToken,
|
||||
}}
|
||||
onError={(error, quote) => {
|
||||
const errorMessage = parseError(error);
|
||||
@@ -204,7 +261,7 @@ export function BuyAndSwapEmbed(props: {
|
||||
pageType: props.pageType,
|
||||
});
|
||||
}}
|
||||
onSuccess={(quote) => {
|
||||
onSuccess={({ quote }) => {
|
||||
reportTokenSwapSuccessful({
|
||||
buyTokenChainId: quote.intent.destinationChainId,
|
||||
buyTokenAddress: quote.intent.destinationTokenAddress,
|
||||
@@ -236,9 +293,10 @@ function TabButton(props: {
|
||||
return (
|
||||
<Button
|
||||
onClick={props.onClick}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"rounded-full text-muted-foreground px-5 text-base bg-accent",
|
||||
props.isActive && "text-foreground border-foreground",
|
||||
"rounded-full text-muted-foreground px-4 bg-accent",
|
||||
props.isActive && "text-foreground border-foreground/50",
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
|
||||
@@ -172,7 +172,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
key={key}
|
||||
stackId={props.variant !== "stacked" ? undefined : "a"}
|
||||
stroke={`var(--color-${key})`}
|
||||
type="natural"
|
||||
type="monotone"
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -5,25 +5,20 @@ import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FaqSection(props: {
|
||||
export function FaqAccordion(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ type ShadcnSidebarBaseLink = {
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
type ShadcnSidebarLink =
|
||||
export type ShadcnSidebarLink =
|
||||
| ShadcnSidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: ShadcnSidebarBaseLink[];
|
||||
links: ShadcnSidebarLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
@@ -97,10 +97,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
|
||||
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
const activeLink = useActiveShadcnSidebarLink(props.links);
|
||||
const parentSubNav = props.links.find(
|
||||
(link) =>
|
||||
"subMenu" in link && link.links.some((l) => l.href === activeLink?.href),
|
||||
);
|
||||
const parentSubNav = findParentSubmenu(props.links, activeLink?.href);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
@@ -109,7 +106,7 @@ function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
className="h-4 bg-muted-foreground/50"
|
||||
orientation="vertical"
|
||||
/>
|
||||
{parentSubNav && "subMenu" in parentSubNav && (
|
||||
{parentSubNav && (
|
||||
<>
|
||||
<span className="text-sm">{parentSubNav.subMenu.label}</span>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
|
||||
@@ -131,24 +128,65 @@ function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("links" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
function walk(
|
||||
navLinks: ShadcnSidebarLink[],
|
||||
): ShadcnSidebarBaseLink | undefined {
|
||||
for (const link of navLinks) {
|
||||
if ("subMenu" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = walk(link.links);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return walk(links);
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
function findParentSubmenu(
|
||||
links: ShadcnSidebarLink[],
|
||||
activeHref: string | undefined,
|
||||
): Extract<ShadcnSidebarLink, { subMenu: unknown }> | undefined {
|
||||
if (!activeHref) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("subMenu" in link) {
|
||||
if (link.links.some((subLink) => subLink.href === activeHref)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
if ("links" in link && !("subMenu" in link)) {
|
||||
const nested = findParentSubmenu(link.links, activeHref);
|
||||
if (nested) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function useIsSubnavActive(links: ShadcnSidebarBaseLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
|
||||
@@ -12,10 +12,6 @@ import {
|
||||
type ProjectPageProps = {
|
||||
header: ProjectPageHeaderProps;
|
||||
footer?: ProjectPageFooterProps;
|
||||
/**
|
||||
* @deprecated only for legacy pages where we still need internal tabs for the moment, currently:
|
||||
* - /webhooks
|
||||
*/
|
||||
tabs?: TabPathLink[];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { useClipboard } from "@/hooks/useClipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SolanaAddress(props: {
|
||||
address: string;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const shortenedAddress = useMemo(() => {
|
||||
return props.shortenAddress !== false
|
||||
? `${props.address.slice(0, 4)}...${props.address.slice(-4)}`
|
||||
: props.address;
|
||||
}, [props.address, props.shortenAddress]);
|
||||
|
||||
const lessShortenedAddress = useMemo(() => {
|
||||
return `${props.address.slice(0, 8)}...${props.address.slice(-8)}`;
|
||||
}, [props.address]);
|
||||
|
||||
const { onCopy, hasCopied } = useClipboard(props.address, 2000);
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild tabIndex={-1}>
|
||||
<Button
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="link"
|
||||
>
|
||||
<div className="flex size-5 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500" />
|
||||
<span className="cursor-pointer font-mono text-sm">
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-80 border-border"
|
||||
onClick={(e) => {
|
||||
// do not close the hover card when clicking anywhere in the content
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Solana Public Key</h3>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="break-all rounded bg-muted p-3 text-center font-mono text-xs leading-relaxed">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<div className="rounded-lg bg-muted/50 p-3">
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
Solana public key for blockchain transactions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import type { SocialProfile } from "thirdweb/react";
|
||||
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { WalletAddress, WalletAddressUI } from "./wallet-address";
|
||||
|
||||
const meta = {
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/WalletAddress",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const client = storybookThirdwebClient as unknown as ThirdwebClient;
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-4xl flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Social Profiles Loaded">
|
||||
<WalletAddressUI
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
profiles={{
|
||||
data: vitalikEth,
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading Social Profiles">
|
||||
<WalletAddressUI
|
||||
address="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={client}
|
||||
shortenAddress
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: true,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Zero address">
|
||||
<WalletAddressUI
|
||||
address={ZERO_ADDRESS}
|
||||
client={client}
|
||||
profiles={{
|
||||
data: [],
|
||||
isPending: false,
|
||||
}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - Invalid Address">
|
||||
<WalletAddress address="not-an-address" client={client} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Real Component - vitalik.eth">
|
||||
<WalletAddress
|
||||
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const vitalikEth: SocialProfile[] = [
|
||||
{
|
||||
type: "ens",
|
||||
name: "vitalik.eth",
|
||||
bio: "mi pinxe lo crino tcati",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
metadata: {
|
||||
name: "vitalik.eth",
|
||||
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
avatar: "https://euc.li/vitalik.eth",
|
||||
description: "mi pinxe lo crino tcati",
|
||||
url: "https://vitalik.ca",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "farcaster",
|
||||
name: "vitalik.eth",
|
||||
bio: undefined,
|
||||
avatar:
|
||||
"https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
metadata: {
|
||||
fid: 5650,
|
||||
bio: undefined,
|
||||
pfp: "https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/b663cd63-fecf-4d0f-7f87-0e0b6fd42800/original",
|
||||
username: "vitalik.eth",
|
||||
addresses: [
|
||||
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"0x96B6bB2bd2Eba3b4Fbefd7DbAC448ad7B6CBf279",
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,11 +1,10 @@
|
||||
"use client";
|
||||
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { CircleSlashIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -13,41 +12,56 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useClipboard } from "@/hooks/useClipboard";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function WalletAddress(props: {
|
||||
type WalletAddressProps = {
|
||||
address: string | undefined;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
client: ThirdwebClient;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
|
||||
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
|
||||
return [
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address,
|
||||
`${address.slice(0, 14)}...${address.slice(-12)}`,
|
||||
];
|
||||
}, [address, props.shortenAddress]);
|
||||
};
|
||||
|
||||
export function WalletAddress(props: WalletAddressProps) {
|
||||
const profiles = useSocialProfiles({
|
||||
address: address,
|
||||
address: props.address,
|
||||
client: props.client,
|
||||
});
|
||||
|
||||
const { onCopy, hasCopied } = useClipboard(address, 2000);
|
||||
return (
|
||||
<WalletAddressUI
|
||||
{...props}
|
||||
profiles={{
|
||||
data: profiles.data || [],
|
||||
isPending: profiles.isPending,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function WalletAddressUI(
|
||||
props: WalletAddressProps & {
|
||||
profiles: {
|
||||
data: SocialProfile[];
|
||||
isPending: boolean;
|
||||
};
|
||||
},
|
||||
) {
|
||||
const address = props.address || ZERO_ADDRESS;
|
||||
|
||||
const shortenedAddress =
|
||||
props.shortenAddress !== false
|
||||
? `${address.slice(0, 6)}...${address.slice(-4)}`
|
||||
: address;
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return (
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline w-fit">
|
||||
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
@@ -78,7 +92,7 @@ export function WalletAddress(props: {
|
||||
<HoverCardTrigger asChild tabIndex={-1}>
|
||||
<Button
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
"flex flex-row items-center gap-2 px-0 max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -88,89 +102,82 @@ export function WalletAddress(props: {
|
||||
<WalletAvatar
|
||||
address={address}
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
profiles={props.profiles.data}
|
||||
thirdwebClient={props.client}
|
||||
fallbackIcon={props.fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
{profiles.data?.[0]?.name || shortenedAddress}
|
||||
<span className="cursor-pointer font-mono max-w-full truncate">
|
||||
{props.profiles.data?.[0]?.name || shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
className="w-80 border-border"
|
||||
className="w-[calc(100vw-2rem)] lg:w-[450px] border-border rounded-xl p-4 lg:p-6"
|
||||
onClick={(e) => {
|
||||
// do not close the hover card when clicking anywhere in the content
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Wallet Address</h3>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<h3 className="font-semibold text-lg">Social Profiles</h3>
|
||||
{profiles.isPending ? (
|
||||
<p className="text-muted-foreground text-sm">Loading profiles...</p>
|
||||
) : !profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm">Wallet Address</h3>
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<h4 className="font-semibold text-md">{profile.name}</h4>
|
||||
<Badge variant="outline">{profile.type}</Badge>
|
||||
<CopyTextButton
|
||||
textToShow={address}
|
||||
textToCopy={address}
|
||||
tooltip="Copy address"
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="text-muted-foreground -translate-x-1.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 border-t pt-5 border-dashed">
|
||||
<h3 className="font-medium text-sm">Social Profiles</h3>
|
||||
|
||||
{props.profiles.isPending ? (
|
||||
<Skeleton className="h-4 w-[50%]" />
|
||||
) : !props.profiles.data?.length ? (
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
<div className="!mt-2">
|
||||
{props.profiles.data?.map((profile) => {
|
||||
const walletAvatarLink = resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: profile.avatar,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center gap-3 py-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
<Avatar className="size-9">
|
||||
<AvatarImage
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
{profile.name.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<h4 className="text-sm leading-none">{profile.name}</h4>
|
||||
<span className="text-muted-foreground text-xs leading-none capitalize">
|
||||
{profile.type === "ens" ? "ENS" : profile.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { resetAnalytics } from "@/analytics/reset";
|
||||
import { CustomChainRenderer } from "@/components/misc/CustomChainRenderer";
|
||||
import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/LazyConfigureNetworkModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { popularChains } from "@/constants/popularChains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
|
||||
@@ -138,11 +139,7 @@ export const CustomConnectWallet = (props: {
|
||||
return (
|
||||
<>
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
chain={props.chain}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -280,11 +277,7 @@ export function useCustomConnectModal() {
|
||||
return useCallback(
|
||||
(options: { chain?: Chain; client: ThirdwebClient }) => {
|
||||
return connect({
|
||||
appMetadata: {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
appMetadata,
|
||||
chain: options?.chain,
|
||||
client: options.client,
|
||||
privacyPolicyUrl: "/privacy-policy",
|
||||
|
||||
+15
-5
@@ -14,6 +14,15 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const searchTypeLabels: Record<SearchType, string> = {
|
||||
email: "Email",
|
||||
phone: "Phone",
|
||||
id: "Auth Identifier",
|
||||
address: "Address",
|
||||
externalWallet: "External Wallet",
|
||||
userId: "User Identifier",
|
||||
};
|
||||
|
||||
export function AdvancedSearchInput(props: {
|
||||
onSearch: (searchType: SearchType, query: string) => void;
|
||||
onClear: () => void;
|
||||
@@ -46,13 +55,14 @@ export function AdvancedSearchInput(props: {
|
||||
value={searchType}
|
||||
onValueChange={(value) => setSearchType(value as SearchType)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-card">
|
||||
<SelectTrigger className="w-[140px] bg-background rounded-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="id">Auth Identifier</SelectItem>
|
||||
<SelectItem value="userId">User Identifier</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -61,8 +71,8 @@ export function AdvancedSearchInput(props: {
|
||||
<div className="flex flex-1">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-card pl-9 border-r-0 rounded-r-none"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
|
||||
placeholder={`Search by ${searchTypeLabels[searchType]}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -86,7 +96,7 @@ export function AdvancedSearchInput(props: {
|
||||
onClick={handleSearch}
|
||||
variant="outline"
|
||||
disabled={!query.trim() || props.isLoading}
|
||||
className="rounded-l-none gap-2 bg-card disabled:opacity-100"
|
||||
className="rounded-l-none rounded-r-full gap-2 bg-background disabled:opacity-100"
|
||||
>
|
||||
{props.isLoading && <Spinner className="size-4" />}
|
||||
Search
|
||||
|
||||
@@ -12,18 +12,25 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
|
||||
const getUserIdentifier = (user: WalletUser) => {
|
||||
const getAuthIdentifier = (user: WalletUser) => {
|
||||
const mainDetail = user.linkedAccounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id ??
|
||||
user.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function SearchResults(props: {
|
||||
results: WalletUser[];
|
||||
client: ThirdwebClient;
|
||||
@@ -51,6 +58,8 @@ export function SearchResults(props: {
|
||||
const mainDetail = user.linkedAccounts?.[0]?.details;
|
||||
const email = mainDetail?.email as string | undefined;
|
||||
const phone = mainDetail?.phone as string | undefined;
|
||||
const authIdentifier = getAuthIdentifier(user);
|
||||
const userIdentifier = user.id;
|
||||
|
||||
// Get external wallet addresses from linkedAccounts where type is 'siwe'
|
||||
const externalWalletAccounts =
|
||||
@@ -68,7 +77,34 @@ export function SearchResults(props: {
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
User Identifier
|
||||
</p>
|
||||
<p className="text-sm">{getUserIdentifier(user)}</p>
|
||||
{userIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userIdentifier)}
|
||||
textToCopy={userIdentifier}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Auth Identifier
|
||||
</p>
|
||||
{authIdentifier ? (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(authIdentifier)}
|
||||
textToCopy={authIdentifier}
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{walletAddress && (
|
||||
|
||||
+92
-54
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -28,22 +28,42 @@ import { SearchResults } from "./SearchResults";
|
||||
import { searchUsers } from "./searchUsers";
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
const getUserIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const getAuthIdentifier = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const mainDetail = accounts[0]?.details;
|
||||
return (
|
||||
mainDetail?.id ??
|
||||
mainDetail?.email ??
|
||||
mainDetail?.phone ??
|
||||
mainDetail?.address ??
|
||||
mainDetail?.id
|
||||
mainDetail?.address
|
||||
);
|
||||
};
|
||||
|
||||
const getExternalWallets = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
return accounts?.filter((account) => account.type === "siwe") || [];
|
||||
const getPrimaryEmail = (accounts: WalletUser["linkedAccounts"]) => {
|
||||
const emailFromPrimary = accounts[0]?.details?.email;
|
||||
if (emailFromPrimary) {
|
||||
return emailFromPrimary;
|
||||
}
|
||||
|
||||
const emailAccount = accounts.find((account) => {
|
||||
return typeof account.details?.email === "string" && account.details.email;
|
||||
});
|
||||
|
||||
if (emailAccount && typeof emailAccount.details.email === "string") {
|
||||
return emailAccount.details.email;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<WalletUser>();
|
||||
|
||||
const truncateIdentifier = (value: string) => {
|
||||
if (value.length <= 18) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, 8)}...${value.slice(-4)}`;
|
||||
};
|
||||
|
||||
export function InAppWalletUsersPageContent(
|
||||
props: {
|
||||
authToken: string;
|
||||
@@ -56,9 +76,30 @@ export function InAppWalletUsersPageContent(
|
||||
) {
|
||||
const columns = useMemo(() => {
|
||||
return [
|
||||
columnHelper.accessor("id", {
|
||||
cell: (cell) => {
|
||||
const userId = cell.getValue();
|
||||
|
||||
if (!userId) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={truncateIdentifier(userId)}
|
||||
textToCopy={userId}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
const identifier = getAuthIdentifier(cell.getValue());
|
||||
|
||||
if (!identifier) {
|
||||
return "N/A";
|
||||
@@ -66,21 +107,17 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={
|
||||
identifier.length > 30
|
||||
? `${identifier.slice(0, 30)}...`
|
||||
: identifier
|
||||
}
|
||||
textToShow={truncateIdentifier(identifier)}
|
||||
textToCopy={identifier}
|
||||
tooltip="Copy User Identifier"
|
||||
tooltip="Copy Auth Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
header: "User Identifier",
|
||||
id: "user_identifier",
|
||||
header: "Auth Identifier",
|
||||
id: "auth_identifier",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -94,33 +131,24 @@ export function InAppWalletUsersPageContent(
|
||||
}),
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const externalWallets = getExternalWallets(cell.getValue());
|
||||
if (externalWallets.length === 0) {
|
||||
return <span className="text-muted-foreground text-sm">None</span>;
|
||||
const email = getPrimaryEmail(cell.getValue());
|
||||
|
||||
if (!email) {
|
||||
return <span className="text-muted-foreground text-sm">N/A</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{externalWallets.slice(0, 2).map((account) => {
|
||||
const address = account.details?.address as string | undefined;
|
||||
return address ? (
|
||||
<div
|
||||
key={`external-${address}-${account.details?.id}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<WalletAddress address={address} client={props.client} />
|
||||
</div>
|
||||
) : null;
|
||||
})}
|
||||
{externalWallets.length > 2 && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
+{externalWallets.length - 2} more
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<CopyTextButton
|
||||
textToShow={email}
|
||||
textToCopy={email}
|
||||
tooltip="Copy Email"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
header: "External Wallets",
|
||||
id: "external_wallets",
|
||||
header: "Email",
|
||||
id: "email",
|
||||
}),
|
||||
columnHelper.accessor("wallets", {
|
||||
cell: (cell) => {
|
||||
@@ -230,20 +258,17 @@ export function InAppWalletUsersPageContent(
|
||||
});
|
||||
const csv = Papa.unparse(
|
||||
usersWallets.map((row) => {
|
||||
const externalWallets = getExternalWallets(row.linkedAccounts);
|
||||
const externalWalletAddresses = externalWallets
|
||||
.map((account) => account.details?.address)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
const email = getPrimaryEmail(row.linkedAccounts);
|
||||
|
||||
return {
|
||||
address: row.wallets[0]?.address || "Uninitialized",
|
||||
created: row.wallets[0]?.createdAt
|
||||
? new Date(row.wallets[0].createdAt).toISOString()
|
||||
: "Wallet not created yet",
|
||||
external_wallets: externalWalletAddresses || "None",
|
||||
email: email || "N/A",
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
user_identifier: getUserIdentifier(row.linkedAccounts),
|
||||
auth_identifier: getAuthIdentifier(row.linkedAccounts) || "N/A",
|
||||
user_identifier: row.id || "N/A",
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -264,11 +289,24 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Top section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col md:flex-row lg:items-center justify-end gap-3">
|
||||
<div className="w-full max-w-lg">
|
||||
<div className="overflow-hidden rounded-xl border border-border bg-card">
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
|
||||
<div>
|
||||
<div className="flex mb-3">
|
||||
<div className="p-2 rounded-full bg-background border border-border">
|
||||
<UserIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
User Wallets
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
View and manage your project's users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start lg:items-end gap-2.5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
|
||||
<div className="w-full lg:w-auto lg:min-w-[320px]">
|
||||
<AdvancedSearchInput
|
||||
onSearch={handleSearch}
|
||||
onClear={handleClearSearch}
|
||||
@@ -277,7 +315,7 @@ export function InAppWalletUsersPageContent(
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="gap-2 bg-card"
|
||||
className="gap-2 bg-background rounded-full"
|
||||
disabled={wallets.length === 0 || isPending}
|
||||
onClick={downloadCSV}
|
||||
variant="outline"
|
||||
@@ -298,11 +336,11 @@ export function InAppWalletUsersPageContent(
|
||||
data={wallets}
|
||||
isFetched={walletsQuery.isFetched}
|
||||
isPending={walletsQuery.isPending}
|
||||
tableContainerClassName="rounded-b-none"
|
||||
title="in-app wallets"
|
||||
tableContainerClassName="rounded-none border-x-0 border-b-0"
|
||||
title="User wallets"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center gap-3 rounded-b-lg border border-t-0 bg-card p-6">
|
||||
<div className="flex justify-center gap-3 border-t bg-card p-6">
|
||||
<Button
|
||||
className="gap-2 bg-background"
|
||||
disabled={activePage === 1 || walletsQuery.isPending}
|
||||
|
||||
@@ -18,10 +18,10 @@ configure({
|
||||
type APIWallet = ListUserWalletsResponses[200]["result"]["wallets"][0];
|
||||
type APIProfile = APIWallet["profiles"][0];
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
@@ -105,6 +105,9 @@ export async function searchUsers(
|
||||
case "externalWallet":
|
||||
queryParams.externalWalletAddress = query;
|
||||
break;
|
||||
case "userId":
|
||||
queryParams.userId = query;
|
||||
break;
|
||||
}
|
||||
|
||||
// Use the generated API function with Bearer authentication
|
||||
|
||||
@@ -3,4 +3,5 @@ export type SearchType =
|
||||
| "phone"
|
||||
| "id"
|
||||
| "address"
|
||||
| "externalWallet";
|
||||
| "externalWallet"
|
||||
| "userId";
|
||||
|
||||
@@ -44,12 +44,11 @@ function AnnouncementBannerUI(props: {
|
||||
}
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
// return (
|
||||
// <AnnouncementBannerUI
|
||||
// href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
|
||||
// label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
|
||||
// trackingLabel="product-rebrand"
|
||||
// />
|
||||
// );
|
||||
return null;
|
||||
return (
|
||||
<AnnouncementBannerUI
|
||||
href="https://blog.thirdweb.com/changelog/project-view-update-wallets/"
|
||||
label="Project View Update - Wallets"
|
||||
trackingLabel="project-view-update-wallets"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
<CreateProjectDialogUI
|
||||
createProject={async (params) => {
|
||||
const res = await createProjectClient(props.teamId, params);
|
||||
await createVaultAccountAndAccessToken({
|
||||
const vaultTokens = await createVaultAccountAndAccessToken({
|
||||
project: res.project,
|
||||
projectSecretKey: res.secret,
|
||||
}).catch((error) => {
|
||||
@@ -74,6 +74,13 @@ const CreateProjectDialog = (props: CreateProjectDialogProps) => {
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const managementAccessToken = vaultTokens.managementToken?.accessToken;
|
||||
|
||||
if (!managementAccessToken) {
|
||||
throw new Error("Missing management access token for project wallet");
|
||||
}
|
||||
|
||||
return {
|
||||
project: res.project,
|
||||
secret: res.secret,
|
||||
|
||||
@@ -41,7 +41,7 @@ export function CopyTextButton(props: {
|
||||
<Button
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground max-w-full truncate",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import { ArrowUpRightIcon, CheckIcon, CopyIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export function LinkWithCopyButton(props: {
|
||||
href: string;
|
||||
textToShow: string;
|
||||
textToCopy: string;
|
||||
copyTooltip: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const Icon = isCopied ? CheckIcon : CopyIcon;
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", props.className)}>
|
||||
<ToolTipLabel label={props.copyTooltip}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="p-1 h-auto shrink-0 opacity-70 hover:opacity-100 text-muted-foreground"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3" />
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:underline flex items-center gap-1 tabular-nums flex-1 truncate hover:text-foreground group"
|
||||
>
|
||||
<span className="max-w-full truncate">{props.textToShow}</span>
|
||||
<ArrowUpRightIcon className="size-3.5 opacity-70 shrink-0 group-hover:opacity-100" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const appMetadata = {
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
export const siwaExamplePrompts = [
|
||||
"How do I add in-app wallet with sign in with google to my react app?",
|
||||
"How do I add user wallet with sign in with google to my react app?",
|
||||
"How do I send a transaction in Unity?",
|
||||
"What does this contract revert error mean?",
|
||||
"I see thirdweb support id in my console log, can you help me?",
|
||||
|
||||
@@ -119,12 +119,17 @@ export function useCsvUpload<
|
||||
}
|
||||
Papa.parse(csv, {
|
||||
complete: (results) => {
|
||||
const data = props.csvParser(results.data as T[]);
|
||||
if (!data[0]?.address) {
|
||||
try {
|
||||
const data = props.csvParser(results.data as T[]);
|
||||
if (!data[0]?.address) {
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
setRawData(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setNoCsv(true);
|
||||
return;
|
||||
}
|
||||
setRawData(data);
|
||||
},
|
||||
header: true,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,10 @@ export async function fetchDashboardContractMetadata(
|
||||
|
||||
return {
|
||||
contractType: compilerMetadata?.name || "",
|
||||
image: contractMetadata.image || "",
|
||||
image:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: "",
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
};
|
||||
|
||||
@@ -76,10 +76,10 @@ const fetchAccountList = ({
|
||||
};
|
||||
};
|
||||
|
||||
// Transform API response to match existing WalletUser format
|
||||
// Transform API response to match the wallet user format
|
||||
function transformToWalletUser(apiWallet: APIWallet): WalletUser {
|
||||
return {
|
||||
id: getProfileId(apiWallet.profiles[0]) || "",
|
||||
id: apiWallet.userId || getProfileId(apiWallet.profiles[0]) || "",
|
||||
linkedAccounts: apiWallet.profiles.map((profile) => {
|
||||
// Create details object based on the profile data
|
||||
let details:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const PROJECT_WALLET_LABEL_SUFFIX = " Wallet";
|
||||
const PROJECT_WALLET_LABEL_MAX_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* Builds the default label for a project's primary server wallet.
|
||||
* Ensures a stable naming convention so the wallet can be identified across flows.
|
||||
*/
|
||||
export function getProjectWalletLabel(projectName: string | undefined) {
|
||||
const baseName = projectName?.trim() || "Project";
|
||||
const maxBaseLength = Math.max(
|
||||
1,
|
||||
PROJECT_WALLET_LABEL_MAX_LENGTH - PROJECT_WALLET_LABEL_SUFFIX.length,
|
||||
);
|
||||
|
||||
const normalizedBase =
|
||||
baseName.length > maxBaseLength
|
||||
? baseName.slice(0, maxBaseLength).trimEnd()
|
||||
: baseName;
|
||||
|
||||
return `${normalizedBase}${PROJECT_WALLET_LABEL_SUFFIX}`;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import "server-only";
|
||||
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { getProjectWalletLabel } from "@/lib/project-wallet";
|
||||
|
||||
export type ProjectWalletSummary = {
|
||||
id: string;
|
||||
address: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
type VaultWalletListItem = {
|
||||
id: string;
|
||||
address: string;
|
||||
metadata?: {
|
||||
label?: string;
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
type?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getProjectWallet(
|
||||
project: Project,
|
||||
): Promise<ProjectWalletSummary | undefined> {
|
||||
const engineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
|
||||
const managementAccessToken =
|
||||
engineCloudService?.managementAccessToken || undefined;
|
||||
const projectWalletAddress = (
|
||||
engineCloudService as { projectWalletAddress?: string } | undefined
|
||||
)?.projectWalletAddress;
|
||||
|
||||
if (
|
||||
!managementAccessToken ||
|
||||
!NEXT_PUBLIC_THIRDWEB_VAULT_URL ||
|
||||
!projectWalletAddress
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const vaultClient = await createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
});
|
||||
|
||||
const response = await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: 0,
|
||||
// @ts-expect-error - SDK expects snake_case for pagination arguments
|
||||
page_size: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const items = response.data.items as VaultWalletListItem[] | undefined;
|
||||
|
||||
if (!items?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaultLabel = getProjectWalletLabel(project.name);
|
||||
|
||||
const serverWallets = items.filter(
|
||||
(item) => item.metadata?.projectId === project.id,
|
||||
);
|
||||
|
||||
const defaultWallet = serverWallets.find(
|
||||
(item) =>
|
||||
item.address.toLowerCase() === projectWalletAddress.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!defaultWallet) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: defaultWallet.id,
|
||||
address: defaultWallet.address,
|
||||
label: defaultWallet.metadata?.label ?? defaultLabel,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to load project wallet", error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ export function teamSubscriptionsStub(
|
||||
// In-App Wallets
|
||||
{
|
||||
amount: usage.inAppWalletAmount?.amount || 0,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x In-App Wallets (Tier 1 at $0.00 / month)`,
|
||||
description: `${usage.inAppWalletAmount?.quantity || 0} x User Wallets (Tier 1 at $0.00 / month)`,
|
||||
thirdwebSku: "usage:in_app_wallet",
|
||||
},
|
||||
// AA Sponsorship
|
||||
|
||||
@@ -94,3 +94,62 @@ export interface AnalyticsQueryParams {
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsOverall {
|
||||
date: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByChainId {
|
||||
date: string;
|
||||
chainId: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByPayer {
|
||||
date: string;
|
||||
payer: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByReceiver {
|
||||
date: string;
|
||||
receiver: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export interface X402SettlementsByResource {
|
||||
date: string;
|
||||
resource: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
interface X402SettlementsByAsset {
|
||||
date: string;
|
||||
asset: string;
|
||||
totalRequests: number;
|
||||
totalValue: number;
|
||||
totalValueUSD: number;
|
||||
}
|
||||
|
||||
export type X402SettlementStats =
|
||||
| X402SettlementsOverall
|
||||
| X402SettlementsByChainId
|
||||
| X402SettlementsByPayer
|
||||
| X402SettlementsByReceiver
|
||||
| X402SettlementsByResource
|
||||
| X402SettlementsByAsset;
|
||||
|
||||
export interface X402QueryParams extends AnalyticsQueryParams {
|
||||
groupBy?: "overall" | "chainId" | "payer" | "resource" | "asset" | "receiver";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ const customClaimAmounts: Record<number, number> = {
|
||||
631571: 0.1,
|
||||
// Sophon testnet
|
||||
531050104: 5,
|
||||
// Arc testnet
|
||||
5042002: 1,
|
||||
// Injective testnet
|
||||
1439: 1,
|
||||
};
|
||||
|
||||
const defaultClaimAmount = 0.01;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const usdCurrencyFormatter = new Intl.NumberFormat("en-US", {
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2, // prefix with $
|
||||
maximumFractionDigits: 6, // prefix with $
|
||||
minimumFractionDigits: 0, // don't show decimal places if value is a whole number
|
||||
notation: "compact", // at max 2 decimal places
|
||||
roundingMode: "halfEven", // round to nearest even number, standard practice for financial calculations
|
||||
|
||||
@@ -23,7 +23,7 @@ export const TEAM_PLANS: Record<
|
||||
features: [
|
||||
"Email Support",
|
||||
"48hr Guaranteed Response",
|
||||
"Custom In-App Wallet Auth",
|
||||
"Custom User Wallet Auth",
|
||||
],
|
||||
price: 99,
|
||||
subTitle: "Everything in Starter, plus:",
|
||||
|
||||
@@ -21,8 +21,8 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
primaryButtonText: "hsl(var(--inverted-foreground))",
|
||||
primaryText: "hsl(var(--foreground))",
|
||||
scrollbarBg: "hsl(var(--muted))",
|
||||
secondaryButtonBg: "hsl(var(--secondary))",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary)/80%)",
|
||||
secondaryButtonBg: "hsl(var(--secondary)/70%)",
|
||||
secondaryButtonHoverBg: "hsl(var(--secondary))",
|
||||
secondaryButtonText: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconColor: "hsl(var(--secondary-foreground))",
|
||||
secondaryIconHoverBg: "hsl(var(--accent))",
|
||||
@@ -33,7 +33,7 @@ export function getSDKTheme(theme: "light" | "dark"): Theme {
|
||||
separatorLine: "hsl(var(--border))",
|
||||
skeletonBg: "hsl(var(--secondary-foreground)/15%)",
|
||||
success: "hsl(var(--success-text))",
|
||||
tertiaryBg: "hsl(var(--muted)/30%)",
|
||||
tertiaryBg: "hsl(var(--muted)/50%)",
|
||||
tooltipBg: "hsl(var(--popover))",
|
||||
tooltipText: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
|
||||
+14
-5
@@ -1,17 +1,26 @@
|
||||
"use client";
|
||||
import { NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyAndSwapEmbed
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineDashboardChain(props.chain.chainId, props.chain)}
|
||||
buyAmount={undefined}
|
||||
tokenAddress={undefined}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.chain.chainId,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
},
|
||||
}}
|
||||
pageType="chain"
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@
|
||||
import { reportChainPageview } from "@/analytics/report";
|
||||
import { useEffectOnce } from "@/hooks/useEffectOnce";
|
||||
|
||||
export function ChainPageView(props: { chainId: number }) {
|
||||
export function ChainPageView(props: { chainId: number; is_testnet: boolean }) {
|
||||
useEffectOnce(() => {
|
||||
reportChainPageview({
|
||||
chainId: props.chainId,
|
||||
is_testnet: props.is_testnet,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export default async function ChainPageLayout(props: {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<ChainPageView chainId={chain.chainId} />
|
||||
<ChainPageView chainId={chain.chainId} is_testnet={chain.testnet} />
|
||||
<div className="border-border border-b bg-card">
|
||||
<TeamHeader />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { FaqAccordion } from "@/components/blocks/faq-section";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
@@ -67,10 +67,21 @@ export default async function Page(props: Props) {
|
||||
)}
|
||||
|
||||
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
|
||||
<div className="py-10">
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
</div>
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqSection(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section className="py-10">
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<FaqAccordion faqs={props.faqs} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-3
@@ -10,8 +10,7 @@ import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { isRecord, maxUint256, type OverrideEntry } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
ClaimCondition as LegacyClaimCondition,
|
||||
@@ -86,7 +85,7 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
]);
|
||||
const snapshot = await fetchSnapshot(
|
||||
condition.merkleRoot,
|
||||
contractMetadata.merkle,
|
||||
isRecord(contractMetadata.merkle) ? contractMetadata.merkle : {},
|
||||
options.contract.client,
|
||||
);
|
||||
return {
|
||||
|
||||
+16
-9
@@ -225,6 +225,10 @@ export const InstallModuleForm = (props: InstallModuleFormProps) => {
|
||||
moduleInfo: {
|
||||
bytecodeUri: selectedModule.metadata.bytecodeUri,
|
||||
},
|
||||
isStylus:
|
||||
(selectedModule.metadata.compilers?.stylus &&
|
||||
selectedModule.metadata.compilers?.stylus?.length > 0) ||
|
||||
false,
|
||||
});
|
||||
},
|
||||
queryKey: [
|
||||
@@ -432,6 +436,7 @@ async function isModuleCompatible(options: {
|
||||
moduleInfo: {
|
||||
bytecodeUri: string;
|
||||
};
|
||||
isStylus: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
// 1. get module's bytecode
|
||||
@@ -444,15 +449,17 @@ async function isModuleCompatible(options: {
|
||||
|
||||
// 2. check compatibility with core and installed modules
|
||||
try {
|
||||
const isCompatible = await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
});
|
||||
const isCompatible =
|
||||
options.isStylus ||
|
||||
(await checkModulesCompatibility({
|
||||
chain: options.contractInfo.chain,
|
||||
client: options.client,
|
||||
coreBytecode: options.contractInfo.bytecode,
|
||||
moduleBytecodes: [
|
||||
moduleBytecode,
|
||||
...options.contractInfo.installedModuleBytecodes,
|
||||
],
|
||||
}));
|
||||
|
||||
return isCompatible;
|
||||
} catch (e) {
|
||||
|
||||
+21
-10
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Suspense, useState } from "react";
|
||||
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
|
||||
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
|
||||
@@ -89,7 +89,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<div className="flex h-[400px] items-center justify-center py-20">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
);
|
||||
@@ -179,15 +179,26 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<NFTDetailsTab client={contract.client} nft={nft} />
|
||||
)}
|
||||
|
||||
{tabs.map((tb) => {
|
||||
return (
|
||||
tb.title === tab && (
|
||||
<div className="w-full" key={tb.title}>
|
||||
{tb.children}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-full">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Spinner className="size-4" />
|
||||
<p className="text-sm text-muted-foreground"> Loading </p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{tabs.map((tb) => {
|
||||
return (
|
||||
tb.title === tab && (
|
||||
<div className="w-full" key={tb.title}>
|
||||
{tb.children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-1
@@ -47,7 +47,10 @@ export default async function Image({
|
||||
chainName: info.chainMetadata.name,
|
||||
contractAddress: info.serverContract.address,
|
||||
displayName: contractDisplayName,
|
||||
logo: contractMetadata.image,
|
||||
logo:
|
||||
contractMetadata.image && typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined,
|
||||
});
|
||||
} catch {
|
||||
return contractOGImageTemplate({
|
||||
|
||||
+5
-5
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
import type { Wallet } from "thirdweb/wallets";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
@@ -10,6 +12,7 @@ const client = getClientThirdwebClient();
|
||||
|
||||
export function PublicPageConnectButton(props: {
|
||||
connectButtonClassName?: string;
|
||||
wallets?: Wallet[];
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
@@ -17,11 +20,7 @@ export function PublicPageConnectButton(props: {
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
autoConnect={false}
|
||||
chains={allChainsV5}
|
||||
client={client}
|
||||
@@ -38,6 +37,7 @@ export function PublicPageConnectButton(props: {
|
||||
}}
|
||||
// we have an AutoConnect already added in root layout with AA configuration
|
||||
theme={getSDKTheme(t)}
|
||||
wallets={props.wallets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -5,11 +5,13 @@ import { useEffectOnce } from "@/hooks/useEffectOnce";
|
||||
export function AssetPageView(props: {
|
||||
assetType: "nft" | "coin";
|
||||
chainId: number;
|
||||
is_testnet: boolean;
|
||||
}) {
|
||||
useEffectOnce(() => {
|
||||
reportAssetPageview({
|
||||
assetType: props.assetType,
|
||||
chainId: props.chainId,
|
||||
is_testnet: props.is_testnet,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+3
@@ -122,6 +122,7 @@ export function TokenDropClaim(props: {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
toast.error("Failed to approve spending", {
|
||||
@@ -160,6 +161,7 @@ export function TokenDropClaim(props: {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
toast.error("Failed to buy tokens", {
|
||||
@@ -172,6 +174,7 @@ export function TokenDropClaim(props: {
|
||||
assetType: "coin",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC20",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
setStepsUI({
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUnixTime, subDays } from "date-fns";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
|
||||
@@ -35,6 +36,11 @@ export function useTokenTransfers(params: {
|
||||
url.searchParams.set("page", params.page.toString());
|
||||
url.searchParams.set("limit", params.limit.toString());
|
||||
url.searchParams.set("clientId", NEXT_PUBLIC_DASHBOARD_CLIENT_ID);
|
||||
const THIRTY_DAYS_AGO = subDays(new Date(), 30);
|
||||
url.searchParams.set(
|
||||
"block_timestamp_from",
|
||||
getUnixTime(THIRTY_DAYS_AGO).toString(),
|
||||
);
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
@@ -46,7 +52,12 @@ export function useTokenTransfers(params: {
|
||||
return data;
|
||||
},
|
||||
queryKey: ["token-transfers", params],
|
||||
refetchInterval: 5000,
|
||||
refetchInterval: (data) => {
|
||||
if (data?.state.error) {
|
||||
return false;
|
||||
}
|
||||
return 5000;
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
retryOnMount: false,
|
||||
|
||||
+65
-28
@@ -76,7 +76,11 @@ export async function ERC20PublicPage(props: {
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
|
||||
<AssetPageView
|
||||
assetType="coin"
|
||||
chainId={props.chainMetadata.chainId}
|
||||
is_testnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
<PageHeader containerClassName="max-w-7xl" />
|
||||
|
||||
<div className="border-b border-dashed">
|
||||
@@ -85,16 +89,31 @@ export async function ERC20PublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
image={contractMetadata.image}
|
||||
image={
|
||||
contractMetadata.image &&
|
||||
typeof contractMetadata.image === "string"
|
||||
? contractMetadata.image
|
||||
: undefined
|
||||
}
|
||||
isDashboardUser={isDashboardUser}
|
||||
name={contractMetadata.name}
|
||||
name={
|
||||
contractMetadata.name && typeof contractMetadata.name === "string"
|
||||
? contractMetadata.name
|
||||
: // if we do not have a contract name fall back to the address
|
||||
props.clientContract.address
|
||||
}
|
||||
socialUrls={
|
||||
typeof contractMetadata.social_urls === "object" &&
|
||||
contractMetadata.social_urls !== null
|
||||
? contractMetadata.social_urls
|
||||
: {}
|
||||
}
|
||||
symbol={contractMetadata.symbol}
|
||||
symbol={
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string"
|
||||
? contractMetadata.symbol
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,21 +158,25 @@ export async function ERC20PublicPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuyEmbed && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
{showBuyEmbed &&
|
||||
contractMetadata.name &&
|
||||
typeof contractMetadata.name === "string" &&
|
||||
contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyEmbed
|
||||
chainMetadata={props.chainMetadata}
|
||||
claimConditionMeta={claimConditionMeta}
|
||||
clientContract={props.clientContract}
|
||||
tokenAddress={props.clientContract.address}
|
||||
tokenDecimals={tokenDecimals}
|
||||
tokenName={contractMetadata.name}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
</GridPatternEmbedContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container flex max-w-7xl grow flex-col pb-10">
|
||||
<div className="flex grow flex-col gap-8">
|
||||
@@ -163,12 +186,15 @@ export async function ERC20PublicPage(props: {
|
||||
contractAddress={props.clientContract.address}
|
||||
/>
|
||||
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
{contractMetadata.symbol &&
|
||||
typeof contractMetadata.symbol === "string" && (
|
||||
<RecentTransfers
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
decimals={tokenDecimals}
|
||||
tokenSymbol={contractMetadata.symbol}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,9 +221,20 @@ function BuyEmbed(props: {
|
||||
if (!props.claimConditionMeta) {
|
||||
return (
|
||||
<BuyAndSwapEmbed
|
||||
chain={props.clientContract.chain}
|
||||
tokenAddress={props.clientContract.address}
|
||||
buyAmount={undefined}
|
||||
// chain={props.clientContract.chain}
|
||||
swapTab={{
|
||||
sellToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
buyToken: undefined,
|
||||
}}
|
||||
buyTab={{
|
||||
buyToken: {
|
||||
chainId: props.clientContract.chain.id,
|
||||
tokenAddress: props.clientContract.address,
|
||||
},
|
||||
}}
|
||||
pageType="asset"
|
||||
/>
|
||||
);
|
||||
|
||||
+5
-1
@@ -18,7 +18,11 @@ export function NFTPublicPageLayout(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
<AssetPageView assetType="nft" chainId={props.chainMetadata.chainId} />
|
||||
<AssetPageView
|
||||
assetType="nft"
|
||||
chainId={props.chainMetadata.chainId}
|
||||
is_testnet={props.chainMetadata.testnet}
|
||||
/>
|
||||
<PageHeader containerClassName="max-w-8xl" />
|
||||
<div className="border-b">
|
||||
<div className="container max-w-8xl">
|
||||
|
||||
+10
-3
@@ -53,6 +53,13 @@ export async function NFTPublicPage(props: {
|
||||
const _isTokenByIndexSupported =
|
||||
props.type === "erc721" && isTokenByIndexSupported(functionSelectors);
|
||||
|
||||
// FIXME: this is technically a bad fallback but we gotta do what we gotta do
|
||||
const contractMetadataWithNameAndSymbolFallback = {
|
||||
...contractMetadata,
|
||||
// fall back to the contract address if the name is not set
|
||||
name: contractMetadata.name || props.clientContract.address,
|
||||
symbol: contractMetadata.symbol || "",
|
||||
};
|
||||
const buyNFTDropCard = nftDropClaimParams ? (
|
||||
<BuyNFTDropCardServer
|
||||
chainMetadata={props.chainMetadata}
|
||||
@@ -73,7 +80,7 @@ export async function NFTPublicPage(props: {
|
||||
<NFTsGrid
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
gridClassName={
|
||||
buyNFTDropCard
|
||||
? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
@@ -110,7 +117,7 @@ export async function NFTPublicPage(props: {
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
contractCreator={contractCreator}
|
||||
contractMetadata={contractMetadata}
|
||||
contractMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
isDashboardUser={isDashboardUser}
|
||||
>
|
||||
<ResponsiveLayout
|
||||
@@ -138,7 +145,7 @@ export async function NFTPublicPage(props: {
|
||||
<PageLoadTokenViewerSheet
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={contractMetadata}
|
||||
collectionMetadata={contractMetadataWithNameAndSymbolFallback}
|
||||
tokenByIndexSupported={_isTokenByIndexSupported}
|
||||
tokenId={BigInt(props.tokenId)}
|
||||
type={props.type}
|
||||
|
||||
+4
@@ -120,6 +120,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
console.error(errorMessage);
|
||||
@@ -147,6 +148,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
assetType: "nft",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
props.onSuccess?.();
|
||||
@@ -163,6 +165,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -180,6 +183,7 @@ export function BuyEditionDrop(props: BuyEditionDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC1155",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+4
@@ -77,6 +77,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -103,6 +104,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
assetType: "nft",
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
props.onSuccess?.();
|
||||
@@ -115,6 +117,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -131,6 +134,7 @@ export function BuyNFTDrop(props: BuyNFTDropProps) {
|
||||
chainId: props.contract.chain.id,
|
||||
contractType: "DropERC721",
|
||||
error: errorMessage,
|
||||
is_testnet: props.chainMetadata.testnet,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+8
-3
@@ -37,7 +37,7 @@ export function TokenViewerSheet(
|
||||
clientContract: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
collectionMetadata: {
|
||||
name: string;
|
||||
name: string | null;
|
||||
image?: string;
|
||||
};
|
||||
type: "erc1155" | "erc721";
|
||||
@@ -57,6 +57,11 @@ export function TokenViewerSheet(
|
||||
) {
|
||||
const tokenId = props.variant === "fetch-data" ? props.tokenId : props.nft.id;
|
||||
|
||||
const collectionMetadataWithNameFallback = {
|
||||
...props.collectionMetadata,
|
||||
name: props.collectionMetadata.name || props.clientContract.address,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
@@ -78,7 +83,7 @@ export function TokenViewerSheet(
|
||||
<FetchAndRenderTokenInfo
|
||||
chainMetadata={props.chainMetadata}
|
||||
clientContract={props.clientContract}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
tokenByIndexSupported={props.tokenByIndexSupported}
|
||||
tokenId={props.tokenId}
|
||||
type={props.type}
|
||||
@@ -86,7 +91,7 @@ export function TokenViewerSheet(
|
||||
) : (
|
||||
<TokenInfoUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
collectionMetadata={props.collectionMetadata}
|
||||
collectionMetadata={collectionMetadataWithNameFallback}
|
||||
contract={props.clientContract}
|
||||
data={props.nft}
|
||||
tokenId={tokenId}
|
||||
|
||||
+4
-1
@@ -84,7 +84,10 @@ export const SettingsMetadata = ({
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
let image: string | undefined = metadata.data?.image;
|
||||
let image: string | undefined =
|
||||
metadata.data?.image && typeof metadata.data.image === "string"
|
||||
? metadata.data.image
|
||||
: undefined;
|
||||
try {
|
||||
image = image
|
||||
? // eslint-disable-next-line no-restricted-syntax
|
||||
|
||||
+1
@@ -137,6 +137,7 @@ export function SettingsPlatformFees({
|
||||
onChange={(value) =>
|
||||
form.setValue("platform_fee_basis_points", value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
value={form.watch("platform_fee_basis_points")}
|
||||
|
||||
+1
@@ -151,6 +151,7 @@ export function SettingsRoyalties({
|
||||
onChange={(value) =>
|
||||
form.setValue("seller_fee_basis_points", value, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
value={form.watch("seller_fee_basis_points")}
|
||||
|
||||
+17
-5
@@ -190,25 +190,37 @@ export async function generateContractLayoutMetadata(params: {
|
||||
.replace("Testnet", "")
|
||||
.trim();
|
||||
|
||||
const title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let title = `${contractDisplayName} | ${cleanedChainName} Smart Contract`;
|
||||
let description = "";
|
||||
|
||||
if (isERC721 || isERC1155) {
|
||||
description = `View tokens, source code, transactions, balances, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}.`;
|
||||
} else if (isERC20) {
|
||||
description = `View ERC20 tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
title = `${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} | Buy, Swap, Bridge & Price`;
|
||||
description = `Buy, swap & bridge ${contractMetadata.name} (${contractMetadata.symbol}) on ${cleanedChainName} with thirdweb Bridge. View contract address, holders, analytics, transactions and live price.`;
|
||||
} else {
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractDisplayName} smart contract on ${cleanedChainName}`;
|
||||
description = `View tokens, transactions, balances, source code, and analytics for the ${contractMetadata.name} smart contract on ${cleanedChainName}`;
|
||||
}
|
||||
|
||||
return {
|
||||
description: description,
|
||||
title: title,
|
||||
openGraph: {
|
||||
description: description,
|
||||
title: title,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
const fallbackTitle = `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`;
|
||||
const fallbackDescription = `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`;
|
||||
|
||||
return {
|
||||
description: `View tokens, transactions, balances, source code, and analytics for the smart contract on Chain ID ${params.chainIdOrSlug}`,
|
||||
title: `${shortenIfAddress(params.contractAddress)} | ${params.chainIdOrSlug}`,
|
||||
description: fallbackDescription,
|
||||
title: fallbackTitle,
|
||||
openGraph: {
|
||||
description: fallbackDescription,
|
||||
title: fallbackTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import type { Status } from "thirdweb/bridge";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { BridgeStatus } from "./bridge-status";
|
||||
|
||||
const meta = {
|
||||
component: BridgeStatus,
|
||||
title: "chain/tx/bridge-status",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container max-w-6xl py-10">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof BridgeStatus>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const completedStatus: Status = {
|
||||
status: "COMPLETED",
|
||||
paymentId:
|
||||
"0x8e0a0d9c0254a75a3004a28e0fb69bae7f2d659938ddd461753627270930db60",
|
||||
originAmount: 129935n,
|
||||
destinationAmount: 100000n,
|
||||
transactions: [
|
||||
{
|
||||
chainId: 42161,
|
||||
transactionHash:
|
||||
"0x37daf698878e73590adb2d6f833e810304a9981829ad983fb355245af1183107",
|
||||
},
|
||||
{
|
||||
chainId: 8453,
|
||||
transactionHash:
|
||||
"0xc72abba2761bb48f605125dc12c17b15088303db960816fa12316f0c90796417",
|
||||
},
|
||||
],
|
||||
originChainId: 42161,
|
||||
originTokenAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
originToken: {
|
||||
chainId: 42161,
|
||||
address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: 6,
|
||||
iconUri: "https://ethereum-optimism.github.io/data/USDC/logo.png",
|
||||
},
|
||||
destinationChainId: 8453,
|
||||
destinationTokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
destinationToken: {
|
||||
chainId: 8453,
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: 6,
|
||||
iconUri:
|
||||
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
|
||||
},
|
||||
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
|
||||
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
|
||||
};
|
||||
|
||||
const pendingStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "PENDING",
|
||||
};
|
||||
|
||||
const failedStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "FAILED",
|
||||
};
|
||||
|
||||
const notFoundStatus: Status = {
|
||||
...completedStatus,
|
||||
status: "NOT_FOUND",
|
||||
transactions: [],
|
||||
};
|
||||
|
||||
export const Completed: Story = {
|
||||
args: {
|
||||
bridgeStatus: completedStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Pending: Story = {
|
||||
args: {
|
||||
bridgeStatus: pendingStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
bridgeStatus: failedStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const NotFound: Story = {
|
||||
args: {
|
||||
bridgeStatus: notFoundStatus,
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPurchaseData: Story = {
|
||||
args: {
|
||||
bridgeStatus: {
|
||||
...completedStatus,
|
||||
purchaseData: {
|
||||
userId: "68d645b7ded999651272bf1e",
|
||||
credits: 32000,
|
||||
transactionId: "fd2606d1-90df-45c6-bd2c-b19a34764a31",
|
||||
},
|
||||
},
|
||||
client: storybookThirdwebClient,
|
||||
},
|
||||
};
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { Img } from "@workspace/ui/components/img";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpRightIcon,
|
||||
CircleCheckIcon,
|
||||
CircleXIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import type { Status, Token } from "thirdweb/bridge";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import { toTokens } from "thirdweb/utils";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { LinkWithCopyButton } from "@/components/ui/link-with-copy-button";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchChain } from "@/utils/fetchChain";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
|
||||
type PurchaseData = Exclude<Status, { status: "NOT_FOUND" }>["purchaseData"];
|
||||
|
||||
export function BridgeStatus(props: {
|
||||
bridgeStatus: Status;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { bridgeStatus } = props;
|
||||
const purchaseDataString =
|
||||
bridgeStatus.status !== "NOT_FOUND" && bridgeStatus.purchaseData
|
||||
? getPurchaseData(bridgeStatus.purchaseData)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-xl border relative">
|
||||
{bridgeStatus.status === "FAILED" &&
|
||||
bridgeStatus.transactions.length > 0 && (
|
||||
<FailedBridgeStatusContent
|
||||
transactions={bridgeStatus.transactions}
|
||||
client={props.client}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(bridgeStatus.status === "COMPLETED" ||
|
||||
bridgeStatus.status === "PENDING") && (
|
||||
<BridgeStatusContent
|
||||
bridgeStatus={bridgeStatus}
|
||||
client={props.client}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="px-6 lg:px-8 py-7 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground "> Status </p>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 text-sm capitalize",
|
||||
bridgeStatus.status === "COMPLETED" && "text-green-500",
|
||||
bridgeStatus.status === "PENDING" && "text-blue-500",
|
||||
bridgeStatus.status === "FAILED" && "text-red-500",
|
||||
)}
|
||||
>
|
||||
{bridgeStatus.status === "COMPLETED" && (
|
||||
<CircleCheckIcon className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "PENDING" && (
|
||||
<Spinner className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "FAILED" && (
|
||||
<CircleXIcon className="size-3.5" />
|
||||
)}
|
||||
{bridgeStatus.status === "NOT_FOUND" && (
|
||||
<CircleXIcon className="size-3.5" />
|
||||
)}
|
||||
|
||||
{bridgeStatus.status.toLowerCase().replace("_", " ")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Payment ID</p>
|
||||
<CopyTextButton
|
||||
textToCopy={bridgeStatus.paymentId}
|
||||
textToShow={bridgeStatus.paymentId}
|
||||
tooltip="Payment ID"
|
||||
className="text-sm -translate-x-1.5 tabular-nums text-muted-foreground"
|
||||
copyIconPosition="left"
|
||||
iconClassName="text-muted-foreground/70"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{purchaseDataString && (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-2 border-t border-dashed">
|
||||
<p className="text-sm text-foreground ">Purchase Data</p>
|
||||
<CodeClient
|
||||
code={purchaseDataString}
|
||||
lang="json"
|
||||
className="[&_code]:text-xs"
|
||||
scrollableClassName="p-3"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TokenInfo(props: {
|
||||
label: string;
|
||||
addressLabel: string;
|
||||
token: Token;
|
||||
amountWei: bigint | undefined;
|
||||
walletAddress: string;
|
||||
client: ThirdwebClient;
|
||||
txHash: string | undefined;
|
||||
}) {
|
||||
const chainQuery = useChainQuery(props.token.chainId);
|
||||
const isNativeToken =
|
||||
props.token.address.toLowerCase() === NATIVE_TOKEN_ADDRESS.toLowerCase();
|
||||
|
||||
const tokenAddress = getAddress(props.token.address);
|
||||
|
||||
return (
|
||||
<div className="flex-1 pt-10 pb-9 px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{props.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="h-6" />
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative hover:ring-2 hover:ring-offset-2 hover:ring-offset-card hover:ring-foreground/30 rounded-full">
|
||||
<Link
|
||||
href={
|
||||
props.token.address.toLowerCase() ===
|
||||
NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
target="_blank"
|
||||
aria-label="View Token"
|
||||
className="absolute inset-0 z-10"
|
||||
/>
|
||||
|
||||
<Img
|
||||
src={props.token.iconUri}
|
||||
alt={props.token.name}
|
||||
className="size-10 rounded-full"
|
||||
fallback={
|
||||
<div className="size-10 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="absolute -bottom-0.5 -right-0.5 bg-background p-0.5 rounded-full">
|
||||
<Img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: chainQuery.data?.icon?.url,
|
||||
})}
|
||||
alt={props.token.name}
|
||||
className="size-4 rounded-full"
|
||||
fallback={
|
||||
<div className="size-4 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-lg text-foreground font-medium tracking-tight leading-none">
|
||||
{props.amountWei ? (
|
||||
<>
|
||||
{toTokens(props.amountWei, props.token.decimals)}{" "}
|
||||
{props.token.symbol}
|
||||
</>
|
||||
) : (
|
||||
"N/A"
|
||||
)}
|
||||
</p>
|
||||
<SkeletonContainer
|
||||
skeletonData={`Chain ${props.token.chainId}`}
|
||||
loadedData={chainQuery.data?.name}
|
||||
className="[&_*]:flex"
|
||||
render={(data) => (
|
||||
<Link
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}`}
|
||||
className="text-sm text-muted-foreground leading-none hover:underline flex items-center gap-1"
|
||||
target="_blank"
|
||||
>
|
||||
{data}{" "}
|
||||
<ArrowUpRightIcon className="size-3.5 text-muted-foreground/70 shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">{props.addressLabel}</p>
|
||||
<WalletAddress
|
||||
address={getAddress(props.walletAddress)}
|
||||
client={props.client}
|
||||
className="py-0.5 h-auto text-sm [&>*]:!font-sans tabular-nums [&>*]:font-normal text-muted-foreground hover:text-foreground"
|
||||
iconClassName="size-3"
|
||||
shortenAddress={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Token Address</p>
|
||||
<LinkWithCopyButton
|
||||
href={
|
||||
isNativeToken
|
||||
? `/${chainQuery.data?.slug || props.token.chainId}`
|
||||
: `/${chainQuery.data?.slug || props.token.chainId}/${props.token.address}`
|
||||
}
|
||||
textToShow={tokenAddress}
|
||||
textToCopy={tokenAddress}
|
||||
copyTooltip="Copy Token Address"
|
||||
className="-translate-x-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground ">Transaction Hash</p>
|
||||
{props.txHash ? (
|
||||
<LinkWithCopyButton
|
||||
className="-translate-x-1"
|
||||
href={`/${chainQuery.data?.slug || props.token.chainId}/tx/${props.txHash}`}
|
||||
textToShow={`${props.txHash.slice(0, 12)}...${props.txHash.slice(-4)}`}
|
||||
textToCopy={props.txHash}
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground ">N/A</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BridgeStatusContent(props: {
|
||||
bridgeStatus: Exclude<Status, { status: "FAILED" } | { status: "NOT_FOUND" }>;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { bridgeStatus } = props;
|
||||
|
||||
const fromTxHash = bridgeStatus.transactions.find(
|
||||
(tx) => tx.chainId === bridgeStatus.originChainId,
|
||||
)?.transactionHash;
|
||||
const toTxHash = bridgeStatus.transactions.find(
|
||||
(tx) => tx.chainId === bridgeStatus.destinationChainId,
|
||||
)?.transactionHash;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 border-b border-dashed">
|
||||
<TokenInfo
|
||||
label="From"
|
||||
token={bridgeStatus.originToken}
|
||||
amountWei={bridgeStatus.originAmount}
|
||||
walletAddress={bridgeStatus.sender}
|
||||
client={props.client}
|
||||
txHash={fromTxHash}
|
||||
addressLabel="Sender"
|
||||
/>
|
||||
|
||||
<div className="border-t lg:border-l lg:border-t-0 border-dashed relative">
|
||||
<div className="p-1.5 rounded-full bg-background absolute top-0 lg:top-1/2 left-1/2 lg:left-0 -translate-y-1/2 -translate-x-1/2 border">
|
||||
<ArrowRightIcon className="size-4 text-muted-foreground rotate-90 lg:rotate-0" />
|
||||
</div>
|
||||
<TokenInfo
|
||||
label="To"
|
||||
token={bridgeStatus.destinationToken}
|
||||
// TODO - destinationAmount is not available in PENDING status, fix it in API
|
||||
amountWei={
|
||||
bridgeStatus.status === "COMPLETED"
|
||||
? bridgeStatus.destinationAmount
|
||||
: undefined
|
||||
}
|
||||
walletAddress={bridgeStatus.receiver}
|
||||
client={props.client}
|
||||
txHash={toTxHash}
|
||||
addressLabel="Receiver"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FailedBridgeStatusContent(props: {
|
||||
transactions: { chainId: number; transactionHash: string }[];
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<div className="px-6 lg:px-8 py-7 space-y-1.5 border-b border-dashed">
|
||||
<h3 className="text-lg font-medium tracking-tight mb-4">Transactions</h3>
|
||||
<div className="space-y-4">
|
||||
{props.transactions.map((tx) => {
|
||||
return (
|
||||
<TxHashRow
|
||||
key={tx.chainId}
|
||||
client={props.client}
|
||||
chainId={tx.chainId}
|
||||
txHash={tx.transactionHash}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useChainQuery(chainId: number | undefined) {
|
||||
return useQuery({
|
||||
enabled: !!chainId,
|
||||
queryKey: ["chain-metadata", chainId],
|
||||
queryFn: async () => {
|
||||
if (!chainId) {
|
||||
return null;
|
||||
}
|
||||
return fetchChain(chainId);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function TxHashRow(props: {
|
||||
client: ThirdwebClient;
|
||||
chainId: number;
|
||||
txHash: string;
|
||||
}) {
|
||||
const chainQuery = useChainQuery(props.chainId);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Img
|
||||
src={resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: chainQuery.data?.icon?.url,
|
||||
})}
|
||||
alt={chainQuery.data?.name}
|
||||
className="size-3.5 rounded-full"
|
||||
fallback={
|
||||
<div className="size-3.5 from-blue-500 to-foreground rounded-full bg-gradient-to-b" />
|
||||
}
|
||||
/>
|
||||
<SkeletonContainer
|
||||
skeletonData={`Chain ${props.chainId}`}
|
||||
loadedData={chainQuery.data?.name}
|
||||
render={(data) => <p className="text-sm text-foreground">{data}</p>}
|
||||
/>
|
||||
</div>
|
||||
<LinkWithCopyButton
|
||||
href={`/${chainQuery.data?.slug || props.chainId}/tx/${props.txHash}`}
|
||||
textToShow={props.txHash}
|
||||
textToCopy={props.txHash}
|
||||
copyTooltip="Copy Transaction Hash"
|
||||
className="-translate-x-0.5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BridgeStatusWithPolling(props: {
|
||||
bridgeStatus: Status;
|
||||
client: ThirdwebClient;
|
||||
chainId: number;
|
||||
transactionHash: string;
|
||||
}) {
|
||||
const bridgeStatusQuery = useQuery({
|
||||
enabled:
|
||||
props.bridgeStatus.status === "PENDING" ||
|
||||
props.bridgeStatus.status === "NOT_FOUND",
|
||||
queryKey: ["bridge-status", props.transactionHash, props.chainId],
|
||||
queryFn: () =>
|
||||
status({
|
||||
chainId: props.chainId,
|
||||
transactionHash: props.transactionHash as `0x${string}`,
|
||||
client: props.client,
|
||||
}),
|
||||
refetchInterval(query) {
|
||||
const status = query.state.data?.status;
|
||||
if (status === "COMPLETED" || status === "FAILED") {
|
||||
return false;
|
||||
}
|
||||
return 5000;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BridgeStatus
|
||||
bridgeStatus={bridgeStatusQuery.data || props.bridgeStatus}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getPurchaseData(purchaseData: PurchaseData) {
|
||||
try {
|
||||
return JSON.stringify(purchaseData, null, 2);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
+355
-126
@@ -1,16 +1,42 @@
|
||||
import { toTokens, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
|
||||
import { ToolTipLabel } from "@workspace/ui/components/tooltip";
|
||||
import { formatDate, formatDistanceToNowStrict, fromUnixTime } from "date-fns";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
CircleCheckIcon,
|
||||
Clock4Icon,
|
||||
InfoIcon,
|
||||
} from "lucide-react";
|
||||
import { getAddress, toTokens } from "thirdweb";
|
||||
import { status } from "thirdweb/bridge";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import {
|
||||
eth_getBlockByHash,
|
||||
eth_getTransactionByHash,
|
||||
eth_getTransactionReceipt,
|
||||
getRpcClient,
|
||||
} from "thirdweb/rpc";
|
||||
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
|
||||
import type { TransactionReceipt } from "thirdweb/transaction";
|
||||
import { hexToNumber, toEther } from "thirdweb/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
import { getChain } from "../../../utils";
|
||||
import { BridgeStatusWithPolling } from "./bridge-status";
|
||||
import { BridgeAndOverviewTabs } from "./tabs";
|
||||
|
||||
type Transaction = Awaited<ReturnType<typeof eth_getTransactionByHash>>;
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ chain_id: string; txHash: `0x${string}` }>;
|
||||
@@ -25,142 +51,345 @@ export default async function Page(props: {
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
|
||||
const [transaction, receipt] = await Promise.all([
|
||||
const [transaction, receipt, bridgeStatus] = await Promise.all([
|
||||
eth_getTransactionByHash(rpcRequest, {
|
||||
hash: params.txHash,
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
eth_getTransactionReceipt(rpcRequest, {
|
||||
hash: params.txHash,
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
status({
|
||||
chainId: chain.chainId,
|
||||
transactionHash: params.txHash,
|
||||
client: serverThirdwebClient,
|
||||
}).catch(() => undefined),
|
||||
]);
|
||||
if (!transaction.blockHash) {
|
||||
return <div>no tx found</div>;
|
||||
|
||||
if (!transaction?.blockHash || !receipt) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center grow">
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<div className="p-2 rounded-full bg-background border">
|
||||
<CircleAlertIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
Transaction not found
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const block = await eth_getBlockByHash(rpcRequest, {
|
||||
blockHash: transaction.blockHash,
|
||||
});
|
||||
|
||||
const overviewContent = (
|
||||
<GeneericTxDetails
|
||||
transaction={transaction}
|
||||
receipt={receipt}
|
||||
block={block}
|
||||
chain={chain}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="container flex flex-col gap-y-10 py-6">
|
||||
<header className="flex flex-col gap-y-2">
|
||||
<p className="font-bold text-3xl">Transaction Details</p>
|
||||
<div className="flex gap-x-2">
|
||||
{shortenAddress(transaction.from)} called on{" "}
|
||||
{shortenAddress(transaction.to || ZERO_ADDRESS)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="pb-20">
|
||||
<div className="flex h-14 items-center border-border border-b">
|
||||
<Breadcrumb className="container max-w-6xl">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={`/${chain.slug}`}>
|
||||
{chain.name.replace("Mainnet", "")}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Transaction Details</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
<div className="container max-w-6xl py-10">
|
||||
<h1 className="font-semibold text-3xl tracking-tight">
|
||||
Transaction Details
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Transaction hash</div>
|
||||
<p>{transaction.hash}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
Status {/* and method */}
|
||||
</div>
|
||||
<div className="flex gap-x-2">
|
||||
<Badge
|
||||
className={
|
||||
receipt.status === "success" ? "bg-green-500" : "bg-red-500"
|
||||
}
|
||||
>
|
||||
{receipt.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Block</div>
|
||||
<p>{Number(transaction.blockNumber)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Timestamp</div>
|
||||
<p>
|
||||
{new Date(
|
||||
Number.parseInt(block.timestamp.toString()) * 1000,
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">From</div>
|
||||
<p>{transaction.from}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
Interacted with contract
|
||||
</div>
|
||||
<p>{transaction.to}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Value</div>
|
||||
<p>{toEther(transaction.value)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Transaction fee</div>
|
||||
<p>{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas price</div>
|
||||
<p>{toEther(transaction.gasPrice || 0n)} ETH</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas usage</div>
|
||||
<p>{receipt.gasUsed.toString()}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Gas fees (Gwei)</div>
|
||||
<p>
|
||||
Base: {toTokens(block.baseFeePerGas || 0n, 9)} | Max:{" "}
|
||||
{toTokens(transaction.maxFeePerGas || 0n, 9)} | Max priority:{" "}
|
||||
{transaction.maxPriorityFeePerGas?.toString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Burnt fees</div>
|
||||
<p>{toEther(block.baseFeePerGas || 0n * receipt.gasUsed)}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator orientation="horizontal" />
|
||||
|
||||
<section className="flex flex-col gap-y-2">
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="flex items-center gap-x-2">Other</div>
|
||||
<p>
|
||||
Txn type: {hexToNumber(transaction.typeHex || "0x0")} (
|
||||
{transaction.type}) | Nonce: {transaction.nonce} | Position:{" "}
|
||||
{transaction.transactionIndex}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 items-start">
|
||||
<div className="flex items-center gap-x-2">Raw Input:</div>
|
||||
<div className="break-words rounded-lg bg-card p-4 text-sm">
|
||||
{transaction.input}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div className="container max-w-6xl">
|
||||
{bridgeStatus ? (
|
||||
<BridgeAndOverviewTabs
|
||||
bridgeStatus={
|
||||
<BridgeStatusWithPolling
|
||||
bridgeStatus={bridgeStatus}
|
||||
client={getClientThirdwebClient()}
|
||||
chainId={chain.chainId}
|
||||
transactionHash={params.txHash}
|
||||
/>
|
||||
}
|
||||
overview={overviewContent}
|
||||
/>
|
||||
) : (
|
||||
overviewContent
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneericTxDetails(props: {
|
||||
transaction: Transaction;
|
||||
receipt: TransactionReceipt;
|
||||
block: {
|
||||
timestamp: bigint;
|
||||
baseFeePerGas: bigint | null;
|
||||
};
|
||||
chain: ChainMetadata;
|
||||
}) {
|
||||
const { transaction, receipt, chain, block } = props;
|
||||
|
||||
const timestamp = getDatefromTimestamp(block.timestamp);
|
||||
|
||||
const fromAddress = getAddress(transaction.from);
|
||||
const toAddress = transaction.to ? getAddress(transaction.to) : undefined;
|
||||
|
||||
return (
|
||||
<div className="border rounded-xl p-4 lg:p-6 bg-card">
|
||||
{/* section 1 */}
|
||||
<section className="border-b pb-6 space-y-5 lg:space-y-3">
|
||||
<GridItem
|
||||
label="Transaction hash"
|
||||
tooltip="A TxHash or transaction hash is a unique 66-character identifier that is generated whenever a transaction is executed."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={transaction.hash}
|
||||
textToShow={transaction.hash}
|
||||
tooltip="Copy transaction hash"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
<GridItem label="Status" tooltip="The status of the transaction.">
|
||||
<Badge
|
||||
className="py-0.5 gap-1.5 px-1.5 capitalize"
|
||||
variant={receipt.status === "success" ? "success" : "destructive"}
|
||||
>
|
||||
{receipt.status === "success" ? (
|
||||
<CircleCheckIcon className="size-3" />
|
||||
) : (
|
||||
<CircleAlertIcon className="size-3" />
|
||||
)}
|
||||
{receipt.status}
|
||||
</Badge>
|
||||
</GridItem>
|
||||
|
||||
{transaction.blockNumber && (
|
||||
<GridItem
|
||||
label="Block"
|
||||
tooltip="Number of the block in which the transaction is recorded. Block confirmations indicate how many blocks have been added since the transaction was produced."
|
||||
>
|
||||
<p>{Number(transaction.blockNumber)}</p>
|
||||
</GridItem>
|
||||
)}
|
||||
|
||||
{timestamp && (
|
||||
<GridItem
|
||||
label="Timestamp"
|
||||
tooltip="The date and time at which a transaction is validated."
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<Clock4Icon className="size-3" />
|
||||
<p>
|
||||
{formatDistanceToNowStrict(timestamp, {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
<p>({formatDate(timestamp, "PP pp z")})</p>
|
||||
</div>
|
||||
</GridItem>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* section 2 */}
|
||||
<section className="border-b py-6 space-y-5 lg:space-y-3">
|
||||
<GridItem label="From" tooltip="The sending party of the transaction.">
|
||||
<CopyTextButton
|
||||
textToCopy={fromAddress}
|
||||
textToShow={fromAddress}
|
||||
tooltip="Copy from address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
|
||||
{toAddress && (
|
||||
<GridItem
|
||||
label="Interacted with (To)"
|
||||
tooltip="The receiving party of the transaction (could be a contract address)."
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={toAddress}
|
||||
textToShow={toAddress}
|
||||
tooltip="Copy to address"
|
||||
variant="ghost"
|
||||
copyIconPosition="right"
|
||||
className="truncate -translate-x-1.5 max-w-full"
|
||||
/>
|
||||
</GridItem>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* section 3 */}
|
||||
<section className="py-6 space-y-5 lg:space-y-3 border-b">
|
||||
<GridItem
|
||||
label="Value"
|
||||
tooltip="The value being transacted in ETH and fiat value. Note: You can click the fiat value (if available) to see historical value at the time of transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther(transaction.value)} {chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Transaction Fee"
|
||||
tooltip="Amount paid to the validator for processing the transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther((transaction.gasPrice || 0n) * receipt.gasUsed)}{" "}
|
||||
{chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Price"
|
||||
tooltip="The price per gas unit the sender is willing to pay"
|
||||
>
|
||||
<p>
|
||||
{toEther(transaction.gasPrice || 0n)} {chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Usage"
|
||||
tooltip="The amount of gas used by the transaction."
|
||||
>
|
||||
<p>{receipt.gasUsed.toString()}</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Gas Fees"
|
||||
tooltip="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively."
|
||||
>
|
||||
<p className="flex gap-2.5">
|
||||
<span>Base: {toTokens(block.baseFeePerGas || 0n, 9)} Gwei</span>
|
||||
|
||||
{transaction.maxFeePerGas && (
|
||||
<>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<span>
|
||||
Max: {toTokens(transaction.maxFeePerGas || 0n, 9)} Gwei
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{transaction.maxPriorityFeePerGas && (
|
||||
<>
|
||||
<span className="text-muted-foreground/50">|</span>
|
||||
<span>
|
||||
Max priority: {transaction.maxPriorityFeePerGas?.toString()}{" "}
|
||||
Gwei
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Burnt fees"
|
||||
tooltip="The amount of fees that were burnt by the transaction."
|
||||
>
|
||||
<p>
|
||||
{toEther((block.baseFeePerGas || 0n) * receipt.gasUsed)}{" "}
|
||||
{chain.nativeCurrency?.symbol}
|
||||
</p>
|
||||
</GridItem>
|
||||
</section>
|
||||
|
||||
{/* section 4 */}
|
||||
<section className="pt-6 space-y-5 lg:space-y-3">
|
||||
<GridItem
|
||||
label="Other Attributes"
|
||||
tooltip="Other data related to this transaction."
|
||||
>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Txn type:</span>
|
||||
<span className="capitalize">
|
||||
{hexToNumber(transaction.typeHex || "0x0")} ({transaction.type})
|
||||
</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Nonce: </span>
|
||||
<span>{transaction.nonce}</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="gap-1 py-1">
|
||||
<span className="text-muted-foreground">Position: </span>
|
||||
<span>{transaction.transactionIndex}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
label="Input Data"
|
||||
tooltip="Additional data included for this transaction. Commonly used as part of contract interaction or as a message sent to the recipient."
|
||||
>
|
||||
<PlainTextCodeBlock
|
||||
code={transaction.input}
|
||||
className="[&_code]:!whitespace-pre-wrap break-all [&_code]:text-xs text-muted-foreground w-full"
|
||||
scrollableClassName="max-h-[400px] p-2 min-h-[80px]"
|
||||
/>
|
||||
</GridItem>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridItem(props: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
tooltip?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1 lg:grid grid-cols-[300px_1fr] items-start">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<ToolTipLabel label={props.tooltip}>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="h-auto w-auto p-1 hidden lg:block"
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
<p className="text-muted-foreground text-sm">{props.label}</p>
|
||||
</div>
|
||||
<div className="text-sm w-full">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getDatefromTimestamp(timestamp: bigint) {
|
||||
try {
|
||||
return fromUnixTime(Number(timestamp));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
|
||||
export function BridgeAndOverviewTabs(props: {
|
||||
bridgeStatus: React.ReactNode;
|
||||
overview: React.ReactNode;
|
||||
}) {
|
||||
const [tab, setTab] = useState<"bridge" | "overview">("bridge");
|
||||
return (
|
||||
<div>
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
name: "Bridge",
|
||||
onClick: () => setTab("bridge"),
|
||||
isActive: tab === "bridge",
|
||||
},
|
||||
{
|
||||
name: "Overview",
|
||||
onClick: () => setTab("overview"),
|
||||
isActive: tab === "overview",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-3" />
|
||||
{tab === "bridge" && props.bridgeStatus}
|
||||
{tab === "overview" && props.overview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -183,13 +183,64 @@ const STYLUS = {
|
||||
],
|
||||
description:
|
||||
"Airdrop your NFTs or tokens to a large number of recipients. Built with Arbitrum Stylus.",
|
||||
displayName: "Arbitrum Stylus Contracts",
|
||||
displayName: "Airdrop - Arbitrum Stylus",
|
||||
id: "stylus",
|
||||
name: "Stylus",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const MODULAR_CONTRACTS_STYLUS = {
|
||||
contracts: [
|
||||
// erc721 token
|
||||
[
|
||||
"thirdweb.eth/ERC721CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC721",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC721",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC721",
|
||||
],
|
||||
{
|
||||
description: "ERC721 NFTs that only owners can mint.",
|
||||
title: "Modular NFT Collection",
|
||||
},
|
||||
],
|
||||
// erc1155 token
|
||||
[
|
||||
"thirdweb.eth/ERC1155CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC1155",
|
||||
"deployer.thirdweb.eth/BatchMetadataERC1155",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC1155",
|
||||
"deployer.thirdweb.eth/SequentialTokenIdERC1155",
|
||||
],
|
||||
{
|
||||
description: "ERC1155 NFTs that only owners can mint.",
|
||||
title: "Modular Edition",
|
||||
},
|
||||
],
|
||||
// erc20 token
|
||||
[
|
||||
"thirdweb.eth/ERC20CoreInitializable",
|
||||
[
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusMintableERC20",
|
||||
"0x6453a486d52e0eb6e79ec4491038e2522a926936/StylusTransferableERC20",
|
||||
],
|
||||
{
|
||||
description: "ERC20 Tokens that only owners can mint.",
|
||||
title: "Modular Token",
|
||||
},
|
||||
],
|
||||
],
|
||||
description:
|
||||
"Collection of highly customizable and upgradeable smart contracts built with the modular contracts framework.",
|
||||
displayName: "Modular Contracts - Arbitrum Stylus",
|
||||
id: "modular-contracts",
|
||||
name: "modular",
|
||||
showInExplore: true,
|
||||
isBeta: true,
|
||||
} satisfies ExploreCategory;
|
||||
|
||||
const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[POPULAR.id]: POPULAR,
|
||||
[NFTS.id]: NFTS,
|
||||
@@ -203,6 +254,7 @@ const CATEGORIES: Record<string, ExploreCategory> = {
|
||||
[COMMERCE.id]: COMMERCE,
|
||||
[STAKING.id]: STAKING,
|
||||
[GOVERNANCE.id]: GOVERNANCE,
|
||||
[MODULAR_CONTRACTS_STYLUS.id]: MODULAR_CONTRACTS_STYLUS,
|
||||
};
|
||||
|
||||
export function getCategory(id: string) {
|
||||
|
||||
+5
@@ -50,6 +50,7 @@ import {
|
||||
} from "@/constants/addresses";
|
||||
import { LAST_USED_PROJECT_ID, LAST_USED_TEAM_ID } from "@/constants/cookie";
|
||||
import { ZERO_FEE_CHAINS } from "@/constants/fee-config";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import {
|
||||
useCustomFactoryAbi,
|
||||
useFunctionParamsFromABI,
|
||||
@@ -206,6 +207,8 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
|
||||
const activeAccount = useActiveAccount();
|
||||
const walletChain = useActiveWalletChain();
|
||||
const walletChainMetadata = useV5DashboardChain(walletChain?.id);
|
||||
|
||||
const { onError } = useTxNotifications(
|
||||
"Successfully deployed contract",
|
||||
"Failed to deploy contract",
|
||||
@@ -656,6 +659,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
chainId: walletChain.id,
|
||||
contractName: metadata.name,
|
||||
publisher: rewriteTwPublisher(metadata.publisher),
|
||||
is_testnet: walletChainMetadata?.testnet,
|
||||
});
|
||||
|
||||
deployStatusModal.nextStep();
|
||||
@@ -694,6 +698,7 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
contractName: metadata.name,
|
||||
errorMessage: parsedError,
|
||||
publisher: rewriteTwPublisher(metadata.publisher),
|
||||
is_testnet: walletChain.testnet,
|
||||
});
|
||||
|
||||
deployStatusModal.close();
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client";
|
||||
import { BookOpenIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ToggleThemeButton } from "@/components/blocks/color-mode-toggle";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "../../../components/ThirdwebMiniLogo";
|
||||
import { PublicPageConnectButton } from "../../(chain)/[chain_id]/[contractAddress]/public-pages/_components/PublicPageConnectButton";
|
||||
@@ -9,7 +14,7 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
<div className="border-b">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-3",
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
@@ -22,18 +27,49 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1 lg:gap-2">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
target="_blank"
|
||||
aria-label="View Documentation"
|
||||
className="text-sm text-muted-foreground hover:text-foreground p-2 hover:bg-accent rounded-full transition-all duration-100"
|
||||
>
|
||||
<BookOpenIcon className="size-5" />
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
</div>
|
||||
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[36px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full p-2 text-muted-foreground hover:text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-5 " />
|
||||
) : (
|
||||
<MoonIcon className="size-5 " />
|
||||
)}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
|
||||
import { reportTokenRowClicked } from "@/analytics/report";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -39,6 +40,7 @@ export function TokensTable(props: {
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Market cap</TableHead>
|
||||
<TableHead>Volume (24h)</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -64,6 +66,9 @@ export function TokensTable(props: {
|
||||
<TableCell>
|
||||
<Skeleton className="ml-auto h-4 w-20" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="ml-auto h-4 w-20" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: tokens.length > 0
|
||||
@@ -81,6 +86,7 @@ export function TokensTable(props: {
|
||||
<AvatarImage
|
||||
src={token.iconUri ?? undefined}
|
||||
alt={token.symbol}
|
||||
className="rounded-full"
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{token.symbol?.slice(0, 2)?.toUpperCase()}
|
||||
@@ -90,20 +96,9 @@ export function TokensTable(props: {
|
||||
<span className="font-medium leading-none text-base">
|
||||
{token.symbol}
|
||||
</span>
|
||||
<Link
|
||||
href={
|
||||
token.address.toLowerCase() ===
|
||||
NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? `/${token.chainId}`
|
||||
: `/${token.chainId}/${token.address}`
|
||||
}
|
||||
prefetch={false}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground leading-none">
|
||||
{token.name}
|
||||
</span>
|
||||
</Link>
|
||||
<span className="text-sm text-muted-foreground leading-none">
|
||||
{token.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -120,6 +115,35 @@ export function TokensTable(props: {
|
||||
? formatUsdCompact(token.volume24hUsd)
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 rounded-full bg-card hover:border-foreground/50 text-muted-foreground hover:bg-inverted hover:text-inverted-foreground"
|
||||
>
|
||||
<Link
|
||||
onClick={() =>
|
||||
reportTokenRowClicked({
|
||||
chainId: token.chainId,
|
||||
tokenAddress: token.address,
|
||||
})
|
||||
}
|
||||
aria-label={`Buy ${token.symbol}`}
|
||||
href={
|
||||
token.address.toLowerCase() ===
|
||||
NATIVE_TOKEN_ADDRESS.toLowerCase()
|
||||
? `/${token.chainId}`
|
||||
: `/${token.chainId}/${token.address}`
|
||||
}
|
||||
prefetch={false}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
Buy
|
||||
<ArrowRightIcon className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BringToFrontIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { bridgeStats } from "../../../bridge/data";
|
||||
import { PageHeader } from "./components/header";
|
||||
import { TokenPage } from "./components/token-page";
|
||||
|
||||
@@ -35,13 +35,11 @@ export default async function Page() {
|
||||
<h1 className="text-4xl font-bold tracking-tighter mb-1">
|
||||
Discover and swap any tokens on any chain, instantly
|
||||
</h1>
|
||||
<Link
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
href="https://thirdweb.com/monetize/bridge"
|
||||
target="_blank"
|
||||
>
|
||||
Powered by thirdweb bridge
|
||||
</Link>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{bridgeStats.supportedChains} chains, {bridgeStats.supportedTokens}{" "}
|
||||
tokens and {bridgeStats.supportedRoutes} routes supported
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<TokenPage chains={chains} />
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getPlanCancelUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CancelPlanPage(props: {
|
||||
params: Promise<{
|
||||
team_id: string;
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
getInvoicePaymentUrl,
|
||||
} from "../../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function CheckoutPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { redirect } from "next/navigation";
|
||||
import { StripeRedirectErrorPage } from "../../_components/StripeRedirectErrorPage";
|
||||
import { getBillingPortalUrl } from "../../utils/billing";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ManageBillingPage(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
"use client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { AutoConnect } from "thirdweb/react";
|
||||
import type { SmartWalletOptions } from "thirdweb/wallets";
|
||||
|
||||
export function TWAutoConnect(props: {
|
||||
accountAbstraction?: SmartWalletOptions;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
return (
|
||||
<AutoConnect
|
||||
accountAbstraction={props.accountAbstraction}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
export function TWAutoConnect(props: { client: ThirdwebClient }) {
|
||||
return <AutoConnect client={props.client} />;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,9 @@ export default async function DropPage({
|
||||
]);
|
||||
|
||||
const thumbnail =
|
||||
project.thumbnail || nft.metadata.image || contractMetadata.image || "";
|
||||
project.thumbnail ||
|
||||
nft.metadata.image ||
|
||||
(typeof contractMetadata.image === "string" ? contractMetadata.image : "");
|
||||
|
||||
const displayName = contractMetadata.name || nft.metadata.name || "";
|
||||
|
||||
|
||||
+11
-1
@@ -55,7 +55,7 @@ export function CreateProjectFormOnboarding(props: {
|
||||
<CreateProjectForm
|
||||
createProject={async (params) => {
|
||||
const res = await createProjectClient(props.teamId, params);
|
||||
await createVaultAccountAndAccessToken({
|
||||
const vaultTokens = await createVaultAccountAndAccessToken({
|
||||
project: res.project,
|
||||
projectSecretKey: res.secret,
|
||||
}).catch((error) => {
|
||||
@@ -65,6 +65,16 @@ export function CreateProjectFormOnboarding(props: {
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const managementAccessToken =
|
||||
vaultTokens.managementToken?.accessToken;
|
||||
|
||||
if (!managementAccessToken) {
|
||||
throw new Error(
|
||||
"Missing management access token for project wallet",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
project: res.project,
|
||||
secret: res.secret,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
HelpCircleIcon,
|
||||
HomeIcon,
|
||||
SettingsIcon,
|
||||
WalletCardsIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
|
||||
@@ -18,11 +17,6 @@ export function TeamSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
ecosystems: Array<{ name: string; slug: string }>;
|
||||
chainSubscriptions: Array<{
|
||||
chainId: number;
|
||||
chainName: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}) {
|
||||
const { layoutPath, children } = props;
|
||||
|
||||
@@ -72,24 +66,6 @@ export function TeamSidebarLayout(props: {
|
||||
{
|
||||
separator: true,
|
||||
} as const,
|
||||
// infrastructure below here
|
||||
// TODO: make this one link to an overview page that has a list of all the chains currently deployed
|
||||
{
|
||||
links: [
|
||||
...props.chainSubscriptions.map((chainSubscription) => ({
|
||||
href: `${layoutPath}/~/infrastructure/${chainSubscription.slug}`,
|
||||
label: chainSubscription.chainName,
|
||||
})),
|
||||
{
|
||||
href: `${layoutPath}/~/infrastructure/deploy`,
|
||||
label: "Deploy Infrastructure",
|
||||
},
|
||||
],
|
||||
subMenu: {
|
||||
icon: WalletCardsIcon,
|
||||
label: "Chain Infrastucture",
|
||||
},
|
||||
},
|
||||
]}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
@@ -116,7 +92,7 @@ export function TeamSidebarLayout(props: {
|
||||
label: "Documentation",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
href: "https://playground.thirdweb.com/wallets/sign-in/button",
|
||||
icon: BoxIcon,
|
||||
label: "Playground",
|
||||
},
|
||||
|
||||
@@ -4,13 +4,11 @@ import { getAuthToken, getAuthTokenWalletAddress } from "@/api/auth-token";
|
||||
import { getProjects } from "@/api/project/projects";
|
||||
import { fetchEcosystemList } from "@/api/team/ecosystems";
|
||||
import { getTeamBySlug, getTeams } from "@/api/team/get-team";
|
||||
import { getChainSubscriptions } from "@/api/team/team-subscription";
|
||||
import { CustomChatButton } from "@/components/chat/CustomChatButton";
|
||||
import { AnnouncementBanner } from "@/components/misc/AnnouncementBanner";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { siwaExamplePrompts } from "@/constants/siwa-example-prompts";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain } from "../../../(dashboard)/(chain)/utils";
|
||||
import { TeamHeaderLoggedIn } from "../../components/TeamHeader/team-header-logged-in.client";
|
||||
import { StaffModeNotice } from "./_components/StaffModeNotice";
|
||||
import { TeamSidebarLayout } from "./TeamSidebarLayout";
|
||||
@@ -21,52 +19,26 @@ export default async function TeamLayout(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const [
|
||||
accountAddress,
|
||||
account,
|
||||
teams,
|
||||
authToken,
|
||||
team,
|
||||
ecosystems,
|
||||
chainSubscriptions,
|
||||
] = await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getValidAccount(`/team/${params.team_slug}`),
|
||||
getTeams(),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
fetchEcosystemList(params.team_slug),
|
||||
getChainSubscriptions(params.team_slug),
|
||||
]);
|
||||
const [accountAddress, account, teams, authToken, team, ecosystems] =
|
||||
await Promise.all([
|
||||
getAuthTokenWalletAddress(),
|
||||
getValidAccount(`/team/${params.team_slug}`),
|
||||
getTeams(),
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
fetchEcosystemList(params.team_slug),
|
||||
]);
|
||||
|
||||
if (!teams || !accountAddress || !authToken || !team) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const [teamsAndProjects, chainSidebarLinks] = await Promise.all([
|
||||
Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
projects: await getProjects(team.slug),
|
||||
team,
|
||||
})),
|
||||
),
|
||||
chainSubscriptions
|
||||
? await Promise.all(
|
||||
chainSubscriptions.map(async (chainSubscription) => {
|
||||
if (!chainSubscription.chainId) {
|
||||
throw new Error("Chain ID is required");
|
||||
}
|
||||
const chain = await getChain(chainSubscription.chainId);
|
||||
|
||||
return {
|
||||
chainId: chain.chainId,
|
||||
chainName: chain.name,
|
||||
slug: chain.slug,
|
||||
};
|
||||
}),
|
||||
).catch(() => [])
|
||||
: [],
|
||||
]);
|
||||
const teamsAndProjects = await Promise.all(
|
||||
teams.map(async (team) => ({
|
||||
projects: await getProjects(team.slug),
|
||||
team,
|
||||
})),
|
||||
);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
@@ -92,9 +64,6 @@ export default async function TeamLayout(props: {
|
||||
</div>
|
||||
|
||||
<TeamSidebarLayout
|
||||
chainSubscriptions={chainSidebarLinks.sort(
|
||||
(a, b) => a.chainId - b.chainId,
|
||||
)}
|
||||
ecosystems={ecosystems.map((ecosystem) => ({
|
||||
name: ecosystem.name,
|
||||
slug: ecosystem.slug,
|
||||
|
||||
+29
-22
@@ -25,6 +25,34 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
|
||||
import { searchParams } from "../search-params";
|
||||
|
||||
function getStatusBadge(invoice: Stripe.Invoice) {
|
||||
switch (invoice.status) {
|
||||
case "paid":
|
||||
return <Badge variant="success">Paid</Badge>;
|
||||
case "open":
|
||||
// we treat "uncollectible" as unpaid
|
||||
case "uncollectible": {
|
||||
// if the invoice has a due date, use that, otherwise use the effective at date (which is the date the invoice was created)
|
||||
const effectiveDueDate = invoice.due_date || invoice.effective_at;
|
||||
|
||||
// if the invoice due date is in the past, we want to display it as past due
|
||||
if (effectiveDueDate && effectiveDueDate * 1000 < Date.now()) {
|
||||
return <Badge variant="destructive">Past Due</Badge>;
|
||||
}
|
||||
return <Badge variant="outline">Open</Badge>;
|
||||
}
|
||||
case "void":
|
||||
return <Badge variant="secondary">Void</Badge>;
|
||||
|
||||
default:
|
||||
return <Badge variant="outline">Unknown</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
function isInvoicePayable(invoice: Stripe.Invoice) {
|
||||
return invoice.status === "open" || invoice.status === "uncollectible";
|
||||
}
|
||||
|
||||
export function BillingHistory(props: {
|
||||
teamSlug: string;
|
||||
invoices: Stripe.Invoice[];
|
||||
@@ -57,27 +85,6 @@ export function BillingHistory(props: {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (invoice: Stripe.Invoice) => {
|
||||
switch (invoice.status) {
|
||||
case "paid":
|
||||
return <Badge variant="success">Paid</Badge>;
|
||||
case "open":
|
||||
// we treate "uncollectible" as unpaid
|
||||
case "uncollectible": {
|
||||
// if the invoice due date is in the past, we want to display it as past due
|
||||
if (invoice.due_date && invoice.due_date * 1000 < Date.now()) {
|
||||
return <Badge variant="destructive">Past Due</Badge>;
|
||||
}
|
||||
return <Badge variant="outline">Open</Badge>;
|
||||
}
|
||||
case "void":
|
||||
return <Badge variant="secondary">Void</Badge>;
|
||||
|
||||
default:
|
||||
return <Badge variant="outline">Unknown</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (props.invoices.length === 0) {
|
||||
if (props.status === "open") {
|
||||
return (
|
||||
@@ -126,7 +133,7 @@ export function BillingHistory(props: {
|
||||
<TableCell>{getStatusBadge(invoice)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-2">
|
||||
{invoice.status === "open" && (
|
||||
{isInvoicePayable(invoice) && (
|
||||
<>
|
||||
{/* always show the crypto payment button */}
|
||||
<ToolTipLabel
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ const authOptions = [
|
||||
"apple",
|
||||
"coinbase",
|
||||
"line",
|
||||
"epic",
|
||||
] as const satisfies AuthOption[];
|
||||
|
||||
type AuthOptionsFormData = {
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@ export const SmartWalletsBillingAlert = (props: { teamSlug: string }) => {
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>Account Abstraction on Mainnet</AlertTitle>
|
||||
<AlertTitle>Gas Sponsorship on Mainnet</AlertTitle>
|
||||
<AlertDescription>
|
||||
To enable AA on mainnet chains,{" "}
|
||||
To enable Gas Sponsorship on mainnet chains,{" "}
|
||||
<UnderlineLink href={`/team/${props.teamSlug}/~/billing`}>
|
||||
subscribe to a billing plan.
|
||||
</UnderlineLink>
|
||||
|
||||
-8
@@ -14,7 +14,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
|
||||
export function DefaultFactoriesSection() {
|
||||
const data = [
|
||||
@@ -38,13 +37,6 @@ export function DefaultFactoriesSection() {
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Ready to use account factories that are pre-deployed on each chain.{" "}
|
||||
<UnderlineLink
|
||||
href="https://playground.thirdweb.com/connect/account-abstraction/connect"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn how to use these in your apps
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
+6
-158
@@ -1,163 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { SearchParams } from "nuqs/server";
|
||||
import { getUserOpUsage } from "@/api/analytics";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { getProject } from "@/api/project/projects";
|
||||
import { getTeamBySlug } from "@/api/team/get-team";
|
||||
import {
|
||||
getLastNDaysRange,
|
||||
type Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { getAbsoluteUrl } from "@/utils/vercel";
|
||||
import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary";
|
||||
import { SmartWalletsBillingAlert } from "./Alerts";
|
||||
import { AccountAbstractionAnalytics } from "./aa-analytics";
|
||||
import { searchParamLoader } from "./search-params";
|
||||
|
||||
interface PageParams {
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// Redirect old Account Abstraction page to new Sponsored Gas Overview
|
||||
export default async function Page(props: {
|
||||
params: Promise<PageParams>;
|
||||
searchParams: Promise<SearchParams>;
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
}) {
|
||||
const [params, searchParams, authToken] = await Promise.all([
|
||||
props.params,
|
||||
searchParamLoader(props.searchParams),
|
||||
getAuthToken(),
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [team, project] = await Promise.all([
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const interval = searchParams.interval ?? "week";
|
||||
const rangeType = searchParams.range || "last-120";
|
||||
|
||||
const range: Range = {
|
||||
from:
|
||||
rangeType === "custom"
|
||||
? searchParams.from
|
||||
: getLastNDaysRange(rangeType).from,
|
||||
to:
|
||||
rangeType === "custom"
|
||||
? searchParams.to
|
||||
: getLastNDaysRange(rangeType).to,
|
||||
type: rangeType,
|
||||
};
|
||||
|
||||
const userOpStats = await getUserOpUsage(
|
||||
{
|
||||
from: range.from,
|
||||
period: interval,
|
||||
projectId: project.id,
|
||||
teamId: project.teamId,
|
||||
to: range.to,
|
||||
},
|
||||
authToken,
|
||||
);
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
});
|
||||
|
||||
const isBundlerServiceEnabled = !!project.services.find(
|
||||
(s) => s.name === "bundler",
|
||||
);
|
||||
|
||||
const hasSmartWalletsWithoutBilling =
|
||||
isBundlerServiceEnabled &&
|
||||
team.billingStatus !== "validPayment" &&
|
||||
team.billingStatus !== "pastDue";
|
||||
|
||||
return (
|
||||
<ProjectPage
|
||||
header={{
|
||||
icon: SmartAccountIcon,
|
||||
client,
|
||||
title: "Account Abstraction",
|
||||
description:
|
||||
"Integrate EIP-7702 and EIP-4337 compliant smart accounts for gasless sponsorships and more.",
|
||||
actions: null,
|
||||
settings: {
|
||||
href: `/team/${params.team_slug}/${params.project_slug}/settings/account-abstraction`,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
type: "docs",
|
||||
href: "https://portal.thirdweb.com/transactions/sponsor",
|
||||
},
|
||||
{
|
||||
type: "playground",
|
||||
href: "https://playground.thirdweb.com/account-abstraction/eip-7702",
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{hasSmartWalletsWithoutBilling && (
|
||||
<>
|
||||
<SmartWalletsBillingAlert teamSlug={params.team_slug} />
|
||||
<div className="h-10" />
|
||||
</>
|
||||
)}
|
||||
<div className="flex grow flex-col gap-10">
|
||||
<AccountAbstractionSummary
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<AccountAbstractionAnalytics
|
||||
client={client}
|
||||
projectId={project.id}
|
||||
teamId={project.teamId}
|
||||
teamSlug={params.team_slug}
|
||||
userOpStats={userOpStats}
|
||||
/>
|
||||
</div>
|
||||
</ProjectPage>
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/wallets/sponsored-gas`,
|
||||
);
|
||||
}
|
||||
|
||||
const seo = {
|
||||
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
|
||||
title: "The Complete Account Abstraction Toolkit | thirdweb",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: seo.desc,
|
||||
openGraph: {
|
||||
description: seo.desc,
|
||||
images: [
|
||||
{
|
||||
alt: seo.title,
|
||||
height: 630,
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
|
||||
width: 1200,
|
||||
},
|
||||
],
|
||||
title: seo.title,
|
||||
},
|
||||
title: seo.title,
|
||||
};
|
||||
|
||||
+2
-2
@@ -371,7 +371,7 @@ export function AccountAbstractionSettingsPage(
|
||||
of the{" "}
|
||||
<UnderlineLink
|
||||
className="text-primary-500"
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet/infrastructure#supported-chains"
|
||||
href="https://thirdweb.com/chainlist?service=account-abstraction"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -552,7 +552,7 @@ export function AccountAbstractionSettingsPage(
|
||||
you build your own rules.{" "}
|
||||
<UnderlineLink
|
||||
className="text-primary-500"
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet/sponsorship-rules#setting-up-a-server-verifier"
|
||||
href="https://portal.thirdweb.com/transactions/policies#server-verifier"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
+2
-5
@@ -11,6 +11,7 @@ import {
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appMetadata } from "@/constants/connect";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
@@ -65,11 +66,7 @@ export const NebulaConnectWallet = (props: {
|
||||
const { customDetailsButton } = props;
|
||||
return (
|
||||
<ConnectButton
|
||||
appMetadata={{
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
name: "thirdweb",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
appMetadata={appMetadata}
|
||||
autoConnect={false}
|
||||
chains={allChainsV5}
|
||||
client={props.client}
|
||||
|
||||
+3
-3
@@ -28,11 +28,11 @@ export function QuickStartSection(props: {
|
||||
icon={ArrowRightLeftIcon}
|
||||
setupTime={5}
|
||||
id="swap_tokens"
|
||||
features={["Swap any token", "Cross-chain swaps"]}
|
||||
features={["Swap any token", "Cross-chain swap"]}
|
||||
description="Swap tokens cross-chain with dedicated swapping endpoints."
|
||||
link={{
|
||||
href: `https://portal.thirdweb.com/payments/swap`,
|
||||
label: "Setup Swaps",
|
||||
href: `https://portal.thirdweb.com/bridge/swap`,
|
||||
label: "Setup Swap",
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
+36
-20
@@ -2,6 +2,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -19,7 +22,6 @@ import {
|
||||
type RouteDiscoveryValidationSchema,
|
||||
routeDiscoveryValidationSchema,
|
||||
} from "@/schema/validations";
|
||||
import { RouteDiscoveryCard } from "./RouteDiscoveryCard";
|
||||
|
||||
export const RouteDiscovery = ({
|
||||
project,
|
||||
@@ -77,33 +79,21 @@ export const RouteDiscovery = ({
|
||||
},
|
||||
);
|
||||
|
||||
const errorText = form.getFieldState("tokenAddress").error?.message;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<RouteDiscoveryCard
|
||||
bottomText=""
|
||||
errorText={form.getFieldState("tokenAddress").error?.message}
|
||||
noPermissionText={undefined}
|
||||
saveButton={
|
||||
// Only show the submit button in the default state
|
||||
{
|
||||
disabled: !form.formState.isDirty,
|
||||
isPending: submitDiscoveryMutation.isPending,
|
||||
type: "submit",
|
||||
variant: "outline",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-semibold text-xl tracking-tight">
|
||||
<div className="relative rounded-lg border border-border bg-card">
|
||||
<div className="relative border-dashed border-b px-4 py-6 lg:px-6">
|
||||
<h3 className="font-semibold text-xl tracking-tight mb-1">
|
||||
Add a token to Bridge
|
||||
</h3>
|
||||
<p className="mt-1.5 mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
|
||||
<p className="mb-4 text-muted-foreground max-w-3xl text-sm text-pretty">
|
||||
Select your chain and input the token address to automatically
|
||||
kick-off the token route discovery process. <br /> This may take
|
||||
up to 20-40 minutes to complete.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 max-w-3xl">
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -142,7 +132,33 @@ export const RouteDiscovery = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RouteDiscoveryCard>
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-4 lg:px-6">
|
||||
{errorText ? (
|
||||
<p className="text-destructive-text text-sm">{errorText}</p>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="gap-1.5 rounded-full"
|
||||
disabled={
|
||||
!form.formState.isDirty || submitDiscoveryMutation.isPending
|
||||
}
|
||||
size="sm"
|
||||
type="submit"
|
||||
variant={"outline"}
|
||||
>
|
||||
{submitDiscoveryMutation.isPending ? (
|
||||
<Spinner className="size-3" />
|
||||
) : (
|
||||
<PlusIcon className="size-4 text-muted-foreground" />
|
||||
)}
|
||||
Add Token
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user