Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ae2991ad7 | ||
|
|
bab4ff5e14 | ||
|
|
621c1de62e | ||
|
|
8bb2b0a227 | ||
|
|
5f82df1652 | ||
|
|
fd4414b451 | ||
|
|
d10436d3d2 | ||
|
|
56b12c6094 | ||
|
|
abbdb0f113 | ||
|
|
d05dbcb5ac | ||
|
|
3a52f9d486 | ||
|
|
9a3ebe1cb1 | ||
|
|
ad2d22558b | ||
|
|
0aac9a8593 | ||
|
|
fa7a81260d | ||
|
|
e5326c43a9 | ||
|
|
57862d0bd0 | ||
|
|
834bbc0883 | ||
|
|
3a9aa1a72b | ||
|
|
572368edc3 | ||
|
|
9b5ca42091 | ||
|
|
c6849a8734 | ||
|
|
7617cc84fc | ||
|
|
e1ad7daf54 | ||
|
|
6ad015effb | ||
|
|
b86d520f3c | ||
|
|
2df891f380 | ||
|
|
58787a28f3 | ||
|
|
e4ba47bd1e | ||
|
|
710ac32b2f | ||
|
|
6258f7429c | ||
|
|
943cd1bbcf | ||
|
|
ed65f4cbe7 | ||
|
|
0483776825 | ||
|
|
cd142f9ff5 | ||
|
|
cfea3b39ee | ||
|
|
d35b6fc1fe | ||
|
|
bcb3b92397 | ||
|
|
be803185aa | ||
|
|
ea247dbd4c | ||
|
|
e8470fcce1 | ||
|
|
a6483f4a80 | ||
|
|
2e901123c4 | ||
|
|
5c5b910570 | ||
|
|
6b9f29d43c | ||
|
|
fad7cb92b7 | ||
|
|
03617047e2 | ||
|
|
04c7243994 | ||
|
|
66cba857dc | ||
|
|
0426fe5c6e | ||
|
|
542360cc69 | ||
|
|
5e883ac550 | ||
|
|
c717eaadae | ||
|
|
691b7e1454 | ||
|
|
243749b08c | ||
|
|
4f38198ae1 | ||
|
|
b371bf9630 | ||
|
|
cf401bee84 |
@@ -15,37 +15,150 @@
|
||||
|
||||
<p align="center"><strong>All-in-one web3 SDK for Browser, Node and Mobile apps</strong></p>
|
||||
|
||||
## Features
|
||||
## Core Package
|
||||
|
||||
- Support for React & React-Native
|
||||
- First party support for [Embedded Wallets](https://portal.thirdweb.com/wallets) (social/email login)
|
||||
- First party support for [Account Abstraction](https://portal.thirdweb.com/wallets/sponsor-gas)
|
||||
- Instant connection to any chain with RPC Edge integration
|
||||
- Integrated IPFS upload/download
|
||||
- UI Components for connection, transactions, nft rendering
|
||||
- High level contract extensions for interacting with common standards
|
||||
#### [`thirdweb`](./packages/thirdweb/README.md)
|
||||
|
||||
The main SDK package providing all-in-one web3 functionality for Browser, Node, and Mobile applications.
|
||||
|
||||
```bash
|
||||
npm install thirdweb
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Type-safe contract and transaction APIs
|
||||
- In-app wallets with social/email login
|
||||
- Account abstraction (ERC4337/EIP7702) support
|
||||
- 500+ external wallets supported
|
||||
- Built in infra (RPC, bundler, paymaster, indexer)
|
||||
- React hooks and UI components
|
||||
- Automatic ABI resolution
|
||||
- IPFS upload/download
|
||||
- Cross-platform support (Web, React Native)
|
||||
|
||||
## Library Comparison
|
||||
### Documentation
|
||||
|
||||
| | thirdweb | Wagmi + Viem | Ethers@6 |
|
||||
| ----------------------------------------- | -------- | ------------------ | -------- |
|
||||
| Type safe contract API | ✅ | ✅ | ✅ |
|
||||
| Type safe wallet API | ✅ | ✅ | ✅ |
|
||||
| EVM utils | ✅ | ✅ | ✅ |
|
||||
| RPC for any EVM | ✅ | ⚠️ public RPC only | ❌ |
|
||||
| Automatic ABI Resolution | ✅ | ❌ | ❌ |
|
||||
| IPFS Upload/Download | ✅ | ❌ | ❌ |
|
||||
| Embedded wallet (email/ social login) | ✅ | ⚠️ via 3rd party | ❌ |
|
||||
| Account abstraction (ERC4337) support | ✅ | ⚠️ via 3rd party | ❌ |
|
||||
| Web3 wallet connectors | ✅ | ✅ | ❌ |
|
||||
| Local wallet creation | ✅ | ✅ | ✅ |
|
||||
| Auth (SIWE) | ✅ | ✅ | ❌ |
|
||||
| Extensions functions for common standards | ✅ | ✅ | ❌ |
|
||||
| React Hooks | ✅ | ✅ | ❌ |
|
||||
| React UI components | ✅ | ❌ | ❌ |
|
||||
| React Native Hooks | ✅ | ✅ | ❌ |
|
||||
| React Native UI Components | ✅ | ❌ | ❌ |
|
||||
Visit the [developer portal](https://portal.thirdweb.com) for full documentation.
|
||||
|
||||
### 🚀 Quick Start
|
||||
|
||||
#### For React Applications
|
||||
|
||||
```bash
|
||||
npm install thirdweb
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { ConnectButton, useActiveAccount } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
clientId: "YOUR_CLIENT_ID",
|
||||
});
|
||||
|
||||
function App() {
|
||||
const account = useActiveAccount();
|
||||
console.log("Connected as", account?.address);
|
||||
|
||||
return <ConnectButton client={client} />;
|
||||
}
|
||||
```
|
||||
|
||||
For React Native Applications, you'll also need to install the `@thirdweb-dev/react-native-adapter` package and import it at app startup for polyfills.
|
||||
|
||||
#### For Backend Applications
|
||||
|
||||
```bash
|
||||
npm install thirdweb
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { createThirdwebClient, Engine } from "thirdweb";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: "YOUR_SECRET_KEY",
|
||||
});
|
||||
|
||||
const wallet = Engine.serverWallet({
|
||||
client,
|
||||
address: "0x...",
|
||||
});
|
||||
|
||||
const transaction = transfer({
|
||||
contract: getContract({
|
||||
client,
|
||||
address: "0x...", // token contract
|
||||
chain: defineChain(1),
|
||||
}),
|
||||
to: "0x...", // recipient
|
||||
amount: "0.01", // amount in tokens
|
||||
});
|
||||
|
||||
await wallet.enqueueTransaction({
|
||||
transaction,
|
||||
});
|
||||
```
|
||||
|
||||
## Adapters
|
||||
|
||||
#### [`@thirdweb-dev/react-native-adapter`](./packages/react-native-adapter/README.md)
|
||||
|
||||
Required polyfills and configuration for running the thirdweb SDK in React Native applications.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/react-native-adapter
|
||||
```
|
||||
|
||||
#### [`@thirdweb-dev/wagmi-adapter`](./packages/wagmi-adapter/README.md)
|
||||
|
||||
Integration layer for using thirdweb's in-app wallets with wagmi.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/wagmi-adapter
|
||||
```
|
||||
|
||||
## Type safe API wrappers
|
||||
|
||||
#### [`@thirdweb-dev/api`](./packages/api/README.md)
|
||||
|
||||
TypeScript SDK for thirdweb's API, combining all of thirdweb products.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/api
|
||||
```
|
||||
|
||||
#### [`@thirdweb-dev/engine`](./packages/engine/README.md)
|
||||
|
||||
TypeScript SDK for Engine, thirdweb's backend onchain executor service.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/engine
|
||||
```
|
||||
|
||||
#### [`@thirdweb-dev/insight`](./packages/insight/README.md)
|
||||
|
||||
TypeScript SDK for Insight, thirdweb's multichain indexer service.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/insight
|
||||
```
|
||||
|
||||
#### [`@thirdweb-dev/vault-sdk`](./packages/vault-sdk/README.md)
|
||||
|
||||
SDK for interacting with Vault, thirdweb's secure key management service.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/vault-sdk
|
||||
```
|
||||
|
||||
#### [`@thirdweb-dev/nebula`](./packages/nebula/README.md)
|
||||
|
||||
TypeScript SDK for Nebula, thirdweb's AI agent service.
|
||||
|
||||
```bash
|
||||
npm install @thirdweb-dev/nebula
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -55,7 +168,8 @@ See our [open source page](https://thirdweb.com/open-source) for more informatio
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Documentation](https://portal.thirdweb.com/typescript/v5)
|
||||
- [Dashboard](https://thirdweb.com/login)
|
||||
- [Documentation](https://portal.thirdweb.com/)
|
||||
- [Templates](https://thirdweb.com/templates)
|
||||
- [YouTube](https://www.youtube.com/c/thirdweb)
|
||||
|
||||
|
||||
+20
-54
@@ -1,29 +1,22 @@
|
||||
#
|
||||
# Public (client)
|
||||
#
|
||||
|
||||
# API authorized domain
|
||||
# not required to build, defaults to prod
|
||||
NEXT_PUBLIC_THIRDWEB_DOMAIN="localhost:3000"
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
# API host. For local development, please use "https://api.thirdweb-dev.com"
|
||||
# otherwise: "https://api.thirdweb.com"
|
||||
# local host - http://127.0.0.1:3005
|
||||
NEXT_PUBLIC_THIRDWEB_API_HOST="https://api.thirdweb-dev.com"
|
||||
|
||||
# Bridge API. For local development, please use "https://bridge.thirdweb-dev.com"
|
||||
# otherwise: "https://bridge.thirdweb.com"
|
||||
NEXT_PUBLIC_THIRDWEB_BRIDGE_HOST="https://bridge.thirdweb-dev.com"
|
||||
|
||||
# Paper API host
|
||||
# in-app wallet host
|
||||
NEXT_PUBLIC_THIRDWEB_EWS_API_HOST="https://ews.thirdweb-dev.com"
|
||||
|
||||
# thirdweb local api host
|
||||
# NEXT_PUBLIC_THIRDWEB_API_HOST="http://127.0.0.1:3005"
|
||||
|
||||
# Client ID
|
||||
# Get the client id from https://thirdweb.com/create-api-key and set it here,
|
||||
# Client ID - Required
|
||||
# Get the client id from https://thirdweb.com/create-api-key and set it here, and also set DASHBOARD_SECRET_KEY below
|
||||
# make sure to allow localhost:3000 in the allowed origins
|
||||
NEXT_PUBLIC_DASHBOARD_CLIENT_ID="<replace_me_with_your_client_id>"
|
||||
NEXT_PUBLIC_DASHBOARD_CLIENT_ID=""
|
||||
|
||||
# IPFS gateway url
|
||||
NEXT_PUBLIC_IPFS_GATEWAY_URL="https://{clientId}.thirdwebstorage-dev.com/ipfs/{cid}/{path}"
|
||||
@@ -39,62 +32,35 @@ NEXT_PUBLIC_TYPESENSE_CONTRACT_API_KEY=
|
||||
# - not required for prod/staging
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
|
||||
# Stripe Customer portal
|
||||
NEXT_PUBLIC_STRIPE_KEY=
|
||||
# required for server wallet management
|
||||
NEXT_PUBLIC_THIRDWEB_VAULT_URL=""
|
||||
NEXT_PUBLIC_ENGINE_CLOUD_URL=""
|
||||
|
||||
|
||||
# Demo Engine - required for showing demo engine page
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL=""
|
||||
|
||||
NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID=
|
||||
|
||||
#
|
||||
# Private (server)
|
||||
#
|
||||
# Get the secret key from https://thirdweb.com/create-api-key and set it here, Make sure to allow localhost:3000 in the allowed origins
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
# Get the secret key from https://thirdweb.com/create-api-key and set it here and also set NEXT_PUBLIC_DASHBOARD_CLIENT_ID above,
|
||||
# Make sure to allow localhost:3000 in the allowed origins
|
||||
DASHBOARD_SECRET_KEY="<replace_me_with_your_secret_key>"
|
||||
|
||||
# Client id for api routes
|
||||
# Client id for api routes - required for contract OG image generation
|
||||
API_ROUTES_CLIENT_ID=
|
||||
|
||||
# <moralis.io API key (used for api routes for token balances and wallet NFTs)
|
||||
# - not required to build (unless using wallet NFTs and token balances)>
|
||||
MORALIS_API_KEY=
|
||||
|
||||
# alchemy.com API key (used for wallet NFTS)
|
||||
# - cannot be restricted to IP/domain because vercel has no stable IPs and it happens during build & runtime api route call
|
||||
# - not required to build (unless testing wallet NFTs)>
|
||||
SSR_ALCHEMY_KEY=
|
||||
|
||||
|
||||
# Hubspot Access Token (used for contact us form)
|
||||
# - not required to build (unless testing contact us form)>
|
||||
HUBSPOT_ACCESS_TOKEN=
|
||||
|
||||
# Upload server url
|
||||
NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER="https://storage.thirdweb-preview.com"
|
||||
|
||||
# Unthread variables - only required for submitting the support form in /support
|
||||
UNTHREAD_API_KEY=""
|
||||
UNTHREAD_TRIAGE_CHANNEL_ID=""
|
||||
UNTHREAD_EMAIL_INBOX_ID=""
|
||||
UNTHREAD_FREE_TIER_ID=""
|
||||
UNTHREAD_GROWTH_TIER_ID=""
|
||||
UNTHREAD_PRO_TIER_ID=""
|
||||
|
||||
# Demo Engine
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL=""
|
||||
|
||||
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
|
||||
API_SERVER_SECRET=""
|
||||
|
||||
# Used for the Faucet page (/<chain_id>)
|
||||
# Used in faucet on chain page (/<chain-id>) and login page (/login) - turnstile is disabled in localhost development
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
|
||||
TURNSTILE_SECRET_KEY=""
|
||||
|
||||
REDIS_URL=""
|
||||
|
||||
ANALYTICS_SERVICE_URL=""
|
||||
|
||||
|
||||
# required for billing parts of the dashboard (team -> settings -> billing / invoices)
|
||||
STRIPE_SECRET_KEY=""
|
||||
|
||||
# required for server wallet management
|
||||
NEXT_PUBLIC_THIRDWEB_VAULT_URL=""
|
||||
NEXT_PUBLIC_ENGINE_CLOUD_URL=""
|
||||
@@ -10,13 +10,6 @@ module.exports = {
|
||||
"plugin:storybook/recommended",
|
||||
],
|
||||
overrides: [
|
||||
// disable restricted imports in chakra
|
||||
{
|
||||
files: "src/chakra/**/*",
|
||||
rules: {
|
||||
"no-restricted-imports": ["off"],
|
||||
},
|
||||
},
|
||||
// allow direct PostHog imports inside analytics helpers
|
||||
{
|
||||
files: "src/@/analytics/**/*",
|
||||
@@ -91,60 +84,6 @@ module.exports = {
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
// these are provided by chakra folder, so we don't want to import them from chakra directly
|
||||
importNames: [
|
||||
"Card",
|
||||
"Button",
|
||||
"Checkbox",
|
||||
"Badge",
|
||||
"Drawer",
|
||||
"Heading",
|
||||
"Text",
|
||||
"FormLabel",
|
||||
"FormHelperText",
|
||||
"FormErrorMessage",
|
||||
"MenuGroup",
|
||||
"VStack",
|
||||
"HStack",
|
||||
"AspectRatio",
|
||||
"useToast",
|
||||
"useClipboard",
|
||||
"Badge",
|
||||
"Stack",
|
||||
// also the types
|
||||
"ButtonProps",
|
||||
"BadgeProps",
|
||||
"DrawerProps",
|
||||
"HeadingProps",
|
||||
"TextProps",
|
||||
"FormLabelProps",
|
||||
"HelpTextProps",
|
||||
"MenuGroupProps",
|
||||
"MenuItemProps",
|
||||
"AspectRatioProps",
|
||||
"BadgeProps",
|
||||
"StackProps",
|
||||
],
|
||||
message:
|
||||
'import component from "chakra" folder instead if you have to use chakra component, But use shadcn component otherwise',
|
||||
name: "@chakra-ui/react",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Import from `@chakra-ui/react` instead of `@chakra-ui/layout`.",
|
||||
name: "@chakra-ui/layout",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Import from `@chakra-ui/react` instead of `@chakra-ui/button`.",
|
||||
name: "@chakra-ui/button",
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.",
|
||||
name: "@chakra-ui/menu",
|
||||
},
|
||||
{
|
||||
importNames: ["useRouter"],
|
||||
message:
|
||||
@@ -157,6 +96,12 @@ module.exports = {
|
||||
name: "posthog-js",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ["**/../@/**"],
|
||||
message: "Use absolute imports instead. Example: '@/foo/bar..'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
|
||||
@@ -20,11 +20,6 @@ const config: StorybookConfig = {
|
||||
name: getAbsolutePath("@storybook/nextjs"),
|
||||
options: {},
|
||||
},
|
||||
refs: {
|
||||
"@chakra-ui/react": {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
features: {
|
||||
experimentalRSC: true,
|
||||
|
||||
@@ -9,10 +9,10 @@ const ContentSecurityPolicy = `
|
||||
img-src * data: blob:;
|
||||
media-src * data: blob:;
|
||||
object-src 'none';
|
||||
style-src 'self' 'unsafe-inline' vercel.live;
|
||||
style-src 'self' 'unsafe-inline' vercel.live us.posthog.com;
|
||||
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 googletagmanager.com us-assets.i.posthog.com edit.framer.com;
|
||||
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 googletagmanager.com us-assets.i.posthog.com edit.framer.com framer.com googletagmanager.com;
|
||||
connect-src * data: blob:;
|
||||
worker-src 'self' blob:;
|
||||
block-all-mixed-content;
|
||||
@@ -115,9 +115,6 @@ const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
const FRAMER_ADDITIONAL_LANGUAGES = ["es"];
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
compiler: {
|
||||
emotion: true,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^2.8.2",
|
||||
"@chakra-ui/styled-system": "^2.9.2",
|
||||
"@chakra-ui/theme-tools": "^2.1.2",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
@@ -36,11 +31,9 @@
|
||||
"abitype": "1.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "5.0.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"framer-motion": "12.23.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.6.1",
|
||||
@@ -64,7 +57,6 @@
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-qrcode-logo": "^3.0.0",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.3",
|
||||
"remark-gfm": "4.0.1",
|
||||
@@ -85,7 +77,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.6",
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "4.0.1",
|
||||
"@next/bundle-analyzer": "15.3.5",
|
||||
"@next/eslint-plugin-next": "15.3.5",
|
||||
@@ -127,12 +118,10 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"fix": "biome check ./src --fix && eslint ./src --fix",
|
||||
"format": "biome format ./src --write",
|
||||
"gen:theme-typings": "chakra-cli tokens src/chakra/theme/index.ts",
|
||||
"knip": "knip",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
"playwright": "playwright test",
|
||||
"postbuild": "next-sitemap",
|
||||
"postinstall": "pnpm run gen:theme-typings",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"start": "next start",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getRawAccount } from "../../app/(app)/account/settings/getAccount";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
|
||||
export async function getRawAccountAction() {
|
||||
return getRawAccount();
|
||||
@@ -1,89 +0,0 @@
|
||||
"use server";
|
||||
import { isAddress, toTokens, ZERO_ADDRESS } from "thirdweb";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import { MORALIS_API_KEY } from "@/constants/server-envs";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
|
||||
type BalanceQueryResponse = Array<{
|
||||
balance: string;
|
||||
decimals: number;
|
||||
name?: string;
|
||||
symbol: string;
|
||||
token_address: string;
|
||||
display_balance: string;
|
||||
}>;
|
||||
|
||||
export async function getTokenBalancesFromMoralis(params: {
|
||||
contractAddress: string;
|
||||
chainId: number;
|
||||
}): Promise<
|
||||
| { data: BalanceQueryResponse; error: undefined }
|
||||
| {
|
||||
data: undefined;
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const { contractAddress, chainId } = params;
|
||||
|
||||
if (!isAddress(contractAddress)) {
|
||||
return {
|
||||
data: undefined,
|
||||
error: "invalid address",
|
||||
};
|
||||
}
|
||||
|
||||
const getNativeBalance = async (): Promise<BalanceQueryResponse> => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const chain = defineDashboardChain(chainId, undefined);
|
||||
const balance = await getWalletBalance({
|
||||
address: contractAddress,
|
||||
chain,
|
||||
client: serverThirdwebClient,
|
||||
});
|
||||
return [
|
||||
{
|
||||
balance: balance.value.toString(),
|
||||
decimals: balance.decimals,
|
||||
display_balance: toTokens(balance.value, balance.decimals),
|
||||
name: "Native Token",
|
||||
symbol: balance.symbol,
|
||||
token_address: ZERO_ADDRESS,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getTokenBalances = async (): Promise<BalanceQueryResponse> => {
|
||||
const _chain = encodeURIComponent(`0x${chainId?.toString(16)}`);
|
||||
const _address = encodeURIComponent(contractAddress);
|
||||
const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`;
|
||||
|
||||
const resp = await fetch(tokenBalanceEndpoint, {
|
||||
headers: {
|
||||
"x-api-key": MORALIS_API_KEY,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
resp.body?.cancel();
|
||||
return [];
|
||||
}
|
||||
const json = await resp.json();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
return json.map((balance: any) => ({
|
||||
...balance,
|
||||
display_balance: toTokens(BigInt(balance.balance), balance.decimals),
|
||||
}));
|
||||
};
|
||||
|
||||
const [nativeBalance, tokenBalances] = await Promise.all([
|
||||
getNativeBalance(),
|
||||
getTokenBalances(),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: [...nativeBalance, ...tokenBalances],
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
import { MORALIS_API_KEY } from "@/constants/server-envs";
|
||||
import {
|
||||
generateAlchemyUrl,
|
||||
transformAlchemyResponseToNFT,
|
||||
} from "@/lib/wallet/nfts/alchemy";
|
||||
import {
|
||||
generateMoralisUrl,
|
||||
transformMoralisResponseToNFT,
|
||||
} from "@/lib/wallet/nfts/moralis";
|
||||
import type { WalletNFT } from "@/lib/wallet/nfts/types";
|
||||
import { getVercelEnv } from "@/utils/vercel";
|
||||
import { isAlchemySupported } from "../lib/wallet/nfts/isAlchemySupported";
|
||||
import { isMoralisSupported } from "../lib/wallet/nfts/isMoralisSupported";
|
||||
|
||||
type WalletNFTApiReturn =
|
||||
| { result: WalletNFT[]; error?: undefined }
|
||||
| { result?: undefined; error: string };
|
||||
|
||||
export async function getWalletNFTs(params: {
|
||||
chainId: number;
|
||||
owner: string;
|
||||
isInsightSupported: boolean;
|
||||
}): Promise<WalletNFTApiReturn> {
|
||||
const { chainId, owner } = params;
|
||||
|
||||
if (params.isInsightSupported) {
|
||||
const response = await getWalletNFTsFromInsight({ chainId, owner });
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
error: response.error,
|
||||
};
|
||||
}
|
||||
|
||||
return { result: response.data };
|
||||
}
|
||||
|
||||
if (isAlchemySupported(chainId)) {
|
||||
const url = generateAlchemyUrl({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
return { error: response.statusText };
|
||||
}
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformAlchemyResponseToNFT(parsedResponse, owner);
|
||||
|
||||
return { error: undefined, result };
|
||||
} catch (err) {
|
||||
console.error("Error fetching NFTs", err);
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
}
|
||||
|
||||
if (isMoralisSupported(chainId) && MORALIS_API_KEY) {
|
||||
const url = generateMoralisUrl({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"X-API-Key": MORALIS_API_KEY,
|
||||
},
|
||||
method: "GET",
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
return { error: response.statusText };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformMoralisResponseToNFT(
|
||||
await parsedResponse,
|
||||
owner,
|
||||
chainId,
|
||||
);
|
||||
|
||||
return { result };
|
||||
} catch (err) {
|
||||
console.error("Error fetching NFTs", err);
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "unsupported chain" };
|
||||
}
|
||||
|
||||
type OwnedNFTInsightResponse = {
|
||||
name: string;
|
||||
description: string;
|
||||
image_url?: string;
|
||||
background_color: string;
|
||||
external_url: string;
|
||||
metadata_url: string;
|
||||
extra_metadata: {
|
||||
customImage?: string;
|
||||
customAnimationUrl?: string;
|
||||
animation_original_url?: string;
|
||||
image_original_url?: string;
|
||||
};
|
||||
collection: {
|
||||
name: string;
|
||||
description: string;
|
||||
extra_metadata: Record<string, string>;
|
||||
};
|
||||
contract: {
|
||||
chain_id: number;
|
||||
address: string;
|
||||
type: "erc1155" | "erc721";
|
||||
name: string;
|
||||
};
|
||||
owner_addresses: string[];
|
||||
token_id: string;
|
||||
balance: string;
|
||||
token_type: "erc1155" | "erc721";
|
||||
};
|
||||
|
||||
async function getWalletNFTsFromInsight(params: {
|
||||
chainId: number;
|
||||
owner: string;
|
||||
}): Promise<
|
||||
| {
|
||||
data: WalletNFT[];
|
||||
ok: true;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const { chainId, owner } = params;
|
||||
|
||||
const thirdwebDomain =
|
||||
getVercelEnv() === "production" ? "thirdweb" : "thirdweb-dev";
|
||||
const url = new URL(`https://insight.${thirdwebDomain}.com/v1/nfts`);
|
||||
url.searchParams.append("chain", chainId.toString());
|
||||
url.searchParams.append("limit", "10");
|
||||
url.searchParams.append("owner_address", owner);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.text();
|
||||
return {
|
||||
error: errorMessage,
|
||||
ok: false,
|
||||
};
|
||||
}
|
||||
|
||||
const nftsResponse = (await response.json()) as {
|
||||
data: OwnedNFTInsightResponse[];
|
||||
};
|
||||
|
||||
const isDev = getVercelEnv() !== "production";
|
||||
|
||||
// NOTE: ipfscdn.io/ to thirdwebstorage-dev.com/ replacement is temporary
|
||||
// This should be fixed in the insight dev endpoint
|
||||
|
||||
const walletNFTs = nftsResponse.data.map((nft) => {
|
||||
const walletNFT: WalletNFT = {
|
||||
chainId: nft.contract.chain_id,
|
||||
contractAddress: nft.contract.address,
|
||||
id: nft.token_id,
|
||||
metadata: {
|
||||
animation_url: isDev
|
||||
? nft.extra_metadata.animation_original_url?.replace(
|
||||
"ipfscdn.io/",
|
||||
"thirdwebstorage-dev.com/",
|
||||
)
|
||||
: nft.extra_metadata.animation_original_url,
|
||||
background_color: nft.background_color,
|
||||
description: nft.description,
|
||||
external_url: nft.external_url,
|
||||
image: isDev
|
||||
? nft.image_url?.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
|
||||
: nft.image_url,
|
||||
name: nft.name,
|
||||
uri: isDev
|
||||
? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
|
||||
: nft.metadata_url,
|
||||
},
|
||||
owner: params.owner,
|
||||
supply: nft.balance,
|
||||
tokenAddress: nft.contract.address,
|
||||
tokenURI: nft.metadata_url,
|
||||
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
|
||||
};
|
||||
|
||||
return walletNFT;
|
||||
});
|
||||
|
||||
return {
|
||||
data: walletNFTs,
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import Stripe from "stripe";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { STRIPE_SECRET_KEY } from "@/constants/server-envs";
|
||||
|
||||
let existingStripe: Stripe | undefined;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import "server-only";
|
||||
import { randomBytes } from "crypto";
|
||||
import { format } from "date-fns";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
|
||||
export async function createTeam(options?: { name?: string; slug?: string }) {
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import type { ProductSKU } from "../types/billing";
|
||||
|
||||
// ----------------------------
|
||||
@@ -269,22 +270,6 @@ export function reportAssetBuyFailed(properties: {
|
||||
|
||||
// Assets Landing Page ----------------------------
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of asset creation started from the assets page
|
||||
* - To track which asset types are being created the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportAssetCreationStarted(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
}) {
|
||||
posthog.capture("asset creation started", {
|
||||
assetType: properties.assetType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of assets imported successfully from the assets page
|
||||
@@ -428,3 +413,93 @@ export function reportUpsellClicked(properties: UpsellParams) {
|
||||
export function reportPaymentCardClick(properties: { id: string }) {
|
||||
posthog.capture("payment card clicked", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To create a funnel "create payment link pageview" -> "payment link created" to understand the conversion rate
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @greg
|
||||
*/
|
||||
export function reportPaymentLinkCreated(properties: {
|
||||
linkId: string;
|
||||
clientId: string;
|
||||
}) {
|
||||
posthog.capture("payment link created", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track funnel "payment link pageview" -> "payment link buy successful" to understand the conversion rate
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @greg
|
||||
*/
|
||||
export function reportPaymentLinkBuySuccessful() {
|
||||
posthog.capture("payment link buy successful");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of failed payment link buys
|
||||
* - To track what errors users encounter when trying to buy from a payment link
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @greg
|
||||
*/
|
||||
export function reportPaymentLinkBuyFailed(properties: {
|
||||
errorMessage: string;
|
||||
}) {
|
||||
posthog.capture("payment link buy failed", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To create a funnel for "asset pageview" -> "asset purchase successful" to understand the conversion rate
|
||||
* - To understand which asset types are being viewed the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportAssetPageview(properties: {
|
||||
assetType: "nft" | "coin";
|
||||
chainId: number;
|
||||
}) {
|
||||
posthog.capture("asset pageview", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the usage of fund wallet modal
|
||||
* - To create a funnel "fund wallet modal opened" -> "fund wallet buy successful" to understand the conversion rate
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportFundWalletOpened() {
|
||||
posthog.capture("fund wallet opened");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of successful fund wallet buys
|
||||
* - To create a funnel "fund wallet modal opened" -> "fund wallet buy successful" to understand the conversion rate
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportFundWalletSuccessful() {
|
||||
posthog.capture("fund wallet successful");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of failed fund wallet buys
|
||||
* - To track the errors that users encounter when trying to buy from a fund wallet modal
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportFundWalletFailed(params: { errorMessage: string }) {
|
||||
posthog.capture("fund wallet failed", params);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import type { Account } from "@/hooks/useApi";
|
||||
import { isAccountOnboardingComplete } from "@/utils/account-onboarding";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import { isAccountOnboardingComplete } from "../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
/**
|
||||
* Just get the account object without enforcing onboarding.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type LinkedWallet = {
|
||||
createdAt: string;
|
||||
@@ -1,6 +1,9 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { LAST_USED_TEAM_ID } from "@/constants/cookies";
|
||||
import {
|
||||
COOKIE_ACTIVE_ACCOUNT,
|
||||
COOKIE_PREFIX_TOKEN,
|
||||
LAST_USED_TEAM_ID,
|
||||
} from "@/constants/cookie";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
|
||||
export async function getAuthToken() {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { fetchDeployMetadata as sdkFetchDeployMetadata } from "thirdweb/contract";
|
||||
import { removeUndefinedFromObjectDeep } from "@/utils/object";
|
||||
import type { ContractId } from "./types";
|
||||
import type { ContractId } from "../../components/contract-components/types";
|
||||
|
||||
// metadata PRE publish, only has the compiler output info (from CLI)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
|
||||
export async function verifyContract(contract: ThirdwebContract) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://contract.thirdweb.com/verify/contract",
|
||||
{
|
||||
body: JSON.stringify({
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Error verifying contract: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`Error verifying contract: ${error}`);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getProjectContracts,
|
||||
type ProjectContract,
|
||||
} from "@/api/getProjectContracts";
|
||||
} from "@/api/project/getProjectContracts";
|
||||
import { fetchChainWithLocalOverrides } from "@/utils/fetchChainWithLocalOverrides";
|
||||
|
||||
export async function getSortedDeployedContracts(params: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type Project = ProjectResponse;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type AuditLogEntry = {
|
||||
who: {
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export async function createDedicatedSupportChannel(
|
||||
teamIdOrSlug: string,
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type AuthOption =
|
||||
| "email"
|
||||
@@ -1,11 +1,11 @@
|
||||
import "server-only";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { LAST_USED_TEAM_ID } from "@/constants/cookies";
|
||||
import { getValidAccount } from "@/api/account/get-account";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { LAST_USED_TEAM_ID } from "@/constants/cookie";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "@/constants/server-envs";
|
||||
import { getValidAccount } from "../../app/(app)/account/settings/getAccount";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
import { getMemberByAccountId } from "./team-members";
|
||||
|
||||
export type Team = TeamResponse & { stripeCustomerId: string | null };
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type TeamInvite = {
|
||||
id: string;
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
const TeamAccountRole = {
|
||||
MEMBER: "MEMBER",
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
import "server-only";
|
||||
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "./auth-token";
|
||||
|
||||
export type VerifiedDomainResponse =
|
||||
| {
|
||||
@@ -200,7 +200,11 @@ export async function createPaymentLink(props: {
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
return;
|
||||
const response = (await res.json()) as {
|
||||
data: PaymentLink;
|
||||
};
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePaymentLink(props: {
|
||||
|
||||
@@ -18,6 +18,7 @@ export function DateRangeSelector(props: {
|
||||
}) {
|
||||
const { range, setRange } = props;
|
||||
const daysDiff = differenceInCalendarDays(range.to, range.from);
|
||||
|
||||
const matchingRange =
|
||||
normalizeTime(range.to).getTime() === normalizeTime(new Date()).getTime()
|
||||
? durationPresets.find((preset) => preset.days === daysDiff)
|
||||
@@ -85,7 +86,7 @@ export function getLastNDaysRange(id: DurationId) {
|
||||
throw new Error(`Invalid duration id: ${id}`);
|
||||
}
|
||||
|
||||
const todayDate = new Date(Date.now() + 1000 * 60 * 60 * 24); // add 1 day to avoid timezone issues
|
||||
const todayDate = new Date(Date.now());
|
||||
|
||||
const value: Range = {
|
||||
from: subDays(todayDate, durationInfo.days),
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import type {
|
||||
DurationId,
|
||||
Range,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
getLastNDaysRange,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
|
||||
export function RangeSelector({
|
||||
range,
|
||||
interval,
|
||||
}: {
|
||||
range?: Range;
|
||||
interval: "day" | "week";
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useDashboardRouter();
|
||||
const [localRange, setRange] = useState<Range>(
|
||||
range || getLastNDaysRange("last-120"),
|
||||
);
|
||||
const [localInterval, setInterval] = useState<"day" | "week">(interval);
|
||||
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
if (range) {
|
||||
setRange(range);
|
||||
return range;
|
||||
}
|
||||
if (searchParams) {
|
||||
const fromStringified = searchParams.get("from");
|
||||
const from = fromStringified
|
||||
? new Date(fromStringified)
|
||||
: getLastNDaysRange("last-120").from;
|
||||
const toStringified = searchParams.get("to");
|
||||
const to = toStringified ? new Date(toStringified) : new Date();
|
||||
const type = (searchParams.get("type") as DurationId) || "last-120";
|
||||
setRange({ from, to, type });
|
||||
setInterval(interval);
|
||||
return { from, to, type } satisfies Range;
|
||||
}
|
||||
return getLastNDaysRange("last-120");
|
||||
},
|
||||
queryKey: ["analytics-range", searchParams?.toString(), range],
|
||||
});
|
||||
|
||||
// prefetch for each interval and default range
|
||||
useQuery({
|
||||
queryFn: async () => {
|
||||
const newSearchParams = new URLSearchParams(searchParams || {});
|
||||
for (const interval of ["day", "week"] as const) {
|
||||
newSearchParams.set("interval", interval);
|
||||
for (const type of [
|
||||
"last-120",
|
||||
"last-60",
|
||||
"last-30",
|
||||
"last-7",
|
||||
] as const) {
|
||||
const newRange = getLastNDaysRange(type);
|
||||
newSearchParams.set("from", newRange.from.toISOString());
|
||||
newSearchParams.set("to", newRange.to.toISOString());
|
||||
newSearchParams.set("type", newRange.type);
|
||||
router.prefetch(`${pathname}?${newSearchParams.toString()}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
queryKey: ["analytics-range", searchParams?.toString()],
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-3 flex-col lg:flex-row">
|
||||
<DateRangeSelector
|
||||
className="rounded-full"
|
||||
range={localRange}
|
||||
setRange={(newRange) => {
|
||||
setRange(newRange);
|
||||
const newSearchParams = new URLSearchParams(searchParams || {});
|
||||
newSearchParams.set("from", newRange.from.toISOString());
|
||||
newSearchParams.set("to", newRange.to.toISOString());
|
||||
newSearchParams.set("type", newRange.type);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
className="bg-card rounded-full"
|
||||
intervalType={localInterval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setInterval(newInterval);
|
||||
const newSearchParams = new URLSearchParams(searchParams || {});
|
||||
newSearchParams.set("interval", newInterval);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useResponsiveSearchParams,
|
||||
useSetResponsiveSearchParams,
|
||||
} from "responsive-rsc";
|
||||
import {
|
||||
DateRangeSelector,
|
||||
type DurationId,
|
||||
} from "@/components/analytics/date-range-selector";
|
||||
import { IntervalSelector } from "@/components/analytics/interval-selector";
|
||||
import { getFiltersFromSearchParams, normalizeTimeISOString } from "@/lib/time";
|
||||
|
||||
export function ResponsiveTimeFilters(props: { defaultRange: DurationId }) {
|
||||
const responsiveSearchParams = useResponsiveSearchParams();
|
||||
const setResponsiveSearchParams = useSetResponsiveSearchParams();
|
||||
const { range, interval } = getFiltersFromSearchParams({
|
||||
defaultRange: props.defaultRange,
|
||||
from: responsiveSearchParams.from,
|
||||
interval: responsiveSearchParams.interval,
|
||||
to: responsiveSearchParams.to,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-3 flex-col lg:flex-row">
|
||||
<DateRangeSelector
|
||||
className="rounded-full"
|
||||
range={range}
|
||||
setRange={(newRange) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
const newParams = {
|
||||
...v,
|
||||
from: normalizeTimeISOString(newRange.from),
|
||||
to: normalizeTimeISOString(newRange.to),
|
||||
};
|
||||
return newParams;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<IntervalSelector
|
||||
className="bg-card rounded-full"
|
||||
intervalType={interval}
|
||||
setIntervalType={(newInterval) => {
|
||||
setResponsiveSearchParams((v) => {
|
||||
const newParams = {
|
||||
...v,
|
||||
interval: newInterval,
|
||||
};
|
||||
return newParams;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ProductSKU } from "@/types/billing";
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
import { CheckIcon, DollarSignIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { CheckoutButton } from "@/components/billing/billing";
|
||||
import { RenewSubscriptionButton } from "@/components/billing/renew-subscription/renew-subscription-button";
|
||||
import { RenewSubscriptionButton } from "@/components/billing/renew-subscription-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
+3
-3
@@ -5,12 +5,12 @@ import { RefreshCwIcon } from "lucide-react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { reSubscribePlan } from "@/actions/billing";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { pollWithTimeout } from "../../../utils/pollWithTimeout";
|
||||
import { tryCatch } from "../../../utils/try-catch";
|
||||
import { pollWithTimeout } from "@/utils/pollWithTimeout";
|
||||
import { tryCatch } from "@/utils/try-catch";
|
||||
|
||||
export function RenewSubscriptionButton(props: {
|
||||
teamId: string;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { useState } from "react";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -9,11 +9,11 @@ import type { TokenMetadata } from "@/api/universal-bridge/tokens";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { fallbackChainIcon } from "@/constants/chain";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useTokensData } from "@/hooks/tokens";
|
||||
import { replaceIpfsUrl } from "@/lib/sdk";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fallbackChainIcon } from "@/utils/chain-icons";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
@@ -122,7 +122,10 @@ export function TokenSelector(props: {
|
||||
return option.label;
|
||||
}
|
||||
const resolvedSrc = token.iconUri
|
||||
? replaceIpfsUrl(token.iconUri, props.client)
|
||||
? resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: token.iconUri,
|
||||
})
|
||||
: fallbackChainIcon;
|
||||
|
||||
return (
|
||||
|
||||
@@ -95,7 +95,6 @@ export const CodeSegment: React.FC<CodeSegmentProps> = ({
|
||||
{!hideTabs && (
|
||||
<TabButtons
|
||||
hideBottomLine={!!onlyTabs}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabContainerClassName="px-3 pt-1.5 gap-0.5"
|
||||
tabIconClassName="size-4"
|
||||
tabs={environments.map((env) => ({
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { useState } from "react";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { FundWalletModal } from "./index";
|
||||
|
||||
const meta: Meta<typeof FundWalletModal> = {
|
||||
title: "Blocks/FundWalletModal",
|
||||
component: Variant,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<div className="p-10">
|
||||
<ConnectButton client={storybookThirdwebClient} />
|
||||
<div className="h-4" />
|
||||
<Story />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Test: Story = {
|
||||
args: {},
|
||||
};
|
||||
function Variant() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button type="button" onClick={() => setIsOpen(true)}>
|
||||
Open
|
||||
</Button>
|
||||
|
||||
<FundWalletModal
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
title="This is a title"
|
||||
description="This is a description"
|
||||
recipientAddress="0x83Dd93fA5D8343094f850f90B3fb90088C1bB425"
|
||||
client={storybookThirdwebClient}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { CheckoutWidget, useActiveWalletChain } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
reportFundWalletFailed,
|
||||
reportFundWalletSuccessful,
|
||||
} from "@/analytics/report";
|
||||
import { SingleNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { TokenSelector } from "@/components/blocks/TokenSelector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DecimalInput } from "@/components/ui/decimal-input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { WalletAddress } from "../wallet-address";
|
||||
|
||||
const formSchema = z.object({
|
||||
chainId: z.number({
|
||||
required_error: "Chain is required",
|
||||
}),
|
||||
token: z.object(
|
||||
{
|
||||
chainId: z.number(),
|
||||
address: z.string(),
|
||||
symbol: z.string(),
|
||||
name: z.string(),
|
||||
decimals: z.number(),
|
||||
},
|
||||
{
|
||||
required_error: "Token is required",
|
||||
},
|
||||
),
|
||||
amount: z
|
||||
.string()
|
||||
.min(1, "Amount is required")
|
||||
.refine((value) => {
|
||||
const num = Number(value);
|
||||
return !Number.isNaN(num) && num > 0;
|
||||
}, "Amount must be greater than 0"),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
type FundWalletModalProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
recipientAddress: string;
|
||||
client: ThirdwebClient;
|
||||
defaultChainId?: number;
|
||||
checkoutWidgetTitle?: string;
|
||||
};
|
||||
|
||||
export function FundWalletModal(props: FundWalletModalProps) {
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent className="p-0 gap-0 !w-full !max-w-md">
|
||||
<FundWalletModalContent
|
||||
open={props.open}
|
||||
onOpenChange={props.onOpenChange}
|
||||
title={props.title}
|
||||
description={props.description}
|
||||
recipientAddress={props.recipientAddress}
|
||||
client={props.client}
|
||||
defaultChainId={props.defaultChainId}
|
||||
checkoutWidgetTitle={props.checkoutWidgetTitle}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function FundWalletModalContent(props: FundWalletModalProps) {
|
||||
const [step, setStep] = useState<"form" | "checkout">("form");
|
||||
const activeChain = useActiveWalletChain();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const form = useForm<FormData>({
|
||||
defaultValues: {
|
||||
chainId: props.defaultChainId || activeChain?.id || 1,
|
||||
token: undefined,
|
||||
amount: "0.1",
|
||||
},
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const selectedChainId = form.watch("chainId");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{step === "form" ? (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(() => {
|
||||
setStep("checkout");
|
||||
})}
|
||||
>
|
||||
<DialogHeader className="p-4 lg:p-6">
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 px-4 lg:px-6 pb-8">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium"> Recipient</h3>
|
||||
<WalletAddress
|
||||
address={props.recipientAddress}
|
||||
client={props.client}
|
||||
className="py-1 h-auto"
|
||||
iconClassName="size-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chainId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chain</FormLabel>
|
||||
<FormControl>
|
||||
<SingleNetworkSelector
|
||||
className="bg-card"
|
||||
chainId={field.value}
|
||||
onChange={(token) => {
|
||||
field.onChange(token);
|
||||
form.resetField("token", {
|
||||
defaultValue: undefined,
|
||||
});
|
||||
}}
|
||||
client={props.client}
|
||||
placeholder="Select a chain"
|
||||
disableDeprecated
|
||||
disableTestnets
|
||||
disableChainId
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="token"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Token</FormLabel>
|
||||
<FormControl>
|
||||
<TokenSelector
|
||||
className="bg-card"
|
||||
disableAddress
|
||||
selectedToken={
|
||||
field.value
|
||||
? {
|
||||
chainId: field.value.chainId,
|
||||
address: field.value.address,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onChange={field.onChange}
|
||||
chainId={selectedChainId}
|
||||
client={props.client}
|
||||
placeholder="Select a token"
|
||||
enabled={!!selectedChainId}
|
||||
addNativeTokenIfMissing={true}
|
||||
showCheck={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<DecimalInput
|
||||
className="bg-card !text-xl h-auto font-semibold"
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="0.1"
|
||||
/>
|
||||
{form.watch("token") && (
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
|
||||
{form.watch("token").symbol}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" className="gap-2">
|
||||
Next
|
||||
<ArrowRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<div>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<CheckoutWidget
|
||||
client={props.client}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineChain(form.getValues("chainId"))}
|
||||
tokenAddress={form.getValues("token").address as `0x${string}`}
|
||||
amount={form.getValues("amount")}
|
||||
seller={props.recipientAddress as `0x${string}`}
|
||||
showThirdwebBranding={false}
|
||||
className="!w-full !max-w-full !min-w-0 !rounded-b-none !border-none"
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
name={props.checkoutWidgetTitle}
|
||||
onSuccess={() => {
|
||||
reportFundWalletSuccessful();
|
||||
}}
|
||||
onError={(error) => {
|
||||
reportFundWalletFailed({
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 p-4 lg:p-6 bg-card border-t rounded-b-lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setStep("form")}
|
||||
className="gap-2"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant="outline"
|
||||
onClick={() => props.onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { CrownIcon, LockIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { TeamPlanBadge } from "@/components/blocks/TeamPlanBadge";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -192,11 +192,11 @@ function WalletAvatar(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("size-6 overflow-hidden rounded-full", props.iconClassName)}
|
||||
className={cn("size-5 overflow-hidden rounded-full", props.iconClassName)}
|
||||
>
|
||||
{resolvedAvatarSrc ? (
|
||||
<Img
|
||||
className={cn("size-6 object-cover", props.iconClassName)}
|
||||
className={cn("size-5 object-cover", props.iconClassName)}
|
||||
src={resolvedAvatarSrc}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MessageCircleIcon, XIcon } from "lucide-react";
|
||||
import { useSelectedLayoutSegments } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useActiveWalletConnectionStatus } from "thirdweb/react";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThirdwebMiniLogo } from "../../../app/(app)/components/ThirdwebMiniLogo";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useFavoriteChainIds } from "@app/(dashboard)/(chain)/components/client/star-button";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -20,6 +19,7 @@ import { LazyConfigureNetworkModal } from "@/components/misc/configure-networks/
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { popularChains } from "@/constants/popularChains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
import {
|
||||
addRecentlyUsedChainId,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
} from "thirdweb/react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useEns } from "@/hooks/contract-hooks";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { shortenIfAddress } from "@/utils/usedapp-external";
|
||||
|
||||
interface PublisherHeaderProps {
|
||||
wallet: string;
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
export const PublisherHeader: React.FC<PublisherHeaderProps> = ({
|
||||
wallet,
|
||||
client,
|
||||
}) => {
|
||||
const ensQuery = useEns({
|
||||
addressOrEnsName: wallet,
|
||||
client,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="font-medium text-base mb-3">Published by</h4>
|
||||
<AccountProvider
|
||||
// passing zero address during loading time to prevent the component from crashing
|
||||
address={ensQuery.data?.address || ZERO_ADDRESS}
|
||||
client={client}
|
||||
>
|
||||
<div className="flex items-center gap-3 relative">
|
||||
<div className="translate-y-0.5">
|
||||
<AccountAvatar
|
||||
className="size-[34px] rounded-full border border-border border-solid object-cover"
|
||||
fallbackComponent={
|
||||
<AccountBlobbie className="size-[34px] rounded-full" />
|
||||
}
|
||||
loadingComponent={
|
||||
<Skeleton className="size-[34px] rounded-full" />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
className="hover:underline leading-none before:absolute before:inset-0"
|
||||
href={replaceDeployerAddress(
|
||||
`/${ensQuery.data?.ensName || wallet}`,
|
||||
)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<AccountName
|
||||
className="text-base leading-none font-medium"
|
||||
fallbackComponent={
|
||||
// When social profile API support other TLDs as well - we can remove this condition
|
||||
ensQuery.data?.ensName ? (
|
||||
<span className="text-base leading-none font-medium">
|
||||
{ensQuery.data?.ensName}
|
||||
</span>
|
||||
) : (
|
||||
<AccountAddress
|
||||
formatFn={(addr) =>
|
||||
shortenIfAddress(replaceDeployerAddress(addr))
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
formatFn={(name) => replaceDeployerAddress(name)}
|
||||
loadingComponent={<Skeleton className="h-6 w-40" />}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-1.5 leading-none">
|
||||
View all published contracts{" "}
|
||||
<ExternalLinkIcon className="size-3 text-muted-foreground" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccountProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import Image from "next/image";
|
||||
import type { FetchDeployMetadataResult } from "thirdweb/contract";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { replaceIpfsUrl } from "@/lib/sdk";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import generalContractIcon from "../../../../../public/assets/tw-icons/general.png";
|
||||
|
||||
type ContractIdImageProps = {
|
||||
@@ -29,13 +29,13 @@ export const ContractIdImage: React.FC<ContractIdImageProps> = ({
|
||||
className="size-8 rounded-full"
|
||||
src={
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY
|
||||
? replaceIpfsUrl(
|
||||
logo,
|
||||
getConfiguredThirdwebClient({
|
||||
? resolveSchemeWithErrorHandler({
|
||||
client: getConfiguredThirdwebClient({
|
||||
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
teamId: undefined,
|
||||
}),
|
||||
)
|
||||
uri: logo,
|
||||
})
|
||||
: logo
|
||||
}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import type { ProjectContract } from "@/api/getProjectContracts";
|
||||
import type { ProjectContract } from "@/api/project/getProjectContracts";
|
||||
import { BadgeContainer, storybookThirdwebClient } from "@/storybook/utils";
|
||||
import { ContractTableUI } from "./contract-table";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ProjectContract } from "@/api/getProjectContracts";
|
||||
import type { ProjectContract } from "@/api/project/getProjectContracts";
|
||||
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
|
||||
import { NetworkSelectDropdown } from "@/components/contract-components/tables/NetworkSelectDropdown";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
@@ -14,11 +14,11 @@ import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import * as ERC4337Ext from "thirdweb/extensions/erc4337";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { toFunctionSelector } from "thirdweb/utils";
|
||||
import { getContractFunctionsFromAbi } from "@/api/contract/getContractFunctionsFromAbi";
|
||||
import {
|
||||
type CodeEnvironment,
|
||||
CodeSegment,
|
||||
} from "@/components/blocks/code/code-segment.client";
|
||||
import { getContractFunctionsFromAbi } from "@/components/contract-components/getContractFunctionsFromAbi";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
@@ -657,7 +657,7 @@ export function CodeOverview(props: {
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-lg font-semibold tracking-tight">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
{isAccountFactory
|
||||
? "Direct contract interaction (advanced)"
|
||||
: chainInfo
|
||||
@@ -743,7 +743,6 @@ export function CodeOverview(props: {
|
||||
<div className="bg-background">
|
||||
<TabButtons
|
||||
tabs={tabList}
|
||||
tabClassName="!text-sm"
|
||||
tabContainerClassName="px-3 pt-2"
|
||||
/>
|
||||
</div>
|
||||
@@ -851,7 +850,7 @@ function AccentButton(props: {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
"text-sm justify-start h-auto py-1 px-2 text-muted-foreground",
|
||||
"text-sm justify-start h-auto py-1.5 px-2 text-muted-foreground font-normal font-mono",
|
||||
props.isActive && "text-foreground bg-accent",
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { moduleToBase64 } from "app/(app)/(dashboard)/published-contract/utils/module-base-64";
|
||||
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { fetchPublishedContractVersion } from "@/api/contract/fetch-contracts-with-versions";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { fetchPublishedContractVersion } from "@/components/contract-components/fetch-contracts-with-versions";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -11,7 +11,7 @@ import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { ContractPublisher } from "../../../../app/(app)/(dashboard)/explore/components/publisher";
|
||||
import { ContractPublisher } from "./contract-publisher";
|
||||
|
||||
interface ContractCardProps {
|
||||
publisher: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { fetchDeployMetadata } from "@/components/contract-components/fetchDeployMetadata";
|
||||
import { fetchDeployMetadata } from "@/api/contract/fetchDeployMetadata";
|
||||
import { ContractIdImage } from "@/components/contract-components/shared/contract-id-image";
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -169,8 +169,8 @@ function ContractFunctionInputs(props: { fn: AbiFunction | AbiEvent }) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center flex-wrap gap-2 border-b pb-3 mb-3">
|
||||
<h3 className="text-lg font-semibold">{fn.name}</h3>
|
||||
<div className="flex items-center flex-wrap gap-2 border-b pb-2.5 mb-3">
|
||||
<h3 className="text-base font-semibold">{fn.name}</h3>
|
||||
{isFunction && <Badge variant="success">{fn.stateMutability}</Badge>}
|
||||
</div>
|
||||
|
||||
@@ -206,7 +206,9 @@ function ContractFunctionInputs(props: { fn: AbiFunction | AbiEvent }) {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<p className="text-muted-foreground text-base">No inputs required</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,6 +224,8 @@ type ExtensionFunctions = {
|
||||
functions: AbiFunction[];
|
||||
};
|
||||
|
||||
type ReadOrWriteTab = "read" | "write";
|
||||
|
||||
export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
fnsOrEvents,
|
||||
contract,
|
||||
@@ -280,14 +284,21 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
return fnsOrEvents.filter((fn) => !("stateMutability" in fn)) as AbiEvent[];
|
||||
}, [fnsOrEvents]);
|
||||
|
||||
const defaultSelected = fnsOrEvents[0];
|
||||
|
||||
const [selectedFunction, setSelectedFunction] = useState<
|
||||
AbiFunction | AbiEvent | undefined
|
||||
>(fnsOrEvents[0]);
|
||||
>(defaultSelected);
|
||||
|
||||
const [_keywordSearch, setKeywordSearch] = useState<string>("");
|
||||
const [keywordSearch] = useDebounce(_keywordSearch, 150);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [activeTab, setActiveTab] = useState<ReadOrWriteTab>(() => {
|
||||
const isDefaultSelectedReadFunction = viewFunctions.find((e) =>
|
||||
e.functions.find((fn) => fn.name === defaultSelected?.name),
|
||||
);
|
||||
return isDefaultSelectedReadFunction ? "read" : "write";
|
||||
});
|
||||
|
||||
const functionSection = (e: ExtensionFunctions) => {
|
||||
const filteredFunctions = keywordSearch
|
||||
@@ -298,7 +309,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
|
||||
return (
|
||||
<div key={e.extension} className="pb-6">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="space-y-0.5">
|
||||
{selectedFunction &&
|
||||
filteredFunctions.map((fn) => (
|
||||
<FunctionsOrEventsListItem
|
||||
@@ -314,30 +325,29 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] gap-4 h-full">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[1fr_2fr] h-full bg-card rounded-lg border">
|
||||
{/* left */}
|
||||
<div className="bg-card rounded-lg border overflow-auto">
|
||||
<div className="overflow-auto border-b lg:border-b-0 lg:border-r">
|
||||
{(writeFunctions.length > 0 || viewFunctions.length > 0) && (
|
||||
<div className="flex flex-col h-full relative">
|
||||
<TabButtons
|
||||
tabContainerClassName="px-3 pt-2"
|
||||
tabClassName="!text-sm"
|
||||
tabs={[
|
||||
...(writeFunctions.length > 0
|
||||
? [
|
||||
{
|
||||
name: "Write",
|
||||
onClick: () => setActiveTab(0),
|
||||
isActive: activeTab === 0,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(viewFunctions.length > 0
|
||||
? [
|
||||
{
|
||||
name: "Read",
|
||||
onClick: () => setActiveTab(1),
|
||||
isActive: activeTab === 1,
|
||||
onClick: () => setActiveTab("read"),
|
||||
isActive: activeTab === "read",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(writeFunctions.length > 0
|
||||
? [
|
||||
{
|
||||
name: "Write",
|
||||
onClick: () => setActiveTab("write"),
|
||||
isActive: activeTab === "write",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -357,18 +367,19 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="h-auto overflow-auto pt-2 px-2">
|
||||
{activeTab === 0 &&
|
||||
{activeTab === "write" &&
|
||||
writeFunctions.length > 0 &&
|
||||
writeFunctions.map((e) => functionSection(e))}
|
||||
|
||||
{activeTab === 1 &&
|
||||
{activeTab === "read" &&
|
||||
viewFunctions.length > 0 &&
|
||||
viewFunctions.map((e) => functionSection(e))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{events.length > 0 && selectedFunction && (
|
||||
<div className="pt-2 px-4">
|
||||
<div className="p-4 space-y-0.5">
|
||||
{events.map((fn) => (
|
||||
<FunctionsOrEventsListItem
|
||||
fn={fn}
|
||||
@@ -382,7 +393,7 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* right */}
|
||||
<div className="bg-card rounded-lg border p-4 overflow-auto">
|
||||
<div className="overflow-auto p-4">
|
||||
{selectedFunction && (
|
||||
<ContractFunction
|
||||
contract={contract}
|
||||
@@ -414,7 +425,7 @@ const FunctionsOrEventsListItem: React.FC<FunctionsOrEventsListItemProps> = ({
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground font-mono w-full justify-start h-auto px-2 py-1",
|
||||
"text-muted-foreground hover:text-foreground font-mono w-full justify-start h-auto px-2 py-1.5",
|
||||
isActive && "text-foreground bg-accent",
|
||||
)}
|
||||
onClick={() => {
|
||||
|
||||
@@ -30,7 +30,6 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -414,14 +413,13 @@ export const InteractiveAbiFunction: React.FC<InteractiveAbiFunctionProps> = (
|
||||
)}
|
||||
|
||||
{error ? (
|
||||
<>
|
||||
<h3 className="text-sm font-medium">Error</h3>
|
||||
<InlineCode
|
||||
className="relative w-full whitespace-pre-wrap rounded-md border border-border p-4 text-red-500"
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
code={formatError(error as any)}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">Error</h3>
|
||||
<PlainTextCodeBlock
|
||||
className="text-red-500 bg-background"
|
||||
code={formatError(error)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
) : readLoading ? (
|
||||
<Skeleton className="h-20 w-full rounded-lg" />
|
||||
) : formattedResponseData ? (
|
||||
|
||||
+2
-97
@@ -1,14 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { CircleAlertIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { useId } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Team } from "@/api/team";
|
||||
|
||||
import { GradientAvatar } from "@/components/blocks/avatar/gradient-avatar";
|
||||
import { ProjectAvatar } from "@/components/blocks/avatar/project-avatar";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
@@ -17,98 +13,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { Fieldset } from "./common";
|
||||
|
||||
export type MinimalTeam = Pick<Team, "id" | "name" | "image" | "slug">;
|
||||
export type MinimalProject = Pick<Project, "id" | "name" | "image" | "slug">;
|
||||
|
||||
export type TeamAndProjectSelection = {
|
||||
team: MinimalTeam | undefined;
|
||||
project: MinimalProject | undefined;
|
||||
};
|
||||
|
||||
export type MinimalTeamsAndProjects = Array<{
|
||||
team: MinimalTeam;
|
||||
projects: MinimalProject[];
|
||||
}>;
|
||||
|
||||
type TeamAndProjectSelectorProps = {
|
||||
teamsAndProjects: MinimalTeamsAndProjects;
|
||||
selection: TeamAndProjectSelection;
|
||||
enabled: boolean;
|
||||
onSelectionChange: (selection: TeamAndProjectSelection) => void;
|
||||
client: ThirdwebClient;
|
||||
onSetEnabled: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export function AddToProjectCardUI(props: TeamAndProjectSelectorProps) {
|
||||
const selectedTeam = props.teamsAndProjects.find(
|
||||
(t) => t.team.id === props.selection.team?.id,
|
||||
);
|
||||
|
||||
const isProjectSelected = props.selection.project && props.enabled;
|
||||
|
||||
return (
|
||||
<Fieldset
|
||||
description="Save the deployed contract in a project's contract list on thirdweb dashboard"
|
||||
headerChildren={
|
||||
<div className="absolute top-4 right-4 lg:top-6 lg:right-6">
|
||||
<Checkbox
|
||||
checked={props.enabled}
|
||||
className="size-5"
|
||||
onCheckedChange={(checked) => {
|
||||
props.onSetEnabled(!!checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
headerClassName="pr-14"
|
||||
legend="Add To Project"
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Team & Project selectors */}
|
||||
{props.enabled && (
|
||||
<AddToProjectSelector
|
||||
client={props.client}
|
||||
onSelectionChange={props.onSelectionChange}
|
||||
selection={props.selection}
|
||||
teamsAndProjects={props.teamsAndProjects}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* No projects alert */}
|
||||
{selectedTeam?.projects.length === 0 && (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle> Selected team has no projects </AlertTitle>
|
||||
<AlertDescription>
|
||||
<UnderlineLink
|
||||
className="inline-flex items-center gap-2"
|
||||
href={`/team/${selectedTeam.team.slug}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Create Project <ExternalLinkIcon className="size-3.5" />
|
||||
</UnderlineLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* No project selected alert */}
|
||||
{!isProjectSelected && (
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5" />
|
||||
<AlertTitle>No Project Selected</AlertTitle>
|
||||
<AlertDescription>
|
||||
Deployed contract will not be saved in thirdweb dashboard
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</Fieldset>
|
||||
);
|
||||
}
|
||||
import type { MinimalTeamsAndProjects, TeamAndProjectSelection } from "./types";
|
||||
|
||||
function SlashSeparator() {
|
||||
return <div className="mx-2 h-5 w-[2px] rotate-[25deg] bg-foreground/20" />;
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
|
||||
export type MinimalTeam = Pick<Team, "id" | "name" | "image" | "slug">;
|
||||
export type MinimalProject = Pick<Project, "id" | "name" | "image" | "slug">;
|
||||
|
||||
export type TeamAndProjectSelection = {
|
||||
team: MinimalTeam | undefined;
|
||||
project: MinimalProject | undefined;
|
||||
};
|
||||
|
||||
export type MinimalTeamsAndProjects = Array<{
|
||||
team: MinimalTeam;
|
||||
projects: MinimalProject[];
|
||||
}>;
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { NFTMetadata } from "thirdweb/utils";
|
||||
import { NFTMediaWithEmptyState } from "@/components/blocks/nft-media";
|
||||
|
||||
interface MediaCellProps {
|
||||
cell: {
|
||||
value: NFTMetadata;
|
||||
};
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
|
||||
export const MediaCell: React.FC<MediaCellProps> = ({ cell, client }) => {
|
||||
const nftMetadata = cell.value;
|
||||
|
||||
return (
|
||||
<NFTMediaWithEmptyState
|
||||
className="pointer-events-none"
|
||||
client={client}
|
||||
height="120px"
|
||||
metadata={nftMetadata}
|
||||
requireInteraction
|
||||
width="120px"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,325 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
BookOpenTextIcon,
|
||||
CalendarDaysIcon,
|
||||
ExternalLinkIcon,
|
||||
PencilIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { download } from "thirdweb/storage";
|
||||
import invariant from "tiny-invariant";
|
||||
import { PublisherHeader } from "@/components//contract-components/publisher/publisher-header";
|
||||
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
|
||||
import type { PublishedContractWithVersion } from "@/components/contract-components/fetch-contracts-with-versions";
|
||||
import { ContractFunctionsOverview } from "@/components/contracts/functions/contract-functions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
usePublishedContractEvents,
|
||||
usePublishedContractFunctions,
|
||||
} from "@/hooks/contract-hooks";
|
||||
import { correctAndUniqueLicenses } from "@/lib/licenses";
|
||||
import { replaceIpfsUrl } from "@/lib/sdk";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ExtendedPublishedContract = PublishedContractWithVersion & {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
publisher?: string;
|
||||
tags?: string[];
|
||||
logo?: string;
|
||||
audit?: string;
|
||||
};
|
||||
|
||||
export function PublishedContract({
|
||||
publishedContract,
|
||||
isLoggedIn,
|
||||
client,
|
||||
className,
|
||||
}: {
|
||||
publishedContract: ExtendedPublishedContract;
|
||||
isLoggedIn: boolean;
|
||||
client: ThirdwebClient;
|
||||
className?: string;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
|
||||
const contractFunctions = usePublishedContractFunctions(publishedContract);
|
||||
const contractEvents = usePublishedContractEvents(publishedContract);
|
||||
|
||||
const publishDate = format(
|
||||
new Date(
|
||||
Number.parseInt(publishedContract?.publishTimestamp.toString() || "0") *
|
||||
1000,
|
||||
),
|
||||
"MMM dd, yyyy",
|
||||
);
|
||||
|
||||
const licenses = correctAndUniqueLicenses(publishedContract?.licenses || []);
|
||||
|
||||
const sources = useQuery({
|
||||
enabled: !!publishedContract.metadata.sources,
|
||||
queryFn: async () => {
|
||||
invariant(
|
||||
publishedContract.metadata.sources,
|
||||
"no compilerMetadata sources available",
|
||||
);
|
||||
return (await fetchSourceFilesFromMetadata(publishedContract, client))
|
||||
.map((source) => {
|
||||
return {
|
||||
...source,
|
||||
filename: source.filename.split("/").pop(),
|
||||
};
|
||||
})
|
||||
.slice()
|
||||
.reverse();
|
||||
},
|
||||
queryKey: ["sources", publishedContract.publishMetadataUri],
|
||||
});
|
||||
|
||||
const implementationAddresses =
|
||||
publishedContract.factoryDeploymentData?.implementationAddresses;
|
||||
|
||||
const factoryAddresses =
|
||||
publishedContract.factoryDeploymentData?.factoryAddresses;
|
||||
|
||||
const hasImplementationAddresses = useMemo(
|
||||
() => Object.values(implementationAddresses || {}).some((v) => v !== ""),
|
||||
[implementationAddresses],
|
||||
);
|
||||
|
||||
const hasFactoryAddresses = useMemo(
|
||||
() => Object.values(factoryAddresses || {}).some((v) => v !== ""),
|
||||
[factoryAddresses],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"py-8 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* left */}
|
||||
<div className="space-y-6">
|
||||
{address === publishedContract.publisher && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto gap-2"
|
||||
>
|
||||
<Link
|
||||
href={`/contracts/publish/${encodeURIComponent(
|
||||
publishedContract.publishMetadataUri.replace("ipfs://", ""),
|
||||
)}`}
|
||||
>
|
||||
<PencilIcon className="size-3" />
|
||||
Edit
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* readme */}
|
||||
{publishedContract?.readme && (
|
||||
<div className="overflow-hidden">
|
||||
<MarkdownRenderer markdownText={publishedContract?.readme} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* changelog */}
|
||||
{publishedContract?.changelog && (
|
||||
<div className="overflow-hidden">
|
||||
<div className="border-b border-dashed pb-3 mb-3">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{publishedContract?.version} Release Notes
|
||||
</h3>
|
||||
</div>
|
||||
<MarkdownRenderer
|
||||
markdownText={publishedContract?.changelog}
|
||||
p={{
|
||||
className: "text-sm leading-6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contractFunctions && (
|
||||
<ContractFunctionsOverview
|
||||
abi={publishedContract?.abi}
|
||||
events={contractEvents}
|
||||
functions={contractFunctions}
|
||||
isLoggedIn={isLoggedIn}
|
||||
sources={sources.data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* right */}
|
||||
<div className="space-y-6">
|
||||
{publishedContract.publisher && (
|
||||
<PublisherHeader
|
||||
client={client}
|
||||
wallet={publishedContract.publisher}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="border-t border-dashed" />
|
||||
|
||||
<ul className="space-y-6">
|
||||
{/* timestamp */}
|
||||
{publishedContract.publishTimestamp && (
|
||||
<li className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full border bg-card">
|
||||
<CalendarDaysIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-sm font-medium leading-none">
|
||||
Publish Date
|
||||
</h5>
|
||||
<p className="text-sm leading-none text-muted-foreground">
|
||||
{publishDate}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* audit */}
|
||||
{publishedContract?.audit && (
|
||||
<li>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full border bg-card">
|
||||
<ShieldCheckIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-sm font-medium leading-none">
|
||||
Audit Report
|
||||
</h5>
|
||||
<Link
|
||||
href={replaceIpfsUrl(publishedContract.audit, client)}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="flex items-center gap-2 text-sm hover:underline leading-none text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
View Audit Report
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{/* license */}
|
||||
<li className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full border bg-card">
|
||||
<BookOpenTextIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h5 className="text-sm font-medium leading-none">
|
||||
License{licenses.length > 1 ? "s" : ""}
|
||||
</h5>
|
||||
<p className="text-sm leading-none text-muted-foreground">
|
||||
{licenses.join(", ") || "None"}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{(publishedContract?.isDeployableViaProxy &&
|
||||
hasImplementationAddresses) ||
|
||||
(publishedContract?.isDeployableViaFactory && hasFactoryAddresses) ? (
|
||||
<li className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full border bg-card">
|
||||
<ServerIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<h5 className="text-sm font-medium">
|
||||
{publishedContract?.isDeployableViaFactory
|
||||
? "Factory"
|
||||
: "Proxy"}{" "}
|
||||
Enabled
|
||||
</h5>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
|
||||
<div className="border-t border-dashed" />
|
||||
|
||||
<Button
|
||||
asChild
|
||||
className="w-full gap-2 bg-card rounded-full"
|
||||
variant="outline"
|
||||
>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/tokens/publish/overview"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about Publish{" "}
|
||||
<ExternalLinkIcon className="size-3.5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ContractSource = {
|
||||
filename: string;
|
||||
source: string;
|
||||
};
|
||||
|
||||
async function fetchSourceFilesFromMetadata(
|
||||
publishedMetadata: ExtendedPublishedContract,
|
||||
client: ThirdwebClient,
|
||||
): Promise<ContractSource[]> {
|
||||
return await Promise.all(
|
||||
Object.entries(publishedMetadata.metadata.sources).map(
|
||||
async ([path, info]) => {
|
||||
const urls = "urls" in info ? info.urls : undefined;
|
||||
const ipfsLink = urls
|
||||
? urls.find((url) => url.includes("ipfs"))
|
||||
: undefined;
|
||||
if (ipfsLink) {
|
||||
const ipfsHash = ipfsLink.split("ipfs/")[1];
|
||||
// 3 sec timeout for sources that haven't been uploaded to ipfs
|
||||
const timeout = new Promise<string>((_resolve, reject) =>
|
||||
setTimeout(() => reject("timeout"), 3000),
|
||||
);
|
||||
const source = await Promise.race([
|
||||
(
|
||||
await download({
|
||||
client,
|
||||
uri: `ipfs://${ipfsHash}`,
|
||||
})
|
||||
).text(),
|
||||
timeout,
|
||||
]);
|
||||
return {
|
||||
filename: path,
|
||||
source,
|
||||
};
|
||||
}
|
||||
if ("content" in info) {
|
||||
return {
|
||||
filename: path,
|
||||
source: info.content || "Could not find source for this contract",
|
||||
};
|
||||
}
|
||||
return {
|
||||
filename: path,
|
||||
source: "Could not find source for this contract",
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,5 @@
|
||||
import type { Abi } from "abitype";
|
||||
import type { SourceFile } from "@/components/contract-components/types";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type { SourceFile } from "../types";
|
||||
|
||||
export function SourcesAccordion({
|
||||
sources,
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { Abi } from "abitype";
|
||||
import type { SourceFile } from "@/components/contract-components/types";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import type { SourceFile } from "../types";
|
||||
import { SourcesAccordion } from "./sources-accordion";
|
||||
|
||||
interface SourcesPanelProps {
|
||||
+36
-35
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { SearchIcon } from "lucide-react";
|
||||
import { SearchIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import type { SearchType } from "./types";
|
||||
|
||||
export function AdvancedSearchInput(props: {
|
||||
@@ -41,56 +41,57 @@ export function AdvancedSearchInput(props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={searchType}
|
||||
onValueChange={(value) => setSearchType(value as SearchType)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col md:flex-row gap-3">
|
||||
<Select
|
||||
value={searchType}
|
||||
onValueChange={(value) => setSearchType(value as SearchType)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-card">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="phone">Phone</SelectItem>
|
||||
<SelectItem value="id">ID</SelectItem>
|
||||
<SelectItem value="address">Address</SelectItem>
|
||||
<SelectItem value="externalWallet">External Wallet</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="flex flex-1">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-card pl-9"
|
||||
className="bg-card pl-9 border-r-0 rounded-r-none"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-3 size-4 text-muted-foreground" />
|
||||
|
||||
{props.hasResults && (
|
||||
<div className="absolute top-1/2 -translate-y-1/2 right-2 ">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleClear}
|
||||
className="p-1 h-auto"
|
||||
>
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSearch}
|
||||
variant="outline"
|
||||
disabled={!query.trim() || props.isLoading}
|
||||
size="sm"
|
||||
className="rounded-l-none gap-2 bg-card disabled:opacity-100"
|
||||
>
|
||||
{props.isLoading ? "Searching..." : "Search"}
|
||||
{props.isLoading && <Spinner className="size-4" />}
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{props.hasResults && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
className="gap-2"
|
||||
>
|
||||
Clear Search & Return to List
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+32
-9
@@ -12,6 +12,7 @@ import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import {
|
||||
ToolTipLabel,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
useAllEmbeddedWallets,
|
||||
useEmbeddedWallets,
|
||||
} from "@/hooks/useEmbeddedWallets";
|
||||
import { CopyTextButton } from "../ui/CopyTextButton";
|
||||
import { AdvancedSearchInput } from "./AdvancedSearchInput";
|
||||
import { SearchResults } from "./SearchResults";
|
||||
import { searchUsers } from "./searchUsers";
|
||||
@@ -57,7 +59,24 @@ export function InAppWalletUsersPageContent(
|
||||
columnHelper.accessor("linkedAccounts", {
|
||||
cell: (cell) => {
|
||||
const identifier = getUserIdentifier(cell.getValue());
|
||||
return <span className="text-sm">{identifier}</span>;
|
||||
|
||||
if (!identifier) {
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
textToShow={
|
||||
identifier.length > 30
|
||||
? `${identifier.slice(0, 30)}...`
|
||||
: identifier
|
||||
}
|
||||
textToCopy={identifier}
|
||||
tooltip="Copy User Identifier"
|
||||
copyIconPosition="left"
|
||||
variant="ghost"
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableColumnFilter: true,
|
||||
header: "User Identifier",
|
||||
@@ -77,7 +96,7 @@ export function InAppWalletUsersPageContent(
|
||||
cell: (cell) => {
|
||||
const externalWallets = getExternalWallets(cell.getValue());
|
||||
if (externalWallets.length === 0) {
|
||||
return <span className="text-muted-foreground text-xs">None</span>;
|
||||
return <span className="text-muted-foreground text-sm">None</span>;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
@@ -111,9 +130,14 @@ export function InAppWalletUsersPageContent(
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<span className="text-sm">
|
||||
{format(new Date(value), "MMM dd, yyyy")}
|
||||
</span>
|
||||
<ToolTipLabel
|
||||
label={format(new Date(value), "MMM dd, yyyy 'at' h:mm:ss a zzz")}
|
||||
hoverable
|
||||
>
|
||||
<span className="text-sm">
|
||||
{format(new Date(value), "MMM dd, yyyy")}
|
||||
</span>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
},
|
||||
header: "Created",
|
||||
@@ -215,7 +239,7 @@ export function InAppWalletUsersPageContent(
|
||||
return {
|
||||
address: row.wallets[0]?.address || "Uninitialized",
|
||||
created: row.wallets[0]?.createdAt
|
||||
? format(new Date(row.wallets[0].createdAt), "MMM dd, yyyy")
|
||||
? new Date(row.wallets[0].createdAt).toISOString()
|
||||
: "Wallet not created yet",
|
||||
external_wallets: externalWalletAddresses || "None",
|
||||
login_methods: row.linkedAccounts.map((acc) => acc.type).join(", "),
|
||||
@@ -243,7 +267,7 @@ export function InAppWalletUsersPageContent(
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Top section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<div className="flex flex-col md:flex-row lg:items-center justify-end gap-3">
|
||||
<div className="w-full max-w-lg">
|
||||
<AdvancedSearchInput
|
||||
onSearch={handleSearch}
|
||||
@@ -253,10 +277,9 @@ export function InAppWalletUsersPageContent(
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="gap-2"
|
||||
className="gap-2 bg-card"
|
||||
disabled={wallets.length === 0 || isPending}
|
||||
onClick={downloadCSV}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{isPending && <Spinner className="size-4" />}
|
||||
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { popularChains } from "@/constants/popularChains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useActiveChainAsDashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { useFavoriteChainIds } from "@/hooks/favorite-chains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
recentlyUsedChainIdsStore,
|
||||
type StoredChain,
|
||||
} from "@/stores/chainStores";
|
||||
import { useFavoriteChainIds } from "../../../app/(app)/(dashboard)/(chain)/components/client/star-button";
|
||||
import { mapV4ChainToV5Chain } from "../../utils/map-chains";
|
||||
import { getSDKTheme } from "../../utils/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "./CustomChainRenderer";
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ComponentWithChildren } from "@/types/component-with-children";
|
||||
|
||||
interface DelayedDisplayProps {
|
||||
delay: number;
|
||||
}
|
||||
|
||||
function useDelayedDisplay(delay: number) {
|
||||
const [displayContent, setDisplayContent] = useState(false);
|
||||
|
||||
// FIXME: this is a weird thing, we should not need it - in the meantime this is a legit use case
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDisplayContent(true);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [delay]);
|
||||
|
||||
return displayContent;
|
||||
}
|
||||
|
||||
export const DelayedDisplay: ComponentWithChildren<DelayedDisplayProps> = ({
|
||||
delay,
|
||||
children,
|
||||
}) => {
|
||||
const displayContent = useDelayedDisplay(delay);
|
||||
|
||||
if (!displayContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { getFaucetClaimAmount } from "@app/api/testnet-faucet/claim/claim-amount";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { FaucetButton } from "app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/FaucetButton";
|
||||
import { GiftIcon } from "app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/icons/GiftIcon";
|
||||
@@ -49,6 +48,7 @@ import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChainMetadataWithServices, ChainServices } from "@/types/chain";
|
||||
import { getFaucetClaimAmount } from "@/utils/faucet";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
|
||||
const GAS_FREE_CHAINS = [
|
||||
|
||||
@@ -79,7 +79,6 @@ export function DatePickerWithRange(props: {
|
||||
<div className={cn("px-4", !props.header && "py-4")}>
|
||||
<TabButtons
|
||||
activeTabClassName="!bg-inverted !text-inverted-foreground"
|
||||
tabClassName="!text-sm"
|
||||
tabContainerClassName="gap-2"
|
||||
tabs={[
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"bg-card relative w-full rounded-lg border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"bg-card relative w-full rounded-xl border border-border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
@@ -38,7 +38,7 @@ const AlertTitle = React.forwardRef<
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
className={cn("font-medium leading-tight tracking-tight", className)}
|
||||
className={cn("font-medium leading-tight", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
@@ -51,7 +51,7 @@ const AlertDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-1.5 text-muted-foreground text-sm [&_p]:leading-relaxed",
|
||||
"mt-1 text-muted-foreground text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -7,12 +7,15 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> & {
|
||||
size?: "sm";
|
||||
}
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
props.size === "sm" && "h-4.5 w-9",
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
@@ -20,6 +23,8 @@ const Switch = React.forwardRef<
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
props.size === "sm" &&
|
||||
"h-4 w-4 translate-x-4 data-[state=checked]:translate-x-4",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
|
||||
@@ -56,7 +56,7 @@ export function TabLinks(props: {
|
||||
<Link
|
||||
aria-disabled={tab.isDisabled}
|
||||
className={cn(
|
||||
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm lg:text-sm",
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg font-medium hover:bg-accent !px-3",
|
||||
!tab.isActive && !tab.isDisabled && "hover:text-foreground",
|
||||
tab.isDisabled && "pointer-events-none",
|
||||
tab.isActive && "!text-foreground",
|
||||
@@ -125,7 +125,7 @@ export function TabButtons(props: {
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg font-medium hover:bg-accent !px-3",
|
||||
!tab.isActive &&
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
tab.isDisabled && "cursor-not-allowed opacity-50",
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const COOKIE_ACTIVE_ACCOUNT = "tw_active_account";
|
||||
export const COOKIE_PREFIX_TOKEN = "tw_token_";
|
||||
|
||||
export async function getActiveAccountCookie() {
|
||||
return (await cookies()).get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
}
|
||||
|
||||
export async function getJWTCookie(address: string) {
|
||||
return (await cookies()).get(COOKIE_PREFIX_TOKEN + address)?.value;
|
||||
}
|
||||
export const LAST_USED_PROJECT_ID = "last-used-project-id";
|
||||
export const LAST_USED_TEAM_ID = "last-used-team-id";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const LAST_USED_PROJECT_ID = "last-used-project-id";
|
||||
export const LAST_USED_TEAM_ID = "last-used-team-id";
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Team } from "@/api/team";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
|
||||
// Note: Growth legacy is considered higher tier in this hierarchy
|
||||
export const planToTierRecordForGating: Record<Team["billingPlan"], number> = {
|
||||
|
||||
@@ -58,16 +58,6 @@ if (isProd && INSIGHT_SERVICE_API_KEY) {
|
||||
);
|
||||
}
|
||||
|
||||
export const MORALIS_API_KEY = process.env.MORALIS_API_KEY || "";
|
||||
|
||||
if (MORALIS_API_KEY) {
|
||||
experimental_taintUniqueValue(
|
||||
"Do not pass MORALIS_API_KEY to the client",
|
||||
process,
|
||||
MORALIS_API_KEY,
|
||||
);
|
||||
}
|
||||
|
||||
export const ANALYTICS_SERVICE_URL = process.env.ANALYTICS_SERVICE_URL || "";
|
||||
|
||||
export const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || "";
|
||||
|
||||
@@ -6,14 +6,14 @@ import { useMemo } from "react";
|
||||
import type { ThirdwebClient, ThirdwebContract } from "thirdweb";
|
||||
import { getContract, resolveContractAbi } from "thirdweb/contract";
|
||||
import { isAddress, isValidENSName } from "thirdweb/utils";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import {
|
||||
fetchPublishedContractVersions,
|
||||
type PublishedContractWithVersion,
|
||||
} from "../components/contract-components/fetch-contracts-with-versions";
|
||||
import { fetchPublishedContracts } from "../components/contract-components/fetchPublishedContracts";
|
||||
import { fetchPublishedContractsFromDeploy } from "../components/contract-components/fetchPublishedContractsFromDeploy";
|
||||
} from "@/api/contract/fetch-contracts-with-versions";
|
||||
import { fetchPublishedContracts } from "@/api/contract/fetchPublishedContracts";
|
||||
import { fetchPublishedContractsFromDeploy } from "@/api/contract/fetchPublishedContractsFromDeploy";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
|
||||
export function useAllVersions(
|
||||
publisherAddress: string | undefined,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
|
||||
async function favoriteChains() {
|
||||
const res = await apiServerProxy<{ data: string[] }>({
|
||||
method: "GET",
|
||||
pathname: "/v1/chains/favorites",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const result = res.data;
|
||||
return result.data ?? [];
|
||||
}
|
||||
|
||||
export function useFavoriteChainIds() {
|
||||
const address = useActiveAccount()?.address;
|
||||
return useQuery({
|
||||
enabled: !!address,
|
||||
queryFn: () => favoriteChains(),
|
||||
queryKey: ["favoriteChains", address],
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { Project } from "@/api/projects";
|
||||
import type { Project } from "@/api/project/projects";
|
||||
import { rotateVaultAccountAndAccessToken } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client";
|
||||
import { accountKeys, authorizedWallets } from "../query-keys/cache-keys";
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: we only want to run effect once */
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export function useEffectOnce(effect: () => void) {
|
||||
const hasCalledEffect = useRef(false);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (hasCalledEffect.current) return;
|
||||
hasCalledEffect.current = true;
|
||||
effect();
|
||||
}, []);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user