Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e87b07a8 | ||
|
|
f2d0f119c1 | ||
|
|
dc07fdd952 | ||
|
|
1d598ce907 | ||
|
|
f157e3484a | ||
|
|
d95b3d38c7 | ||
|
|
b50b320144 | ||
|
|
4c339363c0 | ||
|
|
125cc972de | ||
|
|
436750ed15 | ||
|
|
9cf5ba83e2 | ||
|
|
33666aa8c5 | ||
|
|
e526f75f22 | ||
|
|
f912e42342 | ||
|
|
fe46e81cd5 | ||
|
|
525e60393f | ||
|
|
4baefa5dae | ||
|
|
b2e56f89fe | ||
|
|
ec194a623e | ||
|
|
dc2e514e56 | ||
|
|
ec5362b84a | ||
|
|
683e3b0838 | ||
|
|
8b4b873317 | ||
|
|
67a7d3e3a5 | ||
|
|
755e5d40ca | ||
|
|
a2f561f97b | ||
|
|
72a4ac5a98 | ||
|
|
69ec6e0553 | ||
|
|
21cc01a0aa | ||
|
|
c03734aea9 | ||
|
|
8da0c1fdcd | ||
|
|
8dc27ffcc4 | ||
|
|
f85163edee | ||
|
|
7965d93b7a | ||
|
|
4ccf06260b | ||
|
|
d7dc83f9e6 | ||
|
|
9877176f65 | ||
|
|
67768ac375 | ||
|
|
8937d5511e | ||
|
|
bf81deeb35 | ||
|
|
25a7ad7eb9 | ||
|
|
ac8b09f35e | ||
|
|
f7f179c771 | ||
|
|
d0bf3f3f5e | ||
|
|
96689c6e2a | ||
|
|
0c5c918b21 | ||
|
|
e1763e216a | ||
|
|
309d8f81a9 | ||
|
|
9d72ddd604 | ||
|
|
17b83686e4 | ||
|
|
8ee427d590 | ||
|
|
24319f4570 | ||
|
|
f38b262cd0 | ||
|
|
5b35a6227b | ||
|
|
03e238515b | ||
|
|
72875f9de5 | ||
|
|
97ce74f307 | ||
|
|
3a43fb23a4 | ||
|
|
c7cb6ae7a4 | ||
|
|
7ecfcb95af | ||
|
|
4cf15a2475 | ||
|
|
62ce05ec35 | ||
|
|
563c4c7df1 | ||
|
|
32c91abc02 | ||
|
|
7a2dc25129 | ||
|
|
32568fd421 | ||
|
|
9091e813ab | ||
|
|
23d375778b | ||
|
|
e7b0110803 | ||
|
|
1825ac857e | ||
|
|
3ce2d0602e | ||
|
|
4bde2eb157 | ||
|
|
8d4d991e57 | ||
|
|
22090cc400 | ||
|
|
95a6c38703 | ||
|
|
b1060de7da | ||
|
|
0aca56317a | ||
|
|
5463baa7da | ||
|
|
64ffe199af | ||
|
|
76b522e7bc | ||
|
|
49786404b3 | ||
|
|
64bd603301 | ||
|
|
60a5b7c3e7 | ||
|
|
baf95b9077 | ||
|
|
e29f749342 | ||
|
|
2a7df337f7 | ||
|
|
348e3ab4ad | ||
|
|
e4590a7ada | ||
|
|
a45c558e06 | ||
|
|
a3e7300723 | ||
|
|
4c1f384a63 | ||
|
|
ec099a781a | ||
|
|
185d2f309c | ||
|
|
e8fb41752e | ||
|
|
3f42f522b7 | ||
|
|
bb29e1f705 | ||
|
|
76822982f3 | ||
|
|
52261a5740 | ||
|
|
cd30ba4f3a | ||
|
|
0ea898712b | ||
|
|
1b2caecfc4 | ||
|
|
e574114188 | ||
|
|
aa733b7b3e | ||
|
|
fa0d1141da | ||
|
|
f618e2bffb | ||
|
|
3ab31c8210 | ||
|
|
8eb5d8c01e | ||
|
|
d036b87ae1 | ||
|
|
c86cac4d61 | ||
|
|
af09576247 | ||
|
|
316cef1a58 | ||
|
|
1bfcd375e4 | ||
|
|
86070fa309 |
@@ -16,6 +16,7 @@ env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TW_SECRET_KEY: ${{ secrets.TW_SECRET_KEY }}
|
||||
TW_CLIENT_ID: ${{ secrets.TW_CLIENT_ID }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
jobs:
|
||||
@@ -88,7 +89,7 @@ jobs:
|
||||
- run: pnpm test
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
directory: packages/
|
||||
flags: packages
|
||||
@@ -102,7 +103,7 @@ jobs:
|
||||
runs-on: ubuntu-latest-8
|
||||
strategy:
|
||||
matrix:
|
||||
package_manager: [npm, yarn, pnpm, bun]
|
||||
package_manager: [pnpm] # TODO, reenable [npm, yarn, pnpm, bun]
|
||||
bundler: [vite, webpack, esbuild]
|
||||
steps:
|
||||
- name: Check out the code
|
||||
@@ -123,7 +124,14 @@ jobs:
|
||||
mkdir test-project
|
||||
cd test-project
|
||||
npm init -y
|
||||
${{ matrix.package_manager }} add react react-dom ../packages/thirdweb
|
||||
|
||||
# Handle different package managers
|
||||
if [ "${{ matrix.package_manager }}" = "pnpm" ]; then
|
||||
# Create pnpm workspace
|
||||
echo '{"name": "test-project", "private": true, "workspaces": ["."]}' > package.json
|
||||
echo '{"packages": ["../packages/*"]}' > pnpm-workspace.yaml
|
||||
pnpm add react react-dom ../packages/thirdweb -w
|
||||
fi
|
||||
- name: Create test file
|
||||
run: |
|
||||
cd test-project
|
||||
@@ -133,7 +141,7 @@ jobs:
|
||||
if: matrix.bundler == 'vite'
|
||||
run: |
|
||||
cd test-project
|
||||
${{matrix.package_manager}} add vite
|
||||
${{matrix.package_manager}} add vite -w
|
||||
echo 'import { defineConfig } from "vite"; import {resolve} from "path"; export default defineConfig({ build: { lib: { entry: resolve(__dirname, "index.js"), name: "e2e_test" }, outDir: "dist" }});' > vite.config.js
|
||||
npx vite build
|
||||
|
||||
@@ -141,7 +149,7 @@ jobs:
|
||||
if: matrix.bundler == 'webpack'
|
||||
run: |
|
||||
cd test-project
|
||||
${{matrix.package_manager}} add webpack webpack-cli
|
||||
${{matrix.package_manager}} add webpack webpack-cli -w
|
||||
echo 'const path = require("path"); module.exports = { mode: "production", entry: "./index.js", output: { path: path.resolve(__dirname, "dist"), filename: "bundle.js" }};' > webpack.config.js
|
||||
npx webpack
|
||||
|
||||
@@ -149,7 +157,7 @@ jobs:
|
||||
if: matrix.bundler == 'esbuild'
|
||||
run: |
|
||||
cd test-project
|
||||
${{matrix.package_manager}} add esbuild
|
||||
${{matrix.package_manager}} add esbuild -w
|
||||
npx esbuild index.js --bundle --outdir=dist
|
||||
|
||||
- name: Verify bundle
|
||||
@@ -177,6 +185,9 @@ jobs:
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Build Packages
|
||||
run: pnpm build
|
||||
|
||||
- name: Report bundle size
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
public-hoist-pattern[]=*import-in-the-middle*
|
||||
public-hoist-pattern[]=*require-in-the-middle*
|
||||
public-hoist-pattern[]=*require-in-the-middle*
|
||||
public-hoist-pattern[]=*pino-pretty*
|
||||
|
||||
@@ -58,4 +58,7 @@ module.exports = [
|
||||
"/faucets",
|
||||
// -- brand kit --
|
||||
"/brand-kit",
|
||||
// -- universal bridge landing pages --
|
||||
"/universal-bridge-regions/:region_slug",
|
||||
"/enterprise",
|
||||
];
|
||||
|
||||
@@ -12,7 +12,7 @@ const ContentSecurityPolicy = `
|
||||
style-src 'self' 'unsafe-inline' vercel.live;
|
||||
font-src 'self' vercel.live assets.vercel.com framerusercontent.com fonts.gstatic.com;
|
||||
frame-src * data:;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com static.reo.dev;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com challenges.cloudflare.com;
|
||||
connect-src * data: blob:;
|
||||
worker-src 'self' blob:;
|
||||
block-all-mixed-content;
|
||||
@@ -111,6 +111,9 @@ const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
automaticVercelMonitors: false,
|
||||
};
|
||||
|
||||
// add additional languages to the framer rewrite paths here (english is already included by default)
|
||||
const FRAMER_ADDITIONAL_LANGUAGES = ["es"];
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
@@ -155,10 +158,18 @@ const baseNextConfig: NextConfig = {
|
||||
source: "/home",
|
||||
destination: "https://landing.thirdweb.com",
|
||||
},
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
source: path,
|
||||
destination: `https://landing.thirdweb.com${path}`,
|
||||
})),
|
||||
// flatmap the framer paths for the default language and the additional languages
|
||||
...FRAMER_PATHS.flatMap((path) => [
|
||||
{
|
||||
source: path,
|
||||
destination: `https://landing.thirdweb.com${path}`,
|
||||
},
|
||||
// this is for additional languages
|
||||
...FRAMER_ADDITIONAL_LANGUAGES.map((lang) => ({
|
||||
source: `/${lang}${path}`,
|
||||
destination: `https://landing.thirdweb.com/${lang}${path}`,
|
||||
})),
|
||||
]),
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
||||
+32
-32
@@ -29,26 +29,26 @@
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-dialog": "1.1.7",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||
"@radix-ui/react-hover-card": "^1.1.7",
|
||||
"@radix-ui/react-label": "^2.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.3",
|
||||
"@radix-ui/react-radio-group": "^1.2.4",
|
||||
"@radix-ui/react-select": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
"@radix-ui/react-accordion": "^1.2.7",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.10",
|
||||
"@radix-ui/react-avatar": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.2.2",
|
||||
"@radix-ui/react-dialog": "1.1.10",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.11",
|
||||
"@radix-ui/react-hover-card": "^1.1.10",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-popover": "^1.1.10",
|
||||
"@radix-ui/react-progress": "^1.1.4",
|
||||
"@radix-ui/react-radio-group": "^1.3.3",
|
||||
"@radix-ui/react-select": "^2.2.2",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "1.2.0",
|
||||
"@sentry/nextjs": "9.12.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "1.2.3",
|
||||
"@sentry/nextjs": "9.13.0",
|
||||
"@shazow/whatsabi": "0.21.0",
|
||||
"@tanstack/react-query": "5.72.1",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@tanstack/react-query": "5.74.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "2.0.0",
|
||||
"@vercel/og": "^0.6.8",
|
||||
@@ -66,14 +66,14 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.6.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.487.0",
|
||||
"next": "15.2.4",
|
||||
"lucide-react": "0.503.0",
|
||||
"next": "15.3.1",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.1",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
@@ -89,7 +89,7 @@
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.2",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
@@ -97,20 +97,20 @@
|
||||
"sonner": "2.0.3",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"stripe": "17.7.0",
|
||||
"swagger-ui-react": "^5.20.7",
|
||||
"swagger-ui-react": "^5.21.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.4",
|
||||
"zod": "3.24.2"
|
||||
"zod": "3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.6",
|
||||
"@next/bundle-analyzer": "15.2.4",
|
||||
"@next/eslint-plugin-next": "15.2.4",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@next/bundle-analyzer": "15.3.1",
|
||||
"@next/eslint-plugin-next": "15.3.1",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@storybook/addon-essentials": "8.6.12",
|
||||
"@storybook/addon-interactions": "8.6.12",
|
||||
"@storybook/addon-links": "8.6.12",
|
||||
@@ -121,12 +121,12 @@
|
||||
"@storybook/react": "8.6.12",
|
||||
"@storybook/test": "8.6.12",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/node": "22.14.1",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
@@ -138,7 +138,7 @@
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"knip": "5.47.0",
|
||||
"knip": "5.50.5",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.3",
|
||||
"storybook": "8.6.12",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function acceptInvite(options: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
export type GetBillingCheckoutUrlOptions = {
|
||||
@@ -64,6 +64,83 @@ export async function getBillingCheckoutUrl(
|
||||
|
||||
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl;
|
||||
|
||||
export async function getPlanCancelUrl(options: {
|
||||
teamId: string;
|
||||
redirectUrl: string;
|
||||
}): Promise<{ status: number; url?: string }> {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return {
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/cancel-plan-link`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
redirectTo: options.redirectUrl,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.result) {
|
||||
return {
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
url: json.result as string,
|
||||
};
|
||||
}
|
||||
|
||||
export async function reSubscribePlan(options: {
|
||||
teamId: string;
|
||||
}): Promise<{ status: number }> {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return {
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamId}/checkout/resubscribe-plan`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
};
|
||||
}
|
||||
export type GetBillingPortalUrlOptions = {
|
||||
teamSlug: string | undefined;
|
||||
redirectUrl: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function confirmEmailWithOTP(otp: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getRawAccount } from "../../app/account/settings/getAccount";
|
||||
import { getRawAccount } from "../../app/(app)/account/settings/getAccount";
|
||||
|
||||
export async function getRawAccountAction() {
|
||||
return getRawAccount();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import { getUserThirdwebClient } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
type BalanceQueryResponse = Array<{
|
||||
balance: string;
|
||||
@@ -24,6 +23,7 @@ export async function getTokenBalancesFromMoralis(params: {
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const client = await getUserThirdwebClient();
|
||||
const { contractAddress, chainId } = params;
|
||||
|
||||
if (!isAddress(contractAddress)) {
|
||||
@@ -39,7 +39,7 @@ export async function getTokenBalancesFromMoralis(params: {
|
||||
const balance = await getWalletBalance({
|
||||
address: contractAddress,
|
||||
chain,
|
||||
client: getThirdwebClient(),
|
||||
client,
|
||||
});
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
import {
|
||||
generateAlchemyUrl,
|
||||
isAlchemySupported,
|
||||
transformAlchemyResponseToNFT,
|
||||
} from "lib/wallet/nfts/alchemy";
|
||||
import {
|
||||
generateMoralisUrl,
|
||||
isMoralisSupported,
|
||||
transformMoralisResponseToNFT,
|
||||
} from "lib/wallet/nfts/moralis";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
import { isAlchemySupported } from "../../lib/wallet/nfts/isAlchemySupported";
|
||||
import { isMoralisSupported } from "../../lib/wallet/nfts/isMoralisSupported";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/env";
|
||||
|
||||
type WalletNFTApiReturn =
|
||||
@@ -81,6 +81,7 @@ export async function getWalletNFTs(params: {
|
||||
const result = await transformMoralisResponseToNFT(
|
||||
await parsedResponse,
|
||||
owner,
|
||||
chainId,
|
||||
);
|
||||
|
||||
return { result };
|
||||
@@ -194,6 +195,8 @@ async function getWalletNFTsFromInsight(params: {
|
||||
tokenURI: nft.metadata_url,
|
||||
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
|
||||
supply: nft.balance,
|
||||
tokenAddress: nft.contract.address,
|
||||
chainId: nft.contract.chain_id,
|
||||
};
|
||||
|
||||
return walletNFT;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function joinTeamWaitlist(options: {
|
||||
teamSlug: string;
|
||||
// currently only 'nebula' is supported
|
||||
scope: "nebula";
|
||||
}) {
|
||||
const { teamSlug, scope } = options;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
errorMessage: "You are not authorized to perform this action",
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
errorMessage: "Failed to join waitlist",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
type ProxyActionParams = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function sendTeamInvites(options: {
|
||||
|
||||
@@ -23,7 +23,7 @@ function getStripe() {
|
||||
|
||||
export async function getTeamInvoices(
|
||||
team: Team,
|
||||
options?: { cursor?: string },
|
||||
options?: { cursor?: string; status?: "open" },
|
||||
) {
|
||||
try {
|
||||
const customerId = team.stripeCustomerId;
|
||||
@@ -37,6 +37,8 @@ export async function getTeamInvoices(
|
||||
customer: customerId,
|
||||
limit: 10,
|
||||
starting_after: options?.cursor,
|
||||
// Only return open invoices if the status is open
|
||||
status: options?.status,
|
||||
});
|
||||
|
||||
return invoices;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use server";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function updateAccount(values: {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAddress } from "thirdweb";
|
||||
import { COOKIE_PREFIX_TOKEN } from "../constants/cookie";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
/**
|
||||
* Check that the connected wallet is valid for the current active account
|
||||
*/
|
||||
export async function isWalletValidForActiveAccount(params: {
|
||||
address: string;
|
||||
account: Account;
|
||||
authToken: string;
|
||||
}) {
|
||||
const cookieStore = await cookies();
|
||||
const authCookieNameForAddress =
|
||||
COOKIE_PREFIX_TOKEN + getAddress(params.address);
|
||||
|
||||
// authToken for this wallet address should be present
|
||||
const authTokenForAddress = cookieStore.get(authCookieNameForAddress)?.value;
|
||||
if (!authTokenForAddress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// this authToken should be same as current active authToken
|
||||
if (authTokenForAddress !== params.authToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// authToken should be valid
|
||||
const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${authTokenForAddress}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (accountRes.status !== 200) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const account = (await accountRes.json()).data as Account;
|
||||
|
||||
// the current account should match the account fetched for the authToken
|
||||
return account.id === params.account.id;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
WalletStats,
|
||||
WalletUserStats,
|
||||
} from "types/analytics";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { getChains } from "./chain";
|
||||
|
||||
async function fetchAnalytics(
|
||||
@@ -346,7 +346,7 @@ export async function getEcosystemWalletUsage(args: {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetchAnalytics(
|
||||
`v2/wallets/connects/${ecosystemSlug}?${searchParams.toString()}`,
|
||||
`v2/wallet/connects?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -356,7 +356,10 @@ export async function getEcosystemWalletUsage(args: {
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch ecosystem wallet stats");
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch ecosystem wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import type { ChainService } from "../../app/(dashboard)/(chain)/types/chain";
|
||||
import type { ChainService } from "../../app/(app)/(dashboard)/(chain)/types/chain";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
|
||||
export async function getGasSponsoredChains() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export type LinkedWallet = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
export type Project = ProjectResponse;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export type TeamInvite = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
const TeamAccountRole = {
|
||||
OWNER: "OWNER",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
export type Team = TeamResponse & { stripeCustomerId: string | null };
|
||||
|
||||
@@ -74,31 +74,3 @@ export async function getDefaultTeam() {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type TeamNebulaWaitList = {
|
||||
onWaitlist: boolean;
|
||||
createdAt: null | string;
|
||||
};
|
||||
|
||||
export async function getTeamNebulaWaitList(teamSlug: string) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
return (await res.json()).result as TeamNebulaWaitList;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"use server";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
|
||||
const UB_BASE_URL = process.env.NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST;
|
||||
|
||||
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;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<Webhook>;
|
||||
}
|
||||
|
||||
export async function createWebhook(props: {
|
||||
clientId: string;
|
||||
version?: number;
|
||||
url: string;
|
||||
label: string;
|
||||
secret?: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
url: props.url,
|
||||
label: props.label,
|
||||
version: props.version,
|
||||
secret: props.secret,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export async function deleteWebhook(props: {
|
||||
clientId: string;
|
||||
webhookId: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export type Fee = {
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export async function getFees(props: {
|
||||
clientId: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Fee;
|
||||
}
|
||||
|
||||
export async function updateFee(props: {
|
||||
clientId: string;
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
feeRecipient: props.feeRecipient,
|
||||
feeBps: props.feeBps,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
export type RPCUsageDataItem = {
|
||||
date: string;
|
||||
usageType: "included" | "overage" | "rate-limit";
|
||||
count: string;
|
||||
};
|
||||
|
||||
export const fetchRPCUsage = unstable_cache(
|
||||
async (params: {
|
||||
teamId: string;
|
||||
projectId?: string;
|
||||
authToken: string;
|
||||
from: string;
|
||||
to: string;
|
||||
period: "day" | "week" | "month" | "year" | "all";
|
||||
}) => {
|
||||
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
|
||||
const url = new URL(`${analyticsEndpoint}/v2/rpc/usage-types`);
|
||||
url.searchParams.set("teamId", params.teamId);
|
||||
if (params.projectId) {
|
||||
url.searchParams.set("projectId", params.projectId);
|
||||
}
|
||||
url.searchParams.set("from", params.from);
|
||||
url.searchParams.set("to", params.to);
|
||||
url.searchParams.set("period", params.period);
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
return {
|
||||
ok: false as const,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
|
||||
const resData = await res.json();
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
data: resData.data as RPCUsageDataItem[],
|
||||
};
|
||||
},
|
||||
["rpc-usage"],
|
||||
{
|
||||
revalidate: 60 * 60, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
type Last24HoursRPCUsageApiResponse = {
|
||||
peakRate: {
|
||||
date: string;
|
||||
peakRPS: string;
|
||||
};
|
||||
averageRate: {
|
||||
date: string;
|
||||
averageRate: string;
|
||||
peakRPS: string;
|
||||
includedCount: string;
|
||||
rateLimitedCount: string;
|
||||
overageCount: string;
|
||||
}[];
|
||||
totalCounts: {
|
||||
includedCount: string;
|
||||
rateLimitedCount: string;
|
||||
overageCount: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const getLast24HoursRPCUsage = unstable_cache(
|
||||
async (params: {
|
||||
teamId: string;
|
||||
projectId?: string;
|
||||
authToken: string;
|
||||
}) => {
|
||||
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
|
||||
const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`);
|
||||
url.searchParams.set("teamId", params.teamId);
|
||||
if (params.projectId) {
|
||||
url.searchParams.set("projectId", params.projectId);
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.authToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
return {
|
||||
ok: false as const,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
|
||||
const resData = await res.json();
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
data: resData.data as Last24HoursRPCUsageApiResponse,
|
||||
};
|
||||
},
|
||||
["rpc-usage-last-24-hours:v2"],
|
||||
{
|
||||
revalidate: 60, // 1 minute
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
GetBillingCheckoutUrlAction,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
GetBillingPortalUrlAction,
|
||||
GetBillingPortalUrlOptions,
|
||||
} from "../actions/billing";
|
||||
import type { Team } from "../api/team";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Spinner } from "./ui/Spinner/Spinner";
|
||||
import { Button, type ButtonProps } from "./ui/button";
|
||||
@@ -16,6 +19,7 @@ type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> & {
|
||||
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
|
||||
buttonProps?: Omit<ButtonProps, "children">;
|
||||
children: React.ReactNode;
|
||||
billingStatus: Team["billingStatus"];
|
||||
};
|
||||
|
||||
export function CheckoutButton({
|
||||
@@ -25,6 +29,7 @@ export function CheckoutButton({
|
||||
getBillingCheckoutUrl,
|
||||
children,
|
||||
buttonProps,
|
||||
billingStatus,
|
||||
}: CheckoutButtonProps) {
|
||||
const getUrlMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
@@ -40,35 +45,65 @@ export function CheckoutButton({
|
||||
const errorMessage = "Failed to open checkout page";
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
className={cn(buttonProps?.className, "gap-2")}
|
||||
disabled={getUrlMutation.isPending || buttonProps?.disabled}
|
||||
onClick={async (e) => {
|
||||
buttonProps?.onClick?.(e);
|
||||
getUrlMutation.mutate(undefined, {
|
||||
onSuccess: (res) => {
|
||||
if (!res.url) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
{/* show warning if the team has an invalid payment method */}
|
||||
{billingStatus === "invalidPayment" && (
|
||||
<BillingWarning teamSlug={teamSlug} />
|
||||
)}
|
||||
<Button
|
||||
{...buttonProps}
|
||||
className={cn(buttonProps?.className, "w-full gap-2")}
|
||||
disabled={
|
||||
// disable button if the team has an invalid payment method
|
||||
// api will return 402 error if the team has an invalid payment method
|
||||
billingStatus === "invalidPayment" ||
|
||||
getUrlMutation.isPending ||
|
||||
buttonProps?.disabled
|
||||
}
|
||||
onClick={async (e) => {
|
||||
buttonProps?.onClick?.(e);
|
||||
getUrlMutation.mutate(undefined, {
|
||||
onSuccess: (res) => {
|
||||
if (!res.url) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = window.open(res.url, "_blank");
|
||||
const tab = window.open(res.url, "_blank");
|
||||
|
||||
if (!tab) {
|
||||
if (!tab) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(errorMessage);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{getUrlMutation.isPending && <Spinner className="size-4" />}
|
||||
{children}
|
||||
</Button>
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{getUrlMutation.isPending && <Spinner className="size-4" />}
|
||||
{children}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BillingWarning({ teamSlug }: { teamSlug: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<AlertTriangleIcon className="h-5 w-5 flex-shrink-0" />
|
||||
<p className="text-sm">
|
||||
You have outstanding invoices. Please{" "}
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
|
||||
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
|
||||
>
|
||||
pay them
|
||||
</Link>{" "}
|
||||
to continue.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { getThirdwebClient } from "../../../constants/thirdweb.server";
|
||||
import {
|
||||
BadgeContainer,
|
||||
storybookThirdwebClient,
|
||||
} from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { GradientAvatar } from "./GradientAvatar";
|
||||
|
||||
@@ -17,8 +19,6 @@ export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
@@ -29,7 +29,7 @@ function Story() {
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -38,7 +38,7 @@ function Story() {
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -47,7 +47,7 @@ function Story() {
|
||||
id={"foo"}
|
||||
src={""}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -56,7 +56,7 @@ function Story() {
|
||||
id={"bar"}
|
||||
src={""}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -65,7 +65,7 @@ function Story() {
|
||||
src="invalid-src"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -74,7 +74,7 @@ function Story() {
|
||||
src="https://picsum.photos/200/300"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -83,7 +83,7 @@ function Story() {
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -123,7 +123,7 @@ function ToggleTest() {
|
||||
src={data?.src}
|
||||
id={data?.id}
|
||||
className="size-20"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -132,7 +132,7 @@ function ToggleTest() {
|
||||
className="size-20"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { getThirdwebClient } from "../../../constants/thirdweb.server";
|
||||
import {
|
||||
BadgeContainer,
|
||||
storybookThirdwebClient,
|
||||
} from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProjectAvatar } from "./ProjectAvatar";
|
||||
|
||||
@@ -18,19 +20,25 @@ export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
<ProjectAvatar src={undefined} className="size-6" client={client} />
|
||||
<ProjectAvatar
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
|
||||
<ProjectAvatar src={""} className="size-6" client={client} />
|
||||
<ProjectAvatar
|
||||
src={""}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
@@ -65,14 +73,18 @@ function ToggleTest() {
|
||||
<p> Src+Name is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<ProjectAvatar src={data?.src} className="size-6" client={client} />
|
||||
<ProjectAvatar
|
||||
src={data?.src}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<ProjectAvatar
|
||||
src={data ? "invalid-src" : undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import {
|
||||
BadgeContainer,
|
||||
storybookThirdwebClient,
|
||||
} from "../../../stories/utils";
|
||||
import { MultiNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -41,6 +44,7 @@ function Variant(props: {
|
||||
<BadgeContainer label={props.label}>
|
||||
<MultiNetworkSelector
|
||||
selectedChainIds={chainIds}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={setChainIds}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { MultiSelect } from "@/components/blocks/multi-select";
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { ChainIcon } from "../../../components/icons/ChainIcon";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { ChainIconClient } from "../../../components/icons/ChainIcon";
|
||||
import { useAllChainsData } from "../../../hooks/chains/allChains";
|
||||
|
||||
function cleanChainName(chainName: string) {
|
||||
@@ -19,6 +20,13 @@ export function MultiNetworkSelector(props: {
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
priorityChains?: number[];
|
||||
hideTestnets?: boolean;
|
||||
popoverContentClassName?: string;
|
||||
customTrigger?: React.ReactNode;
|
||||
align?: "center" | "start" | "end";
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
showSelectedValuesInModal?: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -44,13 +52,17 @@ export function MultiNetworkSelector(props: {
|
||||
sortedChains = [...priorityChains, ...otherChains];
|
||||
}
|
||||
|
||||
if (props.hideTestnets) {
|
||||
sortedChains = sortedChains.filter((chain) => !chain.testnet);
|
||||
}
|
||||
|
||||
return sortedChains.map((chain) => {
|
||||
return {
|
||||
label: cleanChainName(chain.name),
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [allChains, props.priorityChains, idToChain]);
|
||||
}, [allChains, props.priorityChains, idToChain, props.hideTestnets]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
@@ -77,10 +89,11 @@ export function MultiNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
<ChainIconClient
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
loading="lazy"
|
||||
src={chain.icon?.url}
|
||||
client={props.client}
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
@@ -94,14 +107,19 @@ export function MultiNetworkSelector(props: {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain, props.disableChainId],
|
||||
[idToChain, props.disableChainId, props.client],
|
||||
);
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
searchPlaceholder="Search by Name or Chain Id"
|
||||
selectedValues={props.selectedChainIds.map(String)}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
customTrigger={props.customTrigger}
|
||||
options={options}
|
||||
align={props.align}
|
||||
side={props.side}
|
||||
showSelectedValuesInModal={props.showSelectedValuesInModal}
|
||||
onSelectedValuesChange={(chainIds) => {
|
||||
props.onChange(chainIds.map(Number));
|
||||
}}
|
||||
@@ -127,6 +145,7 @@ export function SingleNetworkSelector(props: {
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -172,9 +191,10 @@ export function SingleNetworkSelector(props: {
|
||||
return (
|
||||
<div className="flex justify-between gap-4">
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIcon
|
||||
<ChainIconClient
|
||||
className="size-5"
|
||||
ipfsSrc={chain.icon?.url}
|
||||
src={chain.icon?.url}
|
||||
client={props.client}
|
||||
loading="lazy"
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
@@ -189,7 +209,7 @@ export function SingleNetworkSelector(props: {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[idToChain, props.disableChainId],
|
||||
[idToChain, props.disableChainId, props.client],
|
||||
);
|
||||
|
||||
const isLoadingChains = allChains.length === 0;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import {
|
||||
BadgeContainer,
|
||||
storybookThirdwebClient,
|
||||
} from "../../../stories/utils";
|
||||
import { SingleNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -43,6 +46,7 @@ function Variant(props: {
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<SingleNetworkSelector
|
||||
client={storybookThirdwebClient}
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
chainIds={props.chainIds}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
|
||||
import { ThirdwebMiniLogo } from "app/(app)/components/ThirdwebMiniLogo";
|
||||
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
|
||||
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
|
||||
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
|
||||
@@ -15,6 +15,37 @@ type AppFooterProps = {
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
const footerLinks = [
|
||||
{
|
||||
href: "/home",
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
href: "https://blog.thirdweb.com",
|
||||
label: "Blog",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/changelog",
|
||||
label: "Changelog",
|
||||
},
|
||||
{
|
||||
href: "https://feedback.thirdweb.com/",
|
||||
label: "Feedback",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb.com/privacy-policy",
|
||||
label: "Privacy Policy",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb.com/terms",
|
||||
label: "Terms of Service",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb.com/chainlist",
|
||||
label: "Chainlist",
|
||||
},
|
||||
];
|
||||
|
||||
export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
@@ -80,57 +111,28 @@ export function AppFooter(props: AppFooterProps) {
|
||||
</div>
|
||||
{/* bottom row */}
|
||||
<div className="grid grid-flow-col grid-cols-2 grid-rows-5 gap-2 md:flex md:flex-row md:justify-between">
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="/home"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://blog.thirdweb.com"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://portal.thirdweb.com/changelog"
|
||||
target="_blank"
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://feedback.thirdweb.com/"
|
||||
target="_blank"
|
||||
>
|
||||
Feedback
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/privacy-policy"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/terms"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/chainlist"
|
||||
target="_blank"
|
||||
>
|
||||
Chain List
|
||||
</Link>
|
||||
{footerLinks.map((link) => (
|
||||
<FooterLink key={link.href} {...link} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterLink(props: {
|
||||
href: string;
|
||||
label: string;
|
||||
prefetch?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
prefetch={false}
|
||||
className="px-0 py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
{props.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "@/components/ui/chart";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
@@ -36,6 +36,13 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
showLegend?: boolean;
|
||||
|
||||
yAxis?: boolean;
|
||||
xAxis?: {
|
||||
sameDay?: boolean;
|
||||
};
|
||||
|
||||
variant?: "stacked" | "individual";
|
||||
|
||||
// chart className
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
@@ -72,15 +79,21 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
) : (
|
||||
<AreaChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
{props.yAxis && <YAxis tickLine={false} axisLine={false} />}
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
|
||||
tickFormatter={(value) =>
|
||||
formatDate(
|
||||
new Date(value),
|
||||
props.xAxis?.sameDay ? "MMM dd, HH:mm" : "MMM dd",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={
|
||||
@@ -113,17 +126,29 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
{configKeys.map((key) =>
|
||||
key === "maxLine" ? (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey="maxLine"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
fill="none"
|
||||
/>
|
||||
) : (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId={props.variant !== "stacked" ? undefined : "a"}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -47,7 +47,12 @@ interface MultiSelectProps
|
||||
searchTerm: string,
|
||||
) => boolean;
|
||||
|
||||
popoverContentClassName?: string;
|
||||
customTrigger?: React.ReactNode;
|
||||
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
|
||||
align?: "center" | "start" | "end";
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
showSelectedValuesInModal?: boolean;
|
||||
}
|
||||
|
||||
export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
@@ -62,6 +67,8 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
overrideSearchFn,
|
||||
renderOption,
|
||||
searchPlaceholder,
|
||||
popoverContentClassName,
|
||||
showSelectedValuesInModal = false,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -155,111 +162,100 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedValues.length > 0 ? (
|
||||
<div className="flex w-full justify-between">
|
||||
{/* badges */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{selectedValues.slice(0, maxCount).map((value) => {
|
||||
const option = options.find((o) => o.value === value);
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClosableBadge
|
||||
key={value}
|
||||
label={option.label}
|
||||
onClose={() => toggleOption(value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* +X more */}
|
||||
{selectedValues.length > maxCount && (
|
||||
<ClosableBadge
|
||||
label={`+ ${selectedValues.length - maxCount} more`}
|
||||
onClose={clearExtraOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Clear All */}
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
className="rounded p-1 hover:bg-accent"
|
||||
>
|
||||
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex h-full min-h-6"
|
||||
{props.customTrigger || (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedValues.length > 0 ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<SelectedChainsBadges
|
||||
selectedValues={selectedValues}
|
||||
options={options}
|
||||
maxCount={maxCount}
|
||||
onClose={handleClear}
|
||||
toggleOption={toggleOption}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Clear All */}
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
className="rounded p-1 hover:bg-accent"
|
||||
>
|
||||
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex h-full min-h-6"
|
||||
/>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="center"
|
||||
className={cn(
|
||||
"flex max-h-[60vh] flex-col p-0",
|
||||
popoverContentClassName,
|
||||
)}
|
||||
align={props.align}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
maxHeight: "var(--radix-popover-content-available-height)",
|
||||
height:
|
||||
"calc(var(--radix-popover-content-available-height) - 40px)",
|
||||
}}
|
||||
ref={popoverElRef}
|
||||
>
|
||||
<div>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
{optionsToShow.length === 0 && (
|
||||
<div className="flex flex-1 flex-col items-center justify-center py-10 text-sm">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{optionsToShow.length > 0 && (
|
||||
<ScrollShadow
|
||||
scrollableClassName="max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
|
||||
className="rounded"
|
||||
scrollableClassName="p-1 h-full"
|
||||
className="flex-1 rounded"
|
||||
>
|
||||
{/* List */}
|
||||
<div>
|
||||
{optionsToShow.length === 0 && (
|
||||
<div className="flex justify-center py-10">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{optionsToShow.map((option, i) => {
|
||||
const isSelected = selectedValues.includes(option.value);
|
||||
return (
|
||||
@@ -293,7 +289,20 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
})}
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSelectedValuesInModal && selectedValues.length > 0 && (
|
||||
<div className="border-t px-3 py-3">
|
||||
<SelectedChainsBadges
|
||||
selectedValues={selectedValues}
|
||||
options={options}
|
||||
maxCount={maxCount}
|
||||
onClose={handleClear}
|
||||
toggleOption={toggleOption}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
@@ -319,3 +328,44 @@ function ClosableBadge(props: {
|
||||
}
|
||||
|
||||
MultiSelect.displayName = "MultiSelect";
|
||||
|
||||
function SelectedChainsBadges(props: {
|
||||
selectedValues: string[];
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
maxCount: number;
|
||||
onClose: () => void;
|
||||
toggleOption: (value: string) => void;
|
||||
clearExtraOptions: () => void;
|
||||
}) {
|
||||
const { selectedValues, options, maxCount, toggleOption, clearExtraOptions } =
|
||||
props;
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{selectedValues.slice(0, maxCount).map((value) => {
|
||||
const option = options.find((o) => o.value === value);
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClosableBadge
|
||||
key={value}
|
||||
label={option.label}
|
||||
onClose={() => toggleOption(value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* +X more */}
|
||||
{selectedValues.length > maxCount && (
|
||||
<ClosableBadge
|
||||
label={`+ ${selectedValues.length - maxCount} more`}
|
||||
onClose={clearExtraOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { TEAM_PLANS } from "utils/pricing";
|
||||
import { RenewSubscriptionButton } from "../../../components/settings/Account/Billing/renew-subscription/renew-subscription-button";
|
||||
import { useTrack } from "../../../hooks/analytics/useTrack";
|
||||
import { remainingDays } from "../../../utils/date-utils";
|
||||
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
|
||||
@@ -16,20 +17,28 @@ import { CheckoutButton } from "../billing";
|
||||
|
||||
type PricingCardCta = {
|
||||
hint?: string;
|
||||
title: string;
|
||||
|
||||
onClick?: () => void;
|
||||
} & (
|
||||
| {
|
||||
type: "link";
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
| {
|
||||
type: "checkout";
|
||||
label: string;
|
||||
}
|
||||
| {
|
||||
type: "renew";
|
||||
}
|
||||
);
|
||||
|
||||
type PricingCardProps = {
|
||||
getTeam: () => Promise<Team>;
|
||||
teamSlug: string;
|
||||
teamId: string;
|
||||
billingStatus: Team["billingStatus"];
|
||||
billingPlan: keyof typeof TEAM_PLANS;
|
||||
cta?: PricingCardCta;
|
||||
ctaHint?: string;
|
||||
@@ -40,7 +49,10 @@ type PricingCardProps = {
|
||||
};
|
||||
|
||||
export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
getTeam,
|
||||
teamSlug,
|
||||
teamId,
|
||||
billingStatus,
|
||||
billingPlan,
|
||||
cta,
|
||||
highlighted = false,
|
||||
@@ -129,8 +141,12 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
|
||||
{cta && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{cta.type === "renew" && (
|
||||
<RenewSubscriptionButton teamId={teamId} getTeam={getTeam} />
|
||||
)}
|
||||
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
|
||||
<CheckoutButton
|
||||
billingStatus={billingStatus}
|
||||
buttonProps={{
|
||||
variant: highlighted ? "default" : "outline",
|
||||
className: highlighted ? undefined : "bg-background",
|
||||
@@ -140,7 +156,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
sku={billingPlanToSkuMap[billingPlan]}
|
||||
getBillingCheckoutUrl={getBillingCheckoutUrl}
|
||||
>
|
||||
{cta.title}
|
||||
{cta.label}
|
||||
</CheckoutButton>
|
||||
)}
|
||||
|
||||
@@ -151,7 +167,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
asChild
|
||||
>
|
||||
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
|
||||
{cta.title}
|
||||
{cta.label}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ export function WalletAddress(props: {
|
||||
address: string | undefined;
|
||||
shortenAddress?: boolean;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
// default back to zero address if no address provided
|
||||
@@ -78,6 +79,7 @@ export function WalletAddress(props: {
|
||||
address={address}
|
||||
profiles={profiles.data || []}
|
||||
thirdwebClient={thirdwebClient}
|
||||
iconClassName={props.iconClassName}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
@@ -168,6 +170,7 @@ function WalletAvatar(props: {
|
||||
address: string;
|
||||
profiles: SocialProfile[];
|
||||
thirdwebClient: ThirdwebClient;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
const avatar = useMemo(() => {
|
||||
return props.profiles.find(
|
||||
@@ -186,11 +189,20 @@ function WalletAvatar(props: {
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="size-6 overflow-hidden rounded-full">
|
||||
<div
|
||||
className={cn("size-6 overflow-hidden rounded-full", props.iconClassName)}
|
||||
>
|
||||
{resolvedAvatarSrc ? (
|
||||
<Img src={resolvedAvatarSrc} className="size-6 object-cover" />
|
||||
<Img
|
||||
src={resolvedAvatarSrc}
|
||||
className={cn("size-6 object-cover", props.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
<Blobbie address={props.address} size={24} />
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
size={24}
|
||||
className={props.iconClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { PaginationButtons } from "./pagination-buttons";
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -24,11 +24,7 @@ const TableHeader = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-border border-b bg-background",
|
||||
"sticky top-0 z-20 border-none before:absolute before:inset-0 before:border-border before:border-b",
|
||||
className,
|
||||
)}
|
||||
className={cn("relative border-b bg-background", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -4,6 +4,11 @@ export const DASHBOARD_THIRDWEB_CLIENT_ID =
|
||||
export const DASHBOARD_THIRDWEB_SECRET_KEY =
|
||||
process.env.DASHBOARD_SECRET_KEY || "";
|
||||
|
||||
export const NEXT_PUBLIC_NEBULA_APP_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_NEBULA_APP_CLIENT_ID || "";
|
||||
|
||||
export const NEBULA_APP_SECRET_KEY = process.env.NEBULA_APP_SECRET_KEY || "";
|
||||
|
||||
export const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || "";
|
||||
|
||||
export const IPFS_GATEWAY_URL =
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import type { GetAuthTokenResponse } from "../../app/api/auth/get-auth-token/route";
|
||||
import type { GetAuthTokenResponse } from "../../app/(app)/api/auth/get-auth-token/route";
|
||||
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
|
||||
import { getCookie } from "../../lib/cookie";
|
||||
import { getThirdwebClient } from "./thirdweb.server";
|
||||
|
||||
// returns a thirdweb client with optional JWT passed i
|
||||
|
||||
export function useThirdwebClient(jwt?: string) {
|
||||
const account = useActiveAccount();
|
||||
const lastUsedTeamId = getCookie(LAST_USED_TEAM_ID);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["jwt", account?.address],
|
||||
// only enable the query if there is an account and no JWT is passed in directly
|
||||
@@ -33,8 +37,12 @@ export function useThirdwebClient(jwt?: string) {
|
||||
|
||||
return useMemo(
|
||||
// prefer jwt from props over the one from the token query if it exists
|
||||
() => getThirdwebClient(jwt || query.data),
|
||||
[jwt, query.data],
|
||||
() =>
|
||||
getThirdwebClient({
|
||||
jwt: jwt || query.data,
|
||||
teamId: lastUsedTeamId,
|
||||
}),
|
||||
[jwt, query.data, lastUsedTeamId],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import {
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
THIRDWEB_PAY_DOMAIN,
|
||||
THIRDWEB_RPC_DOMAIN,
|
||||
THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
@@ -22,7 +23,14 @@ import { getZkPaymasterData } from "thirdweb/wallets/smart";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
|
||||
// returns a thirdweb client with optional JWT passed in
|
||||
export function getThirdwebClient(jwt?: string) {
|
||||
export function getThirdwebClient(
|
||||
options:
|
||||
| {
|
||||
jwt: string | null | undefined;
|
||||
teamId: string | undefined;
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
if (getVercelEnv() !== "production") {
|
||||
// if not on production: run this when creating a client to set the domains
|
||||
setThirdwebDomains({
|
||||
@@ -32,6 +40,7 @@ export function getThirdwebClient(jwt?: string) {
|
||||
storage: THIRDWEB_STORAGE_DOMAIN,
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
insight: THIRDWEB_INSIGHT_API_DOMAIN,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,7 +78,8 @@ export function getThirdwebClient(jwt?: string) {
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
secretKey: jwt ? jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: options?.teamId,
|
||||
secretKey: options?.jwt ? options.jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
config: {
|
||||
storage: {
|
||||
|
||||
@@ -1,75 +1,64 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Suspense,
|
||||
type TransitionStartFunction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { createStore } from "./reactive";
|
||||
|
||||
// Using useDashboardRouter instead of useRouter gives us a nice progress bar on top of the page when navigating using router.push or router.replace
|
||||
|
||||
// using a store instead of context to avoid triggering re-renders on root component
|
||||
const LoadingRouteHref = createStore<string | undefined>(undefined);
|
||||
const RouteStartTransitionFn = createStore<TransitionStartFunction>((fn) =>
|
||||
fn(),
|
||||
);
|
||||
|
||||
export function useDashboardRouter() {
|
||||
const startTransition = useSyncExternalStore(
|
||||
RouteStartTransitionFn.subscribe,
|
||||
RouteStartTransitionFn.getValue,
|
||||
RouteStartTransitionFn.getValue,
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...router,
|
||||
replace(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.replace(href, options);
|
||||
startTransition(() => {
|
||||
router.replace(href, options);
|
||||
});
|
||||
},
|
||||
push(href: string, options?: { scroll?: boolean }) {
|
||||
LoadingRouteHref.setValue(href);
|
||||
router.push(href, options);
|
||||
startTransition(() => {
|
||||
router.push(href, options);
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [router]);
|
||||
}
|
||||
|
||||
function useRouterLoadingStatus() {
|
||||
const loadingHref = useSyncExternalStore(
|
||||
LoadingRouteHref.subscribe,
|
||||
LoadingRouteHref.getValue,
|
||||
LoadingRouteHref.getValue,
|
||||
);
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParamsStr = searchParams?.toString();
|
||||
|
||||
const routerHref = pathname + (searchParamsStr ? `?${searchParamsStr}` : "");
|
||||
const isLoading = loadingHref && loadingHref !== routerHref;
|
||||
|
||||
// reset loading on route load
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
LoadingRouteHref.setValue(undefined);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
return isLoading;
|
||||
}, [router, startTransition]);
|
||||
}
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Render this on root
|
||||
function DashboardRouterTopProgressBarInner() {
|
||||
const isLoading = useRouterLoadingStatus();
|
||||
const [progress, setProgress] = useState(0);
|
||||
export function DashboardRouterTopProgressBar() {
|
||||
const [isRouteLoading, startTransition] = useTransition();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
RouteStartTransitionFn.setValue(startTransition);
|
||||
}, []);
|
||||
|
||||
const [progress, setProgress] = useState(0);
|
||||
const progressStartedRef = useRef(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (!isRouteLoading) {
|
||||
setProgress(0);
|
||||
progressStartedRef.current = false;
|
||||
return;
|
||||
@@ -85,7 +74,7 @@ function DashboardRouterTopProgressBarInner() {
|
||||
|
||||
async function updateProgressBar(progress: number, delay: number) {
|
||||
if (!isMounted) {
|
||||
if (!isLoading) {
|
||||
if (!isRouteLoading) {
|
||||
setProgress(100);
|
||||
}
|
||||
return;
|
||||
@@ -112,14 +101,14 @@ function DashboardRouterTopProgressBarInner() {
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [isLoading]);
|
||||
}, [isRouteLoading]);
|
||||
|
||||
const width = isLoading ? progress : 100;
|
||||
const width = isRouteLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="fixed top-0 block h-[3px] bg-foreground"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
opacity: isRouteLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
transition: width === 0 ? "none" : "width 0.2s ease, opacity 0.3s ease",
|
||||
zIndex: "100000000",
|
||||
@@ -128,12 +117,3 @@ function DashboardRouterTopProgressBarInner() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// need to wrap with suspense because of useSearchParams usage
|
||||
export function DashboardRouterTopProgressBar() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<DashboardRouterTopProgressBarInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
import { getSDKTheme } from "app/components/sdk-component-theme";
|
||||
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
useActiveAccount,
|
||||
useConnectModal,
|
||||
} from "thirdweb/react";
|
||||
import { useFavoriteChainIds } from "../../../../app/(dashboard)/(chain)/components/client/star-button";
|
||||
import { doLogout } from "../../../../app/login/auth-actions";
|
||||
import { useFavoriteChainIds } from "../../../../app/(app)/(dashboard)/(chain)/components/client/star-button";
|
||||
import { doLogout } from "../../../../app/(app)/login/auth-actions";
|
||||
import { LazyConfigureNetworkModal } from "../../../../components/configure-networks/LazyConfigureNetworkModal";
|
||||
import { useAllChainsData } from "../../../../hooks/chains/allChains";
|
||||
import {
|
||||
@@ -175,6 +175,7 @@ export const CustomConnectWallet = (props: {
|
||||
renderChain(props) {
|
||||
return (
|
||||
<CustomChainRenderer
|
||||
client={thirdwebClient}
|
||||
{...props}
|
||||
openEditChainModal={(c) => {
|
||||
setIsNetworkConfigModalOpen(true);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { type AbiEvent, formatAbiItem } from "abitype";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
type PreparedEvent,
|
||||
type ThirdwebContract,
|
||||
prepareEvent,
|
||||
} from "thirdweb";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { useContractEvents } from "thirdweb/react";
|
||||
import { useResolveContractAbi } from "./useResolveContractAbi";
|
||||
|
||||
export interface InternalTransaction {
|
||||
transactionHash: string;
|
||||
@@ -20,20 +14,8 @@ export interface InternalTransaction {
|
||||
}
|
||||
|
||||
export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
|
||||
const abiQuery = useResolveContractAbi(contract);
|
||||
|
||||
// Get all the Prepare Events from the contract abis
|
||||
const events: PreparedEvent<AbiEvent>[] = useMemo(() => {
|
||||
const eventsItems = (abiQuery.data || []).filter((o) => o.type === "event");
|
||||
const eventSignatures = eventsItems.map((event) => formatAbiItem(event));
|
||||
return eventSignatures.map((signature) =>
|
||||
prepareEvent({ signature: signature as `event ${string}` }),
|
||||
);
|
||||
}, [abiQuery.data]);
|
||||
|
||||
const eventsQuery = useContractEvents({
|
||||
contract,
|
||||
events,
|
||||
blockRange: 20000,
|
||||
watch: autoUpdate,
|
||||
});
|
||||
@@ -42,26 +24,30 @@ export function useActivity(contract: ThirdwebContract, autoUpdate?: boolean) {
|
||||
if (!eventsQuery.data) {
|
||||
return [];
|
||||
}
|
||||
const obj = eventsQuery.data.slice(0, 100).reduce(
|
||||
(acc, curr) => {
|
||||
const internalTx = acc[curr.transactionHash];
|
||||
if (internalTx) {
|
||||
internalTx.events.push(curr);
|
||||
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
|
||||
if (internalTx.blockNumber > curr.blockNumber) {
|
||||
internalTx.blockNumber = curr.blockNumber;
|
||||
const obj = eventsQuery.data
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, 100)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
const internalTx = acc[curr.transactionHash];
|
||||
if (internalTx) {
|
||||
internalTx.events.push(curr);
|
||||
internalTx.events.sort((a, b) => b.logIndex - a.logIndex);
|
||||
if (internalTx.blockNumber > curr.blockNumber) {
|
||||
internalTx.blockNumber = curr.blockNumber;
|
||||
}
|
||||
} else {
|
||||
acc[curr.transactionHash] = {
|
||||
transactionHash: curr.transactionHash,
|
||||
blockNumber: curr.blockNumber,
|
||||
events: [curr],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
acc[curr.transactionHash] = {
|
||||
transactionHash: curr.transactionHash,
|
||||
blockNumber: curr.blockNumber,
|
||||
events: [curr],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, InternalTransaction>,
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, InternalTransaction>,
|
||||
);
|
||||
return Object.values(obj).sort((a, b) =>
|
||||
a.blockNumber > b.blockNumber ? -1 : 1,
|
||||
);
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { ResultItem } from "app/(app)/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import type { EngineStatus } from "../../../app/(app)/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
|
||||
// Engine instances
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { getSDKTheme } from "app/(app)/components/sdk-component-theme";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { useTheme } from "next-themes";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
|
||||
export function UniversalBridgeEmbed({
|
||||
chainId,
|
||||
client,
|
||||
}: { chainId?: number; client: ThirdwebClient }) {
|
||||
const { theme } = useTheme();
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<PayEmbed
|
||||
client={client}
|
||||
payOptions={{
|
||||
mode: "fund_wallet",
|
||||
prefillBuy: {
|
||||
chain,
|
||||
amount: "0.01",
|
||||
},
|
||||
}}
|
||||
theme={getSDKTheme(theme === "light" ? "light" : "dark")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Metadata } from "next";
|
||||
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
|
||||
|
||||
const title = "Universal Bridge: Swap, Bridge, and On-Ramp";
|
||||
const description =
|
||||
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Universal Bridge.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
export default async function RoutesPage({
|
||||
searchParams,
|
||||
}: { searchParams: Record<string, string | string[]> }) {
|
||||
const { chainId } = searchParams;
|
||||
const client = getThirdwebClient(undefined);
|
||||
return (
|
||||
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center overflow-hidden border py-10">
|
||||
<main className="container z-10 flex justify-center">
|
||||
<UniversalBridgeEmbed
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
client={client}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
alt=""
|
||||
src="/assets/login/background.svg"
|
||||
className="-bottom-12 -right-12 pointer-events-none absolute lg:right-0 lg:bottom-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+5
-3
@@ -1,6 +1,6 @@
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
|
||||
@@ -15,6 +15,7 @@ type RouteListCardProps = {
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol: string;
|
||||
destinationTokenName: string;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export async function RouteListCard({
|
||||
@@ -26,6 +27,7 @@ export async function RouteListCard({
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenName,
|
||||
client,
|
||||
}: RouteListCardProps) {
|
||||
const [
|
||||
originChain,
|
||||
@@ -40,13 +42,13 @@ export async function RouteListCard({
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
client: getThirdwebClient(),
|
||||
client,
|
||||
})
|
||||
: undefined,
|
||||
destinationTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: destinationTokenIconUri,
|
||||
client: getThirdwebClient(),
|
||||
client,
|
||||
})
|
||||
: undefined,
|
||||
]);
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
|
||||
type RouteListRowProps = {
|
||||
@@ -15,6 +15,7 @@ type RouteListRowProps = {
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol?: string;
|
||||
destinationTokenName?: string;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
export async function RouteListRow({
|
||||
@@ -26,6 +27,7 @@ export async function RouteListRow({
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenSymbol,
|
||||
client,
|
||||
}: RouteListRowProps) {
|
||||
const [
|
||||
originChain,
|
||||
@@ -40,13 +42,13 @@ export async function RouteListRow({
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
client: getThirdwebClient(),
|
||||
client,
|
||||
})
|
||||
: undefined,
|
||||
destinationTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: destinationTokenIconUri,
|
||||
client: getThirdwebClient(),
|
||||
client,
|
||||
})
|
||||
: undefined,
|
||||
]);
|
||||
+6
-2
@@ -6,12 +6,13 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Address } from "thirdweb";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { getRoutes } from "../../../utils";
|
||||
import { ChainlistPagination } from "../client/pagination";
|
||||
import { RouteListCard } from "../server/routelist-card";
|
||||
import { RouteListRow } from "../server/routelist-row";
|
||||
import { RouteListCard } from "./routelist-card";
|
||||
import { RouteListRow } from "./routelist-row";
|
||||
|
||||
export type SearchParams = Partial<{
|
||||
includeDeprecated: boolean;
|
||||
@@ -136,6 +137,7 @@ export async function RoutesData(props: {
|
||||
const startIndex = (activePage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedRoutes = routesToRender.slice(startIndex, endIndex);
|
||||
const client = getThirdwebClient(undefined);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -170,6 +172,7 @@ export async function RoutesData(props: {
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
client={client}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -193,6 +196,7 @@ export async function RoutesData(props: {
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
client={client}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
|
||||
import { getAuthToken } from "app/api/lib/getAuthToken";
|
||||
import { getAuthToken } from "app/(app)/api/lib/getAuthToken";
|
||||
import type { Route } from "./types/route";
|
||||
|
||||
export async function getRoutes({
|
||||
+5
-21
@@ -25,11 +25,10 @@ import {
|
||||
} from "@/constants/env";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import type { CanClaimResponseType } from "app/(app)/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import Link from "next/link";
|
||||
@@ -52,7 +51,6 @@ import {
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isAccountOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
@@ -78,11 +76,11 @@ const claimFaucetSchema = z.object({
|
||||
export function FaucetButton({
|
||||
chain,
|
||||
amount,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}: {
|
||||
chain: ChainMetadata;
|
||||
amount: number;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const client = useThirdwebClient();
|
||||
@@ -186,7 +184,7 @@ export function FaucetButton({
|
||||
</div>
|
||||
<SendFundsToFaucetModalButton
|
||||
chain={definedChain}
|
||||
isLoggedIn={!!twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
client={client}
|
||||
chainMeta={chain}
|
||||
onFaucetRefill={() => {
|
||||
@@ -198,7 +196,7 @@ export function FaucetButton({
|
||||
}
|
||||
|
||||
// Force users to log in to claim the faucet
|
||||
if (!address || !twAccount) {
|
||||
if (!address || !isLoggedIn) {
|
||||
return (
|
||||
<Button variant="primary" className="w-full" asChild>
|
||||
<Link
|
||||
@@ -210,20 +208,6 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAccountOnboardingComplete(twAccount)) {
|
||||
return (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={
|
||||
pathname ? `/login?next=${encodeURIComponent(pathname)}` : "/login"
|
||||
}
|
||||
>
|
||||
Verify your Email
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { FileText } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { SectionTitle } from "../server/SectionTitle";
|
||||
|
||||
export default function NextSteps(props: { chain: ChainMetadata }) {
|
||||
const { chain } = props;
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Next Steps" />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
|
||||
<FileText className="mt-0.5 size-5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<Link
|
||||
href={
|
||||
"https://blog.thirdweb.com/supercharge-user-adoption-integrate-embedded-wallets-in-minutes/"
|
||||
}
|
||||
className="before:absolute before:inset-0"
|
||||
target="_blank"
|
||||
onClick={() =>
|
||||
trackEvent({
|
||||
category: "nextSteps",
|
||||
action: "click-inapp",
|
||||
label: "success",
|
||||
chain_id: chain.chainId,
|
||||
})
|
||||
}
|
||||
>
|
||||
Create a login for {chain.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Supercharge User Adoption—Integrate In-App Wallets in Minutes
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+11
-6
@@ -1,23 +1,28 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { PayModalButton } from "../client/PayModal";
|
||||
import { CreditCardIcon } from "../icons/CreditCardIcon";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
export function BuyFundsSection(props: {
|
||||
chain: ChainMetadata;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const sanitizedChainName = props.chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Buy Funds" />
|
||||
<SectionTitle title="Bridge" />
|
||||
<div className="flex justify-center rounded-lg border border-border bg-card px-4 py-10">
|
||||
<div className="flex max-w-[520px] flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
client={props.client}
|
||||
/>
|
||||
<CreditCardIcon
|
||||
bg="hsl(var(--background))"
|
||||
@@ -28,21 +33,21 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
|
||||
Buy Funds on {sanitizedChainName} using Universal Bridge
|
||||
Bridge Funds to {sanitizedChainName}
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<p className="max-w-[520px] px-4 text-center text-muted-foreground text-sm">
|
||||
Instantly onramp, bridge & swap into {sanitizedChainName} with any
|
||||
token.
|
||||
Get {props.chain.nativeCurrency.symbol} on {sanitizedChainName} with
|
||||
fiat or any token on another chain.
|
||||
</p>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<PayModalButton
|
||||
chainId={props.chain.chainId}
|
||||
label={`Buy Funds on ${sanitizedChainName}`}
|
||||
label={`Bridge to ${sanitizedChainName}`}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
+6
-4
@@ -1,4 +1,4 @@
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { getFaucetClaimAmount } from "../../../../../../api/testnet-faucet/claim/claim-amount";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
@@ -8,9 +8,10 @@ import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export async function FaucetSection(props: {
|
||||
chain: ChainMetadata;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const { chain, twAccount } = props;
|
||||
const { chain, isLoggedIn, client } = props;
|
||||
|
||||
// Check eligibility.
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
@@ -25,6 +26,7 @@ export async function FaucetSection(props: {
|
||||
<ChainIcon
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
client={client}
|
||||
/>
|
||||
<GiftIcon bg="hsl(var(--background))" className="-ml-2 size-12" />
|
||||
</div>
|
||||
@@ -47,7 +49,7 @@ export async function FaucetSection(props: {
|
||||
<FaucetButton
|
||||
chain={chain}
|
||||
amount={amountToGive}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
+4
-1
@@ -1,8 +1,9 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../../contexts/map-chains";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../../../contexts/map-chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { AddChainToWallet } from "../client/add-chain-to-wallet";
|
||||
|
||||
@@ -10,6 +11,7 @@ type ChainHeaderProps = {
|
||||
headerImageUrl?: string;
|
||||
logoUrl?: string;
|
||||
chain: ChainMetadata;
|
||||
client: ThirdwebClient;
|
||||
};
|
||||
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
@@ -39,6 +41,7 @@ export function ChainHeader(props: ChainHeaderProps) {
|
||||
{/* chain logo */}
|
||||
|
||||
<ChainIcon
|
||||
client={props.client}
|
||||
iconUrl={props.logoUrl}
|
||||
className={cn(
|
||||
"-translate-y-[50%] absolute top-0 left-0 size-20 overflow-hidden rounded-full border border-border bg-muted p-2 lg:size-36 lg:p-4",
|
||||
+53
-4
@@ -17,8 +17,13 @@ import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains";
|
||||
import { NebulaFloatingChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import {
|
||||
getAuthToken,
|
||||
getAuthTokenWalletAddress,
|
||||
} from "../../../../api/lib/getAuthToken";
|
||||
import { StarButton } from "../../components/client/star-button";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
@@ -55,17 +60,60 @@ export default async function ChainPageLayout(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const authToken = await getAuthToken();
|
||||
const [chain, authToken, accountAddress] = await Promise.all([
|
||||
getChain(params.chain_id),
|
||||
getAuthToken(),
|
||||
getAuthTokenWalletAddress(),
|
||||
]);
|
||||
|
||||
if (params.chain_id !== chain.slug) {
|
||||
redirect(chain.slug);
|
||||
}
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const client = getThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: undefined,
|
||||
});
|
||||
|
||||
const chainPromptPrefix = `\
|
||||
You are assisting users exploring the chain ${chain.name} (Chain ID: ${chain.chainId}). Provide concise insights into the types of applications and activities prevalent on this chain, such as DeFi protocols, NFT marketplaces, or gaming platforms. Highlight notable projects or trends without delving into technical details like consensus mechanisms or gas fees.
|
||||
Users may seek comparisons between ${chain.name} and other chains. Provide objective, succinct comparisons focusing on performance, fees, and ecosystem support. Refrain from transaction-specific advice unless requested.
|
||||
Provide users with an understanding of the unique use cases and functionalities that ${chain.name} supports. Discuss how developers leverage this chain for specific applications, such as scalable dApps, low-cost transactions, or specialized token standards, focusing on practical implementations.
|
||||
Users may be interested in utilizing thirdweb tools on ${chain.name}. Offer clear guidance on how thirdweb's SDKs, smart contract templates, and deployment tools integrate with this chain. Emphasize the functionalities enabled by thirdweb without discussing transaction execution unless prompted.
|
||||
Avoid transaction-related actions to be executed by the user unless inquired about.
|
||||
|
||||
The following is the user's message:
|
||||
`;
|
||||
|
||||
const examplePrompts: string[] = [
|
||||
"What are users doing on this chain?",
|
||||
"What are the most active contracts?",
|
||||
"Why would I use this chain over others?",
|
||||
"Can I deploy thirdweb contracts to this chain?",
|
||||
];
|
||||
|
||||
if (chain.chainId !== 1) {
|
||||
examplePrompts.push("Can I bridge assets from Ethereum to this chain?");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NebulaFloatingChatButton
|
||||
pageType="chain"
|
||||
authToken={authToken ?? undefined}
|
||||
label="Ask AI about this chain"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
messagePrefix: chainPromptPrefix,
|
||||
chainIds: [chain.chainId],
|
||||
wallet: accountAddress ?? undefined,
|
||||
}}
|
||||
examplePrompts={examplePrompts.map((prompt) => ({
|
||||
title: prompt,
|
||||
message: prompt,
|
||||
}))}
|
||||
/>
|
||||
<div className="flex h-14 border-border border-b pl-7">
|
||||
<Breadcrumb className="my-auto">
|
||||
<BreadcrumbList>
|
||||
@@ -113,6 +161,7 @@ export default async function ChainPageLayout(props: {
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
logoUrl={chain.icon?.url}
|
||||
chain={chain}
|
||||
client={client}
|
||||
/>
|
||||
|
||||
<div className="h-4 md:h-8" />
|
||||
+4
-3
@@ -77,9 +77,10 @@ export default async function Image({
|
||||
),
|
||||
// download the chain icon if there is one
|
||||
chain.icon?.url && hasWorkingChainIcon
|
||||
? download({ uri: chain.icon.url, client: getThirdwebClient() }).then(
|
||||
(res) => res.arrayBuffer(),
|
||||
)
|
||||
? download({
|
||||
uri: chain.icon.url,
|
||||
client: getThirdwebClient(undefined),
|
||||
}).then((res) => res.arrayBuffer())
|
||||
: undefined,
|
||||
// download the background image (based on chain)
|
||||
fetch(
|
||||
+7
-2
@@ -1,6 +1,8 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "../../../../account/settings/getAccount";
|
||||
import { getUserThirdwebClient } from "../../../../api/lib/getAuthToken";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import NextSteps from "./components/client/NextSteps";
|
||||
import { BuyFundsSection } from "./components/server/BuyFundsSection";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
import { ClaimChainSection } from "./components/server/ClaimChainSection";
|
||||
@@ -17,6 +19,7 @@ export default async function Page(props: {
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const client = await getUserThirdwebClient();
|
||||
const account = await getRawAccount();
|
||||
|
||||
return (
|
||||
@@ -41,9 +44,9 @@ export default async function Page(props: {
|
||||
|
||||
{/* Faucet / Buy Funds */}
|
||||
{chain.testnet ? (
|
||||
<FaucetSection chain={chain} twAccount={account} />
|
||||
<FaucetSection chain={chain} isLoggedIn={!!account} client={client} />
|
||||
) : chain.services.find((c) => c.service === "pay" && c.enabled) ? (
|
||||
<BuyFundsSection chain={chain} />
|
||||
<BuyFundsSection chain={chain} client={client} />
|
||||
) : null}
|
||||
|
||||
{/* Chain Overview */}
|
||||
@@ -57,6 +60,8 @@ export default async function Page(props: {
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
{/*Next Steps */}
|
||||
<NextSteps chain={chain} />
|
||||
|
||||
{/* Claim Chain */}
|
||||
{!chainMetadata && <ClaimChainSection />}
|
||||
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
+3
-5
@@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
@@ -13,14 +11,14 @@ interface CancelTabProps {
|
||||
id: string;
|
||||
contract: ThirdwebContract;
|
||||
isAuction?: boolean;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
id,
|
||||
contract,
|
||||
isAuction,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const { idToChain } = useAllChainsData();
|
||||
@@ -32,7 +30,7 @@ export const CancelTab: React.FC<CancelTabProps> = ({
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-3">
|
||||
<TransactionButton
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
txChainID={contract.chain.id}
|
||||
transactionCount={1}
|
||||
isPending={cancelQuery.isPending}
|
||||
+6
-7
@@ -10,9 +10,8 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/isAlchemySupported";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/isMoralisSupported";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
@@ -23,7 +22,7 @@ interface CreateListingButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
createText?: string;
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
createText = "Create",
|
||||
type,
|
||||
contract,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
isInsightSupported,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
@@ -77,7 +76,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
/>
|
||||
<div className="mt-5">
|
||||
<CreateListingsForm
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
contract={contract}
|
||||
type={type}
|
||||
actionText={createText}
|
||||
@@ -90,7 +89,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
) : (
|
||||
<div className="mt-5">
|
||||
<CreateListingsForm
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
contract={contract}
|
||||
type={type}
|
||||
actionText={createText}
|
||||
+9
-6
@@ -1,7 +1,6 @@
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useDashboardOwnedNFTs } from "@3rdweb-sdk/react/hooks/useDashboardOwnedNFTs";
|
||||
import { useWalletNFTs } from "@3rdweb-sdk/react/hooks/useWalletNFTs";
|
||||
import {
|
||||
@@ -19,8 +18,8 @@ import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/isAlchemySupported";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/isMoralisSupported";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -85,7 +84,7 @@ type CreateListingsFormProps = {
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
mode: "automatic" | "manual";
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
};
|
||||
|
||||
@@ -104,7 +103,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
type,
|
||||
actionText,
|
||||
setOpen,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
mode,
|
||||
isInsightSupported,
|
||||
}) => {
|
||||
@@ -205,6 +204,8 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
owner: nft.owner,
|
||||
type: "ERC721",
|
||||
tokenURI: nft.tokenURI,
|
||||
chainId: nft.chainId,
|
||||
tokenAddress: nft.tokenAddress,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -216,6 +217,8 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
owner: nft.owner,
|
||||
type: "ERC1155",
|
||||
tokenURI: nft.tokenURI,
|
||||
chainId: nft.chainId,
|
||||
tokenAddress: nft.tokenAddress,
|
||||
};
|
||||
}) as WalletNFT[];
|
||||
}, [ownedNFTs, form]);
|
||||
@@ -655,7 +658,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
Cancel
|
||||
</Button>
|
||||
<TransactionButton
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
txChainID={contract.chain.id}
|
||||
isPending={isFormLoading}
|
||||
transactionCount={2}
|
||||
+27
-12
@@ -3,17 +3,16 @@ import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { Flex, GridItem, SimpleGrid, usePrevious } from "@chakra-ui/react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type {
|
||||
DirectListing,
|
||||
EnglishAuction,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { BuyDirectListingButton, useActiveAccount } from "thirdweb/react";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { CancelTab } from "./cancel-tab";
|
||||
import { LISTING_STATUS } from "./types";
|
||||
@@ -23,7 +22,7 @@ interface NFTDrawerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
data: DirectListing | EnglishAuction | null;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
@@ -31,7 +30,7 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
data,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
const prevData = usePrevious(data);
|
||||
@@ -141,11 +140,6 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Todo: Add a Buy button somewhere in this section once the Dashboard is fully migrated to v5 (?)
|
||||
Kien already shipped a prebuilt component for the Marketplace Buy Button in SDK v5
|
||||
*/}
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
{data?.asset.metadata.properties ? (
|
||||
@@ -165,9 +159,30 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
contract={contract}
|
||||
id={renderData.id.toString()}
|
||||
isAuction={renderData.type === "english-auction"}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isOwner && renderData.status === "ACTIVE" && (
|
||||
<BuyDirectListingButton
|
||||
listingId={renderData.id}
|
||||
contractAddress={renderData.assetContractAddress}
|
||||
chain={contract.chain}
|
||||
client={contract.client}
|
||||
className="w-full"
|
||||
quantity={1n}
|
||||
onError={(error) => {
|
||||
toast.error("Failed to buy listing", {
|
||||
description: error.message,
|
||||
});
|
||||
}}
|
||||
onTransactionConfirmed={() => {
|
||||
toast.success("Listing bought successfully");
|
||||
}}
|
||||
>
|
||||
Buy Listing
|
||||
</BuyDirectListingButton>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import {
|
||||
IconButton,
|
||||
Select,
|
||||
@@ -99,7 +98,7 @@ interface MarketplaceTableProps {
|
||||
start: number;
|
||||
}>
|
||||
>;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
|
||||
@@ -111,7 +110,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
totalCountQuery,
|
||||
queryParams,
|
||||
setQueryParams,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
const [listingsToShow, setListingsToShow_] = useState<"all" | "valid">("all");
|
||||
|
||||
@@ -216,7 +215,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
data={tokenRow}
|
||||
isOpen={!!tokenRow}
|
||||
onClose={() => setTokenRow(null)}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
<Table {...getTableProps()}>
|
||||
<Thead>
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
@@ -9,7 +8,7 @@ import { ContractDirectListingsPage } from "./ContractDirectListingsPage";
|
||||
|
||||
export function ContractDirectListingsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -28,7 +27,7 @@ export function ContractDirectListingsPageClient(props: {
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={props.contract}
|
||||
twAccount={props.twAccount}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
+4
-5
@@ -1,19 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { DirectListingsTable } from "./components/table";
|
||||
|
||||
interface ContractDirectListingsPageProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
export const ContractDirectListingsPage: React.FC<
|
||||
ContractDirectListingsPageProps
|
||||
> = ({ contract, twAccount, isInsightSupported }) => {
|
||||
> = ({ contract, isLoggedIn, isInsightSupported }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -26,12 +25,12 @@ export const ContractDirectListingsPage: React.FC<
|
||||
contract={contract}
|
||||
type="direct-listings"
|
||||
createText="Create Direct Listing"
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DirectListingsTable contract={contract} twAccount={twAccount} />
|
||||
<DirectListingsTable contract={contract} isLoggedIn={isLoggedIn} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -13,14 +12,14 @@ import { MarketplaceTable } from "../../components/marketplace-table";
|
||||
|
||||
interface DirectListingsTableProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
|
||||
|
||||
export const DirectListingsTable: React.FC<DirectListingsTableProps> = ({
|
||||
contract,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_STATE);
|
||||
const getAllQueryResult = useReadContract(getAllListings, {
|
||||
@@ -43,7 +42,7 @@ export const DirectListingsTable: React.FC<DirectListingsTableProps> = ({
|
||||
totalCountQuery={totalCountQuery}
|
||||
queryParams={queryParams}
|
||||
setQueryParams={setQueryParams}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -24,7 +24,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ContractDirectListingsPageClient
|
||||
contract={info.contract}
|
||||
twAccount={account}
|
||||
isLoggedIn={!!account}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ContractDirectListingsPage
|
||||
contract={info.contract}
|
||||
twAccount={account}
|
||||
isLoggedIn={!!account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { ErrorPage, LoadingPage } from "../../_components/page-skeletons";
|
||||
import { RedirectToContractOverview } from "../../_components/redirect-contract-overview.client";
|
||||
@@ -9,7 +8,7 @@ import { ContractEnglishAuctionsPage } from "./ContractEnglishAuctionsPage";
|
||||
|
||||
export function ContractEnglishAuctionsPageClient(props: {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}) {
|
||||
const metadataQuery = useContractPageMetadata(props.contract);
|
||||
|
||||
@@ -28,7 +27,7 @@ export function ContractEnglishAuctionsPageClient(props: {
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={props.contract}
|
||||
twAccount={props.twAccount}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
+4
-5
@@ -1,19 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { EnglishAuctionsTable } from "./components/table";
|
||||
|
||||
interface ContractEnglishAuctionsProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
export const ContractEnglishAuctionsPage: React.FC<
|
||||
ContractEnglishAuctionsProps
|
||||
> = ({ contract, twAccount, isInsightSupported }) => {
|
||||
> = ({ contract, isLoggedIn, isInsightSupported }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -25,13 +24,13 @@ export const ContractEnglishAuctionsPage: React.FC<
|
||||
contract={contract}
|
||||
type="english-auctions"
|
||||
createText="Create English Auction"
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnglishAuctionsTable contract={contract} twAccount={twAccount} />
|
||||
<EnglishAuctionsTable contract={contract} isLoggedIn={isLoggedIn} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
@@ -13,14 +12,14 @@ import { MarketplaceTable } from "../../components/marketplace-table";
|
||||
|
||||
interface EnglishAuctionsTableProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY_STATE = { count: 50, start: 0 };
|
||||
|
||||
export const EnglishAuctionsTable: React.FC<EnglishAuctionsTableProps> = ({
|
||||
contract,
|
||||
twAccount,
|
||||
isLoggedIn,
|
||||
}) => {
|
||||
const [queryParams, setQueryParams] = useState(DEFAULT_QUERY_STATE);
|
||||
const getAllQueryResult = useReadContract(getAllAuctions, {
|
||||
@@ -37,7 +36,7 @@ export const EnglishAuctionsTable: React.FC<EnglishAuctionsTableProps> = ({
|
||||
|
||||
return (
|
||||
<MarketplaceTable
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={isLoggedIn}
|
||||
contract={contract}
|
||||
getAllQueryResult={getAllQueryResult}
|
||||
getValidQueryResult={getValidQueryResult}
|
||||
+2
-2
@@ -25,7 +25,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ContractEnglishAuctionsPageClient
|
||||
contract={info.contract}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={!!twAccount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.contract}
|
||||
twAccount={twAccount}
|
||||
isLoggedIn={!!twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
+1
@@ -21,6 +21,7 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
|
||||
owner: `0x_fake_${idx}`,
|
||||
type: "ERC721",
|
||||
supply: 1n,
|
||||
tokenAddress: ZERO_ADDRESS,
|
||||
});
|
||||
|
||||
interface NFTCardsProps {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user