Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+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=""
|
||||
@@ -96,6 +96,12 @@ module.exports = {
|
||||
name: "posthog-js",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ["**/../@/**"],
|
||||
message: "Use absolute imports instead. Example: '@/foo/bar..'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -57,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",
|
||||
@@ -130,4 +129,4 @@
|
||||
"update-checkly": "npx checkly deploy"
|
||||
},
|
||||
"version": "3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+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: {
|
||||
|
||||
@@ -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,10 +9,10 @@ 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 { cn } from "@/lib/utils";
|
||||
import { fallbackChainIcon } from "@/utils/chain-icons";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
|
||||
|
||||
@@ -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
-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";
|
||||
@@ -743,7 +743,6 @@ export function CodeOverview(props: {
|
||||
<div className="bg-background">
|
||||
<TabButtons
|
||||
tabs={tabList}
|
||||
tabClassName="!text-sm"
|
||||
tabContainerClassName="px-3 pt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+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,
|
||||
|
||||
@@ -332,7 +332,6 @@ export const ContractFunctionsPanel: React.FC<ContractFunctionsPanelProps> = ({
|
||||
<div className="flex flex-col h-full relative">
|
||||
<TabButtons
|
||||
tabContainerClassName="px-3 pt-2"
|
||||
tabClassName="!text-sm"
|
||||
tabs={[
|
||||
...(viewFunctions.length > 0
|
||||
? [
|
||||
|
||||
@@ -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
-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";
|
||||
|
||||
@@ -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={[
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}, []);
|
||||
}
|
||||
@@ -5,62 +5,85 @@ import {
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { sendAndConfirmTransaction, type ThirdwebContract } from "thirdweb";
|
||||
import {
|
||||
type Chain,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
} from "thirdweb";
|
||||
import { distribute, distributeByToken } from "thirdweb/extensions/split";
|
||||
import { getOwnedTokens } from "thirdweb/insight";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { getTokenBalancesFromMoralis } from "@/actions/getBalancesFromMoralis";
|
||||
import { parseError } from "../utils/errorParser";
|
||||
|
||||
function getTokenBalancesQuery(contract: ThirdwebContract) {
|
||||
function getTokenBalancesQuery(params: {
|
||||
ownerAddress: string;
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
}) {
|
||||
return queryOptions({
|
||||
queryFn: async () => {
|
||||
const res = await getTokenBalancesFromMoralis({
|
||||
chainId: contract.chain.id,
|
||||
contractAddress: contract.address,
|
||||
return getOwnedTokens({
|
||||
client: params.client,
|
||||
chains: [params.chain],
|
||||
ownerAddress: params.ownerAddress,
|
||||
queryOptions: {
|
||||
include_native: "true",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.data) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
queryKey: ["split-balances", contract.chain.id, contract.address],
|
||||
queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress],
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSplitBalances(contract: ThirdwebContract) {
|
||||
return useQuery(getTokenBalancesQuery(contract));
|
||||
export function useOwnedTokenBalances(params: {
|
||||
ownerAddress: string;
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
}) {
|
||||
return useQuery(getTokenBalancesQuery(params));
|
||||
}
|
||||
|
||||
export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
const account = useActiveAccount();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const params = {
|
||||
ownerAddress: contract.address, // because we want to fetch the balance of split contract
|
||||
client: contract.client,
|
||||
chain: contract.chain,
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
invariant(account, "No active account");
|
||||
|
||||
const balances =
|
||||
// get the cached data if it exists, otherwise fetch it
|
||||
queryClient.getQueryData(getTokenBalancesQuery(contract).queryKey) ||
|
||||
(await queryClient.fetchQuery(getTokenBalancesQuery(contract)));
|
||||
queryClient.getQueryData(getTokenBalancesQuery(params).queryKey) ||
|
||||
(await queryClient.fetchQuery(getTokenBalancesQuery(params)));
|
||||
|
||||
const distributions = balances
|
||||
.filter((token) => token.display_balance !== "0.0")
|
||||
.filter((token) => token.value !== 0n)
|
||||
.map(async (currency) => {
|
||||
const transaction =
|
||||
currency.name === "Native Token"
|
||||
? distribute({ contract })
|
||||
: distributeByToken({
|
||||
contract,
|
||||
tokenAddress: currency.token_address,
|
||||
tokenAddress: currency.tokenAddress,
|
||||
});
|
||||
const promise = sendAndConfirmTransaction({
|
||||
account,
|
||||
transaction,
|
||||
});
|
||||
toast.promise(promise, {
|
||||
error: `Error distributing ${currency.name}`,
|
||||
error: (err) => ({
|
||||
message: `Error distributing ${currency.name}`,
|
||||
description: parseError(err),
|
||||
}),
|
||||
loading: `Distributing ${currency.name}`,
|
||||
success: `Successfully distributed ${currency.name}`,
|
||||
});
|
||||
@@ -69,7 +92,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
return await Promise.all(distributions);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(getTokenBalancesQuery(contract));
|
||||
queryClient.invalidateQueries(getTokenBalancesQuery(params));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import { getOwnedNFTs } from "thirdweb/insight";
|
||||
import invariant from "tiny-invariant";
|
||||
import { getWalletNFTs } from "@/actions/getWalletNFTs";
|
||||
|
||||
export function useWalletNFTs(params: {
|
||||
export function useOwnedNFTsInsight(params: {
|
||||
chainId: number;
|
||||
walletAddress?: string;
|
||||
isInsightSupported: boolean;
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
}) {
|
||||
return useQuery({
|
||||
enabled: !!params.walletAddress,
|
||||
queryFn: async () => {
|
||||
invariant(params.walletAddress, "walletAddress is required");
|
||||
return getWalletNFTs({
|
||||
chainId: params.chainId,
|
||||
isInsightSupported: params.isInsightSupported,
|
||||
owner: params.walletAddress,
|
||||
|
||||
if (!params.isInsightSupported) {
|
||||
throw new Error("Unsupported chain");
|
||||
}
|
||||
|
||||
const res = await getOwnedNFTs({
|
||||
client: params.client,
|
||||
chains: [params.chain],
|
||||
ownerAddress: params.walletAddress,
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
queryKey: ["walletNfts", params.chainId, params.walletAddress],
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { fallbackChainIcon } from "@/constants/chain";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fallbackChainIcon } from "@/utils/chain-icons";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
|
||||
type ImageProps = React.ComponentProps<"img">;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export function TokenIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M15.9377 15.9377C19.3603 15.4795 22 12.548 22 9C22 5.13401 18.866 2 15 2C11.452 2 8.52049 4.63967 8.06227 8.06227M16 15C16 18.866 12.866 22 9 22C5.13401 22 2 18.866 2 15C2 11.134 5.13401 8 9 8C12.866 8 16 11.134 16 15Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function WalletProductIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M16 7.99995V4.5006C16 3.66887 16 3.253 15.8248 2.99742C15.6717 2.77413 15.4346 2.62245 15.1678 2.57703C14.8623 2.52505 14.4847 2.69932 13.7295 3.04787L4.85901 7.14194C4.18551 7.45279 3.84875 7.60822 3.60211 7.84927C3.38406 8.06237 3.21762 8.3225 3.1155 8.60978C3 8.93474 3 9.30563 3 10.0474V14.9999M16.5 14.4999H16.51M3 11.2L3 17.7999C3 18.9201 3 19.4801 3.21799 19.9079C3.40973 20.2843 3.71569 20.5902 4.09202 20.782C4.51984 20.9999 5.07989 20.9999 6.2 20.9999H17.8C18.9201 20.9999 19.4802 21 19.908 20.782C20.2843 20.5902 20.5903 20.2843 20.782 19.9079C21 19.4801 21 18.9201 21 17.7999V11.2C21 10.0798 21 9.51979 20.782 9.09197C20.5903 8.71565 20.2843 8.40969 19.908 8.21794C19.4802 7.99995 18.9201 7.99995 17.8 7.99995L6.2 7.99995C5.0799 7.99995 4.51984 7.99995 4.09202 8.21794C3.7157 8.40968 3.40973 8.71564 3.21799 9.09197C3 9.51979 3 10.0798 3 11.2ZM17 14.4999C17 14.7761 16.7761 14.9999 16.5 14.9999C16.2239 14.9999 16 14.7761 16 14.4999C16 14.2238 16.2239 13.9999 16.5 13.9999C16.7761 13.9999 17 14.2238 17 14.4999Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import "server-only";
|
||||
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { NFTMetadata } from "thirdweb/utils";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { handleArbitraryTokenURI, shouldDownloadURI } from "./tokenUri";
|
||||
import {
|
||||
type AlchemySupportedChainId,
|
||||
alchemySupportedChainIdsMap,
|
||||
type GenerateURLParams,
|
||||
type WalletNFT,
|
||||
} from "./types";
|
||||
|
||||
export function generateAlchemyUrl({ chainId, owner }: GenerateURLParams) {
|
||||
const url = new URL(
|
||||
`https://${
|
||||
alchemySupportedChainIdsMap[chainId as AlchemySupportedChainId]
|
||||
}.g.alchemy.com/nft/v2/${process.env.SSR_ALCHEMY_KEY}/getNFTs`,
|
||||
);
|
||||
url.searchParams.set("owner", owner);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function transformAlchemyResponseToNFT(
|
||||
alchemyResponse: AlchemyResponse,
|
||||
owner: string,
|
||||
): Promise<WalletNFT[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
alchemyResponse.ownedNfts.map(async (alchemyNFT) => {
|
||||
const rawUri = alchemyNFT.tokenUri.raw;
|
||||
|
||||
try {
|
||||
return {
|
||||
contractAddress: alchemyNFT.contract.address,
|
||||
id: alchemyNFT.id.tokenId,
|
||||
metadata: shouldDownloadURI(rawUri)
|
||||
? await download({
|
||||
client: serverThirdwebClient,
|
||||
uri: handleArbitraryTokenURI(rawUri),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch(() => ({}))
|
||||
: rawUri,
|
||||
owner,
|
||||
supply: alchemyNFT.balance || "1",
|
||||
tokenURI: rawUri,
|
||||
type: alchemyNFT.id.tokenMetadata.tokenType,
|
||||
} as WalletNFT;
|
||||
} catch {
|
||||
return undefined as unknown as WalletNFT;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
type AlchemyResponse = {
|
||||
ownedNfts: Array<{
|
||||
contract: {
|
||||
address: string;
|
||||
};
|
||||
id: {
|
||||
tokenId: string;
|
||||
tokenMetadata: {
|
||||
tokenType: "ERC721" | "ERC1155";
|
||||
};
|
||||
};
|
||||
balance: string;
|
||||
title: string;
|
||||
description: string;
|
||||
tokenUri: {
|
||||
raw: string;
|
||||
gateway: string;
|
||||
};
|
||||
media: Array<{
|
||||
raw: string;
|
||||
gateway: string;
|
||||
}>;
|
||||
metadata: NFTMetadata;
|
||||
timeLastUpdated: string;
|
||||
contractMetadata: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
totalSupply: string;
|
||||
tokenType: "ERC721" | "ERC1155";
|
||||
};
|
||||
}>;
|
||||
pageKey: string;
|
||||
totalCount: number;
|
||||
blockHash: string;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
type AlchemySupportedChainId,
|
||||
alchemySupportedChainIds,
|
||||
} from "./types";
|
||||
|
||||
export function isAlchemySupported(
|
||||
chainId: number,
|
||||
): chainId is AlchemySupportedChainId {
|
||||
return alchemySupportedChainIds.includes(chainId.toString());
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {
|
||||
type MoralisSupportedChainId,
|
||||
moralisSupportedChainIds,
|
||||
} from "./types";
|
||||
|
||||
export function isMoralisSupported(
|
||||
chainId: number,
|
||||
): chainId is MoralisSupportedChainId {
|
||||
return moralisSupportedChainIds.includes(chainId.toString());
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import "server-only";
|
||||
|
||||
import { download } from "thirdweb/storage";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { handleArbitraryTokenURI, shouldDownloadURI } from "./tokenUri";
|
||||
import type { GenerateURLParams, WalletNFT } from "./types";
|
||||
|
||||
export function generateMoralisUrl({ chainId, owner }: GenerateURLParams) {
|
||||
const url = new URL(`https://deep-index.moralis.io/api/v2/${owner}/nft`);
|
||||
|
||||
url.searchParams.append("chain", `0x${chainId.toString(16)}`);
|
||||
url.searchParams.append("format", "hex");
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function transformMoralisResponseToNFT(
|
||||
moralisResponse: MoralisResponse,
|
||||
owner: string,
|
||||
chainId: number,
|
||||
): Promise<WalletNFT[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
moralisResponse.result.map(async (moralisNft) => {
|
||||
try {
|
||||
return {
|
||||
chainId,
|
||||
contractAddress: moralisNft.token_address,
|
||||
id: moralisNft.token_id,
|
||||
metadata: shouldDownloadURI(moralisNft.token_uri)
|
||||
? await download({
|
||||
client: serverThirdwebClient,
|
||||
uri: handleArbitraryTokenURI(moralisNft.token_uri),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch(() => ({}))
|
||||
: moralisNft.token_uri,
|
||||
owner,
|
||||
supply: moralisNft.amount || "1",
|
||||
tokenAddress: moralisNft.token_address,
|
||||
tokenId: moralisNft.token_id,
|
||||
tokenURI: moralisNft.token_uri,
|
||||
type: moralisNft.contract_type,
|
||||
} as WalletNFT;
|
||||
} catch {
|
||||
return undefined as unknown as WalletNFT;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
}
|
||||
|
||||
type MoralisResponse = {
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
cursor: string | null;
|
||||
result: Array<{
|
||||
token_address: string;
|
||||
token_id: string;
|
||||
owner_of: string;
|
||||
block_number: string;
|
||||
block_number_minted: string;
|
||||
token_hash: string;
|
||||
amount: string;
|
||||
contract_type: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
token_uri: `https://ipfs.moralis.io:2053/ipfs/${string}`;
|
||||
metadata: string;
|
||||
last_token_uri_sync: string;
|
||||
last_metadata_sync: string;
|
||||
}>;
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
export function handleArbitraryTokenURI(rawUri: string): string {
|
||||
if (rawUri.startsWith("ipfs://")) {
|
||||
return rawUri;
|
||||
}
|
||||
if (!rawUri.includes("ipfs")) {
|
||||
return rawUri;
|
||||
}
|
||||
const uriSplit = `ipfs://${rawUri.split("/ipfs/")}`;
|
||||
|
||||
return uriSplit[uriSplit.length - 1] || "";
|
||||
}
|
||||
|
||||
export function shouldDownloadURI(rawUri: string): boolean {
|
||||
return handleArbitraryTokenURI(rawUri).startsWith("ipfs://");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user