Compare commits
65
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34c5d24043 | ||
|
|
e612f16a85 | ||
|
|
4a30b9c89a | ||
|
|
ef9cf3409b | ||
|
|
fd20a6cabb | ||
|
|
9095041ea7 | ||
|
|
2dbb9e2de4 | ||
|
|
00b8adf3a2 | ||
|
|
ae3dfa36c3 | ||
|
|
eb603f8908 | ||
|
|
e11b0da20e | ||
|
|
4c51fe6a8f | ||
|
|
64034c0ddb | ||
|
|
90c1e576ff | ||
|
|
27d5a81c49 | ||
|
|
dbadc23e04 | ||
|
|
21d5d0b6af | ||
|
|
b2239c91c9 | ||
|
|
ca701a48c2 | ||
|
|
94aae902a1 | ||
|
|
0c7149eb44 | ||
|
|
ce9220ea00 | ||
|
|
99c2af5e25 | ||
|
|
7495f46222 | ||
|
|
b8afa98de6 | ||
|
|
496615feae | ||
|
|
e2931df858 | ||
|
|
85bde21dc9 | ||
|
|
2c84ac4089 | ||
|
|
5ee449fc9c | ||
|
|
a94e9c2396 | ||
|
|
64f5e474bb | ||
|
|
aa0d23e966 | ||
|
|
b888413ca6 | ||
|
|
108a79ab3e | ||
|
|
51390f3af0 | ||
|
|
fb8c95a325 | ||
|
|
cf02cbf3af | ||
|
|
f152139d73 | ||
|
|
16307019be | ||
|
|
10fb338877 | ||
|
|
95412c75b9 | ||
|
|
7bc513c770 | ||
|
|
9069351e1a | ||
|
|
8f6363f4e4 | ||
|
|
a83104efff | ||
|
|
7936ebb6e9 | ||
|
|
3b986d52e5 | ||
|
|
ee4e13e098 | ||
|
|
8dc9e4102d | ||
|
|
33fc3e5226 | ||
|
|
ed47e93f1d | ||
|
|
1f7cda6698 | ||
|
|
c26fc676c9 | ||
|
|
e1cccd7a10 | ||
|
|
3646964f2e | ||
|
|
db145b5621 | ||
|
|
db3838032a | ||
|
|
60021b3a44 | ||
|
|
a81899d60a | ||
|
|
b366d56039 | ||
|
|
e568bcea60 | ||
|
|
a94f22928a | ||
|
|
5249cb7409 | ||
|
|
b37ed57365 |
@@ -1,19 +0,0 @@
|
||||
name: Auto Author Assign
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review, draft]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
assign-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.pull_request.author_association == 'MEMBER' ||
|
||||
github.event.pull_request.author_association == 'OWNER' ||
|
||||
github.event.pull_request.author_association == 'COLLABORATOR' ||
|
||||
github.event.pull_request.author_association == 'CONTRIBUTOR'
|
||||
steps:
|
||||
- uses: toshimaru/auto-author-assign@16f0022cf3d7970c106d8d1105f75a1165edb516 # v2.1.1
|
||||
@@ -10,6 +10,7 @@ module.exports = [
|
||||
"/wallets",
|
||||
"/account-abstraction",
|
||||
"/payments",
|
||||
"/payments/x402",
|
||||
"/auth",
|
||||
"/in-app-wallets",
|
||||
"/transactions",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"ogl": "^1.0.11",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
|
||||
import { headers } from "next/headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import Stripe from "stripe";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import {
|
||||
@@ -88,6 +88,9 @@ export async function fetchClientSecret(team: Team) {
|
||||
throw new Error("No customer ID found");
|
||||
}
|
||||
|
||||
// try to get the gclid cookie
|
||||
const gclid = (await cookies()).get("gclid")?.value;
|
||||
|
||||
// Create Checkout Sessions from body params.
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
ui_mode: "embedded",
|
||||
@@ -117,6 +120,8 @@ export async function fetchClientSecret(team: Team) {
|
||||
missing_payment_method: "cancel",
|
||||
},
|
||||
},
|
||||
// if gclid exists, set it as a metadata field so we can attribute the conversion later
|
||||
metadata: gclid ? { gclid } : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ type AssetContractType =
|
||||
*/
|
||||
export function reportAssetBuySuccessful(properties: {
|
||||
chainId: number;
|
||||
contractType: AssetContractType;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
}) {
|
||||
posthog.capture("asset buy successful", {
|
||||
@@ -243,9 +243,51 @@ type TokenSwapParams = {
|
||||
pageType: "asset" | "bridge" | "chain";
|
||||
};
|
||||
|
||||
type TokenBuyParams = {
|
||||
buyTokenChainId: number | undefined;
|
||||
buyTokenAddress: string | undefined;
|
||||
pageType: "asset" | "bridge" | "chain";
|
||||
};
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of successful token swaps from the token page
|
||||
* - To track number of successful token buys
|
||||
* - To track which tokens are being bought the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportTokenBuySuccessful(properties: TokenBuyParams) {
|
||||
posthog.capture("token buy successful", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of failed token buys
|
||||
* - To track which token buys are failing
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportTokenBuyFailed(properties: TokenBuyParams) {
|
||||
posthog.capture("token buy failed", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of cancelled token buys
|
||||
* - To track which token buys are being cancelled
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportTokenBuyCancelled(properties: TokenBuyParams) {
|
||||
posthog.capture("token buy cancelled", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of successful token swaps
|
||||
* - To track which tokens are being swapped the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
@@ -271,7 +313,7 @@ export function reportSwapWidgetShown(properties: {
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of failed token swaps from the token page
|
||||
* - To track number of failed token swaps
|
||||
* - To track which tokens are being swapped the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
@@ -287,7 +329,7 @@ export function reportTokenSwapFailed(
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of cancelled token swaps from the token page
|
||||
* - To track number of cancelled token swaps
|
||||
* - To track which tokens are being swapped the most
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
@@ -299,7 +341,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of failed asset purchases from the token page
|
||||
* - To track number of failed asset purchases
|
||||
* - To track the errors that users encounter when trying to purchase an asset
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
@@ -307,7 +349,7 @@ export function reportTokenSwapCancelled(properties: TokenSwapParams) {
|
||||
*/
|
||||
export function reportAssetBuyFailed(properties: {
|
||||
chainId: number;
|
||||
contractType: AssetContractType;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
error: string;
|
||||
}) {
|
||||
@@ -329,7 +371,7 @@ export function reportAssetBuyFailed(properties: {
|
||||
*/
|
||||
export function reportAssetBuyCancelled(properties: {
|
||||
chainId: number;
|
||||
contractType: AssetContractType;
|
||||
contractType: AssetContractType | undefined;
|
||||
assetType: "nft" | "coin";
|
||||
}) {
|
||||
posthog.capture("asset buy cancelled", {
|
||||
@@ -649,3 +691,17 @@ export function reportProductFeedback(properties: {
|
||||
source: properties.source,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track conversions for the bridge page links
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*
|
||||
*/
|
||||
export function reportBridgePageLinkClick(params: {
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
}) {
|
||||
posthog.capture("bridge page link clicked", params);
|
||||
}
|
||||
|
||||
@@ -592,7 +592,7 @@ const cached_getUniversalBridgeUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
@@ -636,7 +636,7 @@ const cached_getUniversalBridgeWalletUsage = unstable_cache(
|
||||
if (res?.status !== 200) {
|
||||
const reason = await res?.text();
|
||||
console.error(
|
||||
`Failed to fetch universal bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
`Failed to fetch bridge wallet stats: ${res?.status} - ${res.statusText} - ${reason}`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { Mesh, Program, Renderer, Triangle, Vec2 } from "ogl";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const vertex = `
|
||||
attribute vec2 position;
|
||||
void main(){gl_Position=vec4(position,0.0,1.0);}
|
||||
`;
|
||||
|
||||
const fragment = `
|
||||
#ifdef GL_ES
|
||||
precision lowp float;
|
||||
#endif
|
||||
uniform vec2 uResolution;
|
||||
uniform float uTime;
|
||||
uniform float uHueShift;
|
||||
uniform float uNoise;
|
||||
uniform float uScan;
|
||||
uniform float uScanFreq;
|
||||
uniform float uWarp;
|
||||
uniform vec3 uBackgroundColor;
|
||||
uniform float uBackgroundOpacity;
|
||||
uniform float uPatternLightness;
|
||||
#define iTime uTime
|
||||
#define iResolution uResolution
|
||||
|
||||
vec4 buf[8];
|
||||
float rand(vec2 c){return fract(sin(dot(c,vec2(12.9898,78.233)))*43758.5453);}
|
||||
|
||||
mat3 rgb2yiq=mat3(0.299,0.587,0.114,0.596,-0.274,-0.322,0.211,-0.523,0.312);
|
||||
mat3 yiq2rgb=mat3(1.0,0.956,0.621,1.0,-0.272,-0.647,1.0,-1.106,1.703);
|
||||
|
||||
vec3 hueShiftRGB(vec3 col,float deg){
|
||||
vec3 yiq=rgb2yiq*col;
|
||||
float rad=radians(deg);
|
||||
float cosh=cos(rad),sinh=sin(rad);
|
||||
vec3 yiqShift=vec3(yiq.x,yiq.y*cosh-yiq.z*sinh,yiq.y*sinh+yiq.z*cosh);
|
||||
return clamp(yiq2rgb*yiqShift,0.0,1.0);
|
||||
}
|
||||
|
||||
vec4 sigmoid(vec4 x){return 1./(1.+exp(-x));}
|
||||
|
||||
vec4 cppn_fn(vec2 coordinate,float in0,float in1,float in2){
|
||||
buf[6]=vec4(coordinate.x,coordinate.y,0.3948333106474662+in0,0.36+in1);
|
||||
buf[7]=vec4(0.14+in2,sqrt(coordinate.x*coordinate.x+coordinate.y*coordinate.y),0.,0.);
|
||||
buf[0]=mat4(vec4(6.5404263,-3.6126034,0.7590882,-1.13613),vec4(2.4582713,3.1660357,1.2219609,0.06276096),vec4(-5.478085,-6.159632,1.8701609,-4.7742867),vec4(6.039214,-5.542865,-0.90925294,3.251348))*buf[6]+mat4(vec4(0.8473259,-5.722911,3.975766,1.6522468),vec4(-0.24321538,0.5839259,-1.7661959,-5.350116),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(0.21808943,1.1243913,-1.7969975,5.0294676);
|
||||
buf[1]=mat4(vec4(-3.3522482,-6.0612736,0.55641043,-4.4719114),vec4(0.8631464,1.7432913,5.643898,1.6106541),vec4(2.4941394,-3.5012043,1.7184316,6.357333),vec4(3.310376,8.209261,1.1355612,-1.165539))*buf[6]+mat4(vec4(5.24046,-13.034365,0.009859298,15.870829),vec4(2.987511,3.129433,-0.89023495,-1.6822904),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-5.9457836,-6.573602,-0.8812491,1.5436668);
|
||||
buf[0]=sigmoid(buf[0]);buf[1]=sigmoid(buf[1]);
|
||||
buf[2]=mat4(vec4(-15.219568,8.095543,-2.429353,-1.9381982),vec4(-5.951362,4.3115187,2.6393783,1.274315),vec4(-7.3145227,6.7297835,5.2473326,5.9411426),vec4(5.0796127,8.979051,-1.7278991,-1.158976))*buf[6]+mat4(vec4(-11.967154,-11.608155,6.1486754,11.237008),vec4(2.124141,-6.263192,-1.7050359,-0.7021966),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-4.17164,-3.2281182,-4.576417,-3.6401186);
|
||||
buf[3]=mat4(vec4(3.1832156,-13.738922,1.879223,3.233465),vec4(0.64300746,12.768129,1.9141049,0.50990224),vec4(-0.049295485,4.4807224,1.4733979,1.801449),vec4(5.0039253,13.000481,3.3991797,-4.5561905))*buf[6]+mat4(vec4(-0.1285731,7.720628,-3.1425676,4.742367),vec4(0.6393625,3.714393,-0.8108378,-0.39174938),vec4(0.,0.,0.,0.),vec4(0.,0.,0.,0.))*buf[7]+vec4(-1.1811101,-21.621881,0.7851888,1.2329718);
|
||||
buf[2]=sigmoid(buf[2]);buf[3]=sigmoid(buf[3]);
|
||||
buf[4]=mat4(vec4(5.214916,-7.183024,2.7228765,2.6592617),vec4(-5.601878,-25.3591,4.067988,0.4602802),vec4(-10.57759,24.286327,21.102104,37.546658),vec4(4.3024497,-1.9625226,2.3458803,-1.372816))*buf[0]+mat4(vec4(-17.6526,-10.507558,2.2587414,12.462782),vec4(6.265566,-502.75443,-12.642513,0.9112289),vec4(-10.983244,20.741234,-9.701768,-0.7635988),vec4(5.383626,1.4819539,-4.1911616,-4.8444734))*buf[1]+mat4(vec4(12.785233,-16.345072,-0.39901125,1.7955981),vec4(-30.48365,-1.8345358,1.4542528,-1.1118771),vec4(19.872723,-7.337935,-42.941723,-98.52709),vec4(8.337645,-2.7312303,-2.2927687,-36.142323))*buf[2]+mat4(vec4(-16.298317,3.5471997,-0.44300047,-9.444417),vec4(57.5077,-35.609753,16.163465,-4.1534753),vec4(-0.07470326,-3.8656476,-7.0901804,3.1523974),vec4(-12.559385,-7.077619,1.490437,-0.8211543))*buf[3]+vec4(-7.67914,15.927437,1.3207729,-1.6686112);
|
||||
buf[5]=mat4(vec4(-1.4109162,-0.372762,-3.770383,-21.367174),vec4(-6.2103205,-9.35908,0.92529047,8.82561),vec4(11.460242,-22.348068,13.625772,-18.693201),vec4(-0.3429052,-3.9905605,-2.4626114,-0.45033523))*buf[0]+mat4(vec4(7.3481627,-4.3661838,-6.3037653,-3.868115),vec4(1.5462853,6.5488915,1.9701879,-0.58291394),vec4(6.5858274,-2.2180402,3.7127688,-1.3730392),vec4(-5.7973905,10.134961,-2.3395722,-5.965605))*buf[1]+mat4(vec4(-2.5132585,-6.6685553,-1.4029363,-0.16285264),vec4(-0.37908727,0.53738135,4.389061,-1.3024765),vec4(-0.70647055,2.0111287,-5.1659346,-3.728635),vec4(-13.562562,10.487719,-0.9173751,-2.6487076))*buf[2]+mat4(vec4(-8.645013,6.5546675,-6.3944063,-5.5933375),vec4(-0.57783127,-1.077275,36.91025,5.736769),vec4(14.283112,3.7146652,7.1452246,-4.5958776),vec4(2.7192075,3.6021907,-4.366337,-2.3653464))*buf[3]+vec4(-5.9000807,-4.329569,1.2427121,8.59503);
|
||||
buf[4]=sigmoid(buf[4]);buf[5]=sigmoid(buf[5]);
|
||||
buf[6]=mat4(vec4(-1.61102,0.7970257,1.4675229,0.20917463),vec4(-28.793737,-7.1390953,1.5025433,4.656581),vec4(-10.94861,39.66238,0.74318546,-10.095605),vec4(-0.7229728,-1.5483948,0.7301322,2.1687684))*buf[0]+mat4(vec4(3.2547753,21.489103,-1.0194173,-3.3100595),vec4(-3.7316632,-3.3792162,-7.223193,-0.23685838),vec4(13.1804495,0.7916005,5.338587,5.687114),vec4(-4.167605,-17.798311,-6.815736,-1.6451967))*buf[1]+mat4(vec4(0.604885,-7.800309,-7.213122,-2.741014),vec4(-3.522382,-0.12359311,-0.5258442,0.43852118),vec4(9.6752825,-22.853785,2.062431,0.099892326),vec4(-4.3196306,-17.730087,2.5184598,5.30267))*buf[2]+mat4(vec4(-6.545563,-15.790176,-6.0438633,-5.415399),vec4(-43.591583,28.551912,-16.00161,18.84728),vec4(4.212382,8.394307,3.0958717,8.657522),vec4(-5.0237565,-4.450633,-4.4768,-5.5010443))*buf[3]+mat4(vec4(1.6985557,-67.05806,6.897715,1.9004834),vec4(1.8680354,2.3915145,2.5231109,4.081538),vec4(11.158006,1.7294737,2.0738268,7.386411),vec4(-4.256034,-306.24686,8.258898,-17.132736))*buf[4]+mat4(vec4(1.6889864,-4.5852966,3.8534803,-6.3482175),vec4(1.3543309,-1.2640043,9.932754,2.9079645),vec4(-5.2770967,0.07150358,-0.13962056,3.3269649),vec4(28.34703,-4.918278,6.1044083,4.085355))*buf[5]+vec4(6.6818056,12.522166,-3.7075126,-4.104386);
|
||||
buf[7]=mat4(vec4(-8.265602,-4.7027016,5.098234,0.7509808),vec4(8.6507845,-17.15949,16.51939,-8.884479),vec4(-4.036479,-2.3946867,-2.6055532,-1.9866527),vec4(-2.2167742,-1.8135649,-5.9759874,4.8846445))*buf[0]+mat4(vec4(6.7790847,3.5076547,-2.8191125,-2.7028968),vec4(-5.743024,-0.27844876,1.4958696,-5.0517144),vec4(13.122226,15.735168,-2.9397483,-4.101023),vec4(-14.375265,-5.030483,-6.2599335,2.9848232))*buf[1]+mat4(vec4(4.0950394,-0.94011575,-5.674733,4.755022),vec4(4.3809423,4.8310084,1.7425908,-3.437416),vec4(2.117492,0.16342592,-104.56341,16.949184),vec4(-5.22543,-2.994248,3.8350096,-1.9364246))*buf[2]+mat4(vec4(-5.900337,1.7946124,-13.604192,-3.8060522),vec4(6.6583457,31.911177,25.164474,91.81147),vec4(11.840538,4.1503043,-0.7314397,6.768467),vec4(-6.3967767,4.034772,6.1714606,-0.32874924))*buf[3]+mat4(vec4(3.4992442,-196.91893,-8.923708,2.8142626),vec4(3.4806502,-3.1846354,5.1725626,5.1804223),vec4(-2.4009497,15.585794,1.2863957,2.0252278),vec4(-71.25271,-62.441242,-8.138444,0.50670296))*buf[4]+mat4(vec4(-12.291733,-11.176166,-7.3474145,4.390294),vec4(10.805477,5.6337385,-0.9385842,-4.7348723),vec4(-12.869276,-7.039391,5.3029537,7.5436664),vec4(1.4593618,8.91898,3.5101583,5.840625))*buf[5]+vec4(2.2415268,-6.705987,-0.98861027,-2.117676);
|
||||
buf[6]=sigmoid(buf[6]);buf[7]=sigmoid(buf[7]);
|
||||
buf[0]=mat4(vec4(1.6794263,1.3817469,2.9625452,0.),vec4(-1.8834411,-1.4806935,-3.5924516,0.),vec4(-1.3279216,-1.0918057,-2.3124623,0.),vec4(0.2662234,0.23235129,0.44178495,0.))*buf[0]+mat4(vec4(-0.6299101,-0.5945583,-0.9125601,0.),vec4(0.17828953,0.18300213,0.18182953,0.),vec4(-2.96544,-2.5819945,-4.9001055,0.),vec4(1.4195864,1.1868085,2.5176322,0.))*buf[1]+mat4(vec4(-1.2584374,-1.0552157,-2.1688404,0.),vec4(-0.7200217,-0.52666044,-1.438251,0.),vec4(0.15345335,0.15196142,0.272854,0.),vec4(0.945728,0.8861938,1.2766753,0.))*buf[2]+mat4(vec4(-2.4218085,-1.968602,-4.35166,0.),vec4(-22.683098,-18.0544,-41.954372,0.),vec4(0.63792,0.5470648,1.1078634,0.),vec4(-1.5489894,-1.3075932,-2.6444845,0.))*buf[3]+mat4(vec4(-0.49252132,-0.39877754,-0.91366625,0.),vec4(0.95609266,0.7923952,1.640221,0.),vec4(0.30616966,0.15693925,0.8639857,0.),vec4(1.1825981,0.94504964,2.176963,0.))*buf[4]+mat4(vec4(0.35446745,0.3293795,0.59547555,0.),vec4(-0.58784515,-0.48177817,-1.0614829,0.),vec4(2.5271258,1.9991658,4.6846647,0.),vec4(0.13042648,0.08864098,0.30187556,0.))*buf[5]+mat4(vec4(-1.7718065,-1.4033192,-3.3355875,0.),vec4(3.1664357,2.638297,5.378702,0.),vec4(-3.1724713,-2.6107926,-5.549295,0.),vec4(-2.851368,-2.249092,-5.3013067,0.))*buf[6]+mat4(vec4(1.5203838,1.2212278,2.8404984,0.),vec4(1.5210563,1.2651345,2.683903,0.),vec4(2.9789467,2.4364579,5.2347264,0.),vec4(2.2270417,1.8825914,3.8028636,0.))*buf[7]+vec4(-1.5468478,-3.6171484,0.24762098,0.);
|
||||
buf[0]=sigmoid(buf[0]);
|
||||
return vec4(buf[0].x,buf[0].y,buf[0].z,1.);
|
||||
}
|
||||
|
||||
void mainImage(out vec4 fragColor,in vec2 fragCoord){
|
||||
vec2 uv=fragCoord/uResolution.xy*2.-1.;
|
||||
uv.y*=-1.;
|
||||
uv+=uWarp*vec2(sin(uv.y*6.283+uTime*0.5),cos(uv.x*6.283+uTime*0.5))*0.05;
|
||||
fragColor=cppn_fn(uv,0.1*sin(0.3*uTime),0.1*sin(0.69*uTime),0.1*sin(0.44*uTime));
|
||||
}
|
||||
|
||||
void main(){
|
||||
vec4 col;mainImage(col,gl_FragCoord.xy);
|
||||
col.rgb=hueShiftRGB(col.rgb,uHueShift);
|
||||
float scanline_val=sin(gl_FragCoord.y*uScanFreq)*0.5+0.5;
|
||||
col.rgb*=1.-(scanline_val*scanline_val)*uScan;
|
||||
col.rgb+=(rand(gl_FragCoord.xy+uTime)-0.5)*uNoise;
|
||||
|
||||
// Apply lightness control to pattern
|
||||
col.rgb = col.rgb * uPatternLightness;
|
||||
|
||||
// Use background color as base, then add pattern on top
|
||||
vec3 finalColor = uBackgroundColor + col.rgb * uBackgroundOpacity;
|
||||
|
||||
gl_FragColor=vec4(clamp(finalColor,0.0,1.0),1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
hueShift?: number;
|
||||
noiseIntensity?: number;
|
||||
scanlineIntensity?: number;
|
||||
speed?: number;
|
||||
scanlineFrequency?: number;
|
||||
warpAmount?: number;
|
||||
resolutionScale?: number;
|
||||
backgroundColor?: [number, number, number];
|
||||
backgroundOpacity?: number;
|
||||
patternLightness?: number;
|
||||
};
|
||||
|
||||
export function DarkVeil({
|
||||
hueShift = 0,
|
||||
noiseIntensity = 0,
|
||||
scanlineIntensity = 0,
|
||||
speed = 0.5,
|
||||
scanlineFrequency = 0,
|
||||
warpAmount = 0,
|
||||
resolutionScale = 1,
|
||||
backgroundColor = [0, 0, 0],
|
||||
backgroundOpacity = 1,
|
||||
patternLightness = 1.0,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
useEffect(() => {
|
||||
const canvas = ref.current as HTMLCanvasElement;
|
||||
const parent = canvas.parentElement as HTMLElement;
|
||||
|
||||
const renderer = new Renderer({
|
||||
dpr: Math.min(window.devicePixelRatio, 2),
|
||||
canvas,
|
||||
});
|
||||
|
||||
const gl = renderer.gl;
|
||||
const geometry = new Triangle(gl);
|
||||
|
||||
const program = new Program(gl, {
|
||||
vertex,
|
||||
fragment,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vec2() },
|
||||
uHueShift: { value: hueShift },
|
||||
uNoise: { value: noiseIntensity },
|
||||
uScan: { value: scanlineIntensity },
|
||||
uScanFreq: { value: scanlineFrequency },
|
||||
uWarp: { value: warpAmount },
|
||||
uBackgroundColor: { value: backgroundColor },
|
||||
uBackgroundOpacity: { value: backgroundOpacity },
|
||||
uPatternLightness: { value: patternLightness },
|
||||
},
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
const resize = () => {
|
||||
const w = parent.clientWidth,
|
||||
h = parent.clientHeight;
|
||||
renderer.setSize(w * resolutionScale, h * resolutionScale);
|
||||
program.uniforms.uResolution.value.set(w, h);
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const start = performance.now();
|
||||
let frame = 0;
|
||||
|
||||
const loop = () => {
|
||||
program.uniforms.uTime.value =
|
||||
((performance.now() - start) / 1000) * speed;
|
||||
program.uniforms.uHueShift.value = hueShift;
|
||||
program.uniforms.uNoise.value = noiseIntensity;
|
||||
program.uniforms.uScan.value = scanlineIntensity;
|
||||
program.uniforms.uScanFreq.value = scanlineFrequency;
|
||||
program.uniforms.uWarp.value = warpAmount;
|
||||
program.uniforms.uBackgroundColor.value = backgroundColor;
|
||||
program.uniforms.uBackgroundOpacity.value = backgroundOpacity;
|
||||
program.uniforms.uPatternLightness.value = patternLightness;
|
||||
renderer.render({ scene: mesh });
|
||||
frame = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
loop();
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
window.removeEventListener("resize", resize);
|
||||
};
|
||||
}, [
|
||||
hueShift,
|
||||
noiseIntensity,
|
||||
scanlineIntensity,
|
||||
speed,
|
||||
scanlineFrequency,
|
||||
warpAmount,
|
||||
resolutionScale,
|
||||
backgroundColor,
|
||||
backgroundOpacity,
|
||||
patternLightness,
|
||||
]);
|
||||
return <canvas ref={ref} className="w-full h-full block bg-background" />;
|
||||
}
|
||||
@@ -1,29 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Chain, ThirdwebClient } from "thirdweb";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { BuyWidget, SwapWidget } from "thirdweb/react";
|
||||
import {
|
||||
reportAssetBuyCancelled,
|
||||
reportAssetBuyFailed,
|
||||
reportAssetBuySuccessful,
|
||||
reportSwapWidgetShown,
|
||||
reportTokenBuyCancelled,
|
||||
reportTokenBuyFailed,
|
||||
reportTokenBuySuccessful,
|
||||
reportTokenSwapCancelled,
|
||||
reportTokenSwapFailed,
|
||||
reportTokenSwapSuccessful,
|
||||
} from "@/analytics/report";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID,
|
||||
NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID,
|
||||
NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID,
|
||||
} from "@/constants/public-envs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { getConfiguredThirdwebClient } from "../../constants/thirdweb.server";
|
||||
|
||||
type PageType = "asset" | "bridge" | "chain";
|
||||
|
||||
export function BuyAndSwapEmbed(props: {
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
tokenAddress: string | undefined;
|
||||
buyAmount: string | undefined;
|
||||
pageType: "asset" | "bridge" | "chain";
|
||||
pageType: PageType;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const [tab, setTab] = useState<"buy" | "swap">("swap");
|
||||
@@ -41,8 +51,23 @@ export function BuyAndSwapEmbed(props: {
|
||||
});
|
||||
}, [props.pageType]);
|
||||
|
||||
const client = useMemo(() => {
|
||||
return getConfiguredThirdwebClient({
|
||||
clientId:
|
||||
props.pageType === "asset"
|
||||
? NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID
|
||||
: props.pageType === "bridge"
|
||||
? NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID
|
||||
: props.pageType === "chain"
|
||||
? NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID
|
||||
: undefined,
|
||||
secretKey: undefined,
|
||||
teamId: undefined,
|
||||
});
|
||||
}, [props.pageType]);
|
||||
|
||||
return (
|
||||
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10">
|
||||
<div className="bg-card rounded-2xl border overflow-hidden flex flex-col relative z-10 shadow-xl min-w-0">
|
||||
<div className="flex gap-2.5 p-4 border-b border-dashed">
|
||||
<TabButton
|
||||
label="Swap"
|
||||
@@ -60,38 +85,87 @@ export function BuyAndSwapEmbed(props: {
|
||||
<BuyWidget
|
||||
amount={props.buyAmount || "1"}
|
||||
chain={props.chain}
|
||||
className="!rounded-2xl !w-full !border-none"
|
||||
className="!rounded-2xl !border-none"
|
||||
title=""
|
||||
client={props.client}
|
||||
client={client}
|
||||
connectOptions={{
|
||||
autoConnect: false,
|
||||
}}
|
||||
onError={(e) => {
|
||||
onError={(e, quote) => {
|
||||
const errorMessage = parseError(e);
|
||||
|
||||
reportTokenBuyFailed({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
});
|
||||
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyFailed({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: "DropERC20",
|
||||
error: errorMessage,
|
||||
contractType: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCancel={() => {
|
||||
onCancel={(quote) => {
|
||||
reportTokenBuyCancelled({
|
||||
buyTokenChainId:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenAddress:
|
||||
quote?.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote?.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
});
|
||||
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuyCancelled({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: "DropERC20",
|
||||
contractType: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSuccess={() => {
|
||||
onSuccess={(quote) => {
|
||||
reportTokenBuySuccessful({
|
||||
buyTokenChainId:
|
||||
quote.type === "buy"
|
||||
? quote.intent.destinationChainId
|
||||
: quote.type === "onramp"
|
||||
? quote.intent.chainId
|
||||
: undefined,
|
||||
buyTokenAddress:
|
||||
quote.type === "buy"
|
||||
? quote.intent.destinationTokenAddress
|
||||
: quote.type === "onramp"
|
||||
? quote.intent.tokenAddress
|
||||
: undefined,
|
||||
pageType: props.pageType,
|
||||
});
|
||||
|
||||
if (props.pageType === "asset") {
|
||||
reportAssetBuySuccessful({
|
||||
assetType: "coin",
|
||||
chainId: props.chain.id,
|
||||
contractType: "DropERC20",
|
||||
contractType: undefined,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -103,9 +177,9 @@ export function BuyAndSwapEmbed(props: {
|
||||
|
||||
{tab === "swap" && (
|
||||
<SwapWidget
|
||||
client={props.client}
|
||||
client={client}
|
||||
theme={themeObj}
|
||||
className="!rounded-2xl !border-none !w-full"
|
||||
className="!rounded-2xl !border-none"
|
||||
prefill={{
|
||||
// buy this token by default
|
||||
buyToken: {
|
||||
|
||||
@@ -30,6 +30,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
headerClassName?: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
customHeader?: React.ReactNode;
|
||||
// chart config
|
||||
@@ -70,6 +71,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
<Card className={props.className}>
|
||||
{props.header && (
|
||||
<CardHeader className={props.header.headerClassName}>
|
||||
{props.header.icon}
|
||||
<CardTitle className={cn("mb-2", props.header.titleClassName)}>
|
||||
{props.header.title}
|
||||
</CardTitle>
|
||||
|
||||
@@ -4,9 +4,10 @@ import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
|
||||
export function ToggleThemeButton() {
|
||||
export function ToggleThemeButton(props: { className?: string }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
@@ -15,7 +16,10 @@ export function ToggleThemeButton() {
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
className={cn(
|
||||
"h-auto w-auto rounded-full border border-border bg-background p-2",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function FaqSection(props: {
|
||||
faqs: Array<{ title: string; description: string }>;
|
||||
}) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-2xl md:text-3xl font-semibold mb-4 tracking-tight">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{props.faqs.map((faq, faqIndex) => (
|
||||
<FaqItem
|
||||
key={faq.title}
|
||||
title={faq.title}
|
||||
description={faq.description}
|
||||
className={cn(faqIndex === props.faqs.length - 1 && "border-b-0")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FaqItem(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpenn] = useState(false);
|
||||
const contentId = useId();
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className={cn("border-b border-dashed", props.className)}>
|
||||
<h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setIsOpenn(!isOpen)}
|
||||
aria-controls={contentId}
|
||||
aria-expanded={isOpen}
|
||||
className={cn(
|
||||
"w-full justify-between h-auto py-5 text-base text-muted-foreground hover:bg-transparent pl-0 text-wrap text-left gap-6",
|
||||
isOpen && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{props.title}
|
||||
<ChevronDownIcon
|
||||
className={cn(
|
||||
"size-4 shrink-0 transition-transform duration-200",
|
||||
isOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
"text-muted-foreground text-base leading-7 pb-6 max-w-4xl",
|
||||
!isOpen && "hidden",
|
||||
)}
|
||||
id={contentId}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
</DynamicHeight>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export function GridPatternEmbedContainer(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className=" sm:flex sm:justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 rounded-lg overflow-hidden relative">
|
||||
<div className="flex justify-center w-full sm:border sm:border-dashed sm:bg-accent/20 sm:py-12 sm:rounded-xl sm:overflow-hidden relative">
|
||||
<GridPattern
|
||||
width={30}
|
||||
height={30}
|
||||
@@ -17,7 +17,7 @@ export function GridPatternEmbedContainer(props: {
|
||||
"linear-gradient(to bottom right,white,transparent,transparent)",
|
||||
}}
|
||||
/>
|
||||
<div className="sm:w-[420px] z-10">{props.children}</div>
|
||||
<div className="z-10 min-w-0">{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function AdvancedSearchInput(props: {
|
||||
value={searchType}
|
||||
onValueChange={(value) => setSearchType(value as SearchType)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-card">
|
||||
<SelectTrigger className="w-[140px] bg-background rounded-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -61,7 +61,7 @@ export function AdvancedSearchInput(props: {
|
||||
<div className="flex flex-1">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
className="bg-card pl-9 border-r-0 rounded-r-none"
|
||||
className="bg-background pl-9 border-r-0 rounded-r-none rounded-l-full"
|
||||
placeholder={`Search by ${searchType}...`}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
@@ -86,7 +86,7 @@ export function AdvancedSearchInput(props: {
|
||||
onClick={handleSearch}
|
||||
variant="outline"
|
||||
disabled={!query.trim() || props.isLoading}
|
||||
className="rounded-l-none gap-2 bg-card disabled:opacity-100"
|
||||
className="rounded-l-none rounded-r-full gap-2 bg-background disabled:opacity-100"
|
||||
>
|
||||
{props.isLoading && <Spinner className="size-4" />}
|
||||
Search
|
||||
|
||||
+22
-9
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ArrowRightIcon, UserIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -264,11 +264,24 @@ export function InAppWalletUsersPageContent(
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Top section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col md:flex-row lg:items-center justify-end gap-3">
|
||||
<div className="w-full max-w-lg">
|
||||
<div className="overflow-hidden rounded-xl border border-border bg-card">
|
||||
<div className="flex flex-col lg:flex-row lg:justify-between p-4 lg:px-6 py-5 lg:items-center gap-5">
|
||||
<div>
|
||||
<div className="flex mb-3">
|
||||
<div className="p-2 rounded-full bg-background border border-border">
|
||||
<UserIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
User Wallets
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
View and manage your project's users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start lg:items-end gap-2.5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
|
||||
<div className="w-full lg:w-auto lg:min-w-[320px]">
|
||||
<AdvancedSearchInput
|
||||
onSearch={handleSearch}
|
||||
onClear={handleClearSearch}
|
||||
@@ -277,7 +290,7 @@ export function InAppWalletUsersPageContent(
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="gap-2 bg-card"
|
||||
className="gap-2 bg-background rounded-full"
|
||||
disabled={wallets.length === 0 || isPending}
|
||||
onClick={downloadCSV}
|
||||
variant="outline"
|
||||
@@ -298,11 +311,11 @@ export function InAppWalletUsersPageContent(
|
||||
data={wallets}
|
||||
isFetched={walletsQuery.isFetched}
|
||||
isPending={walletsQuery.isPending}
|
||||
tableContainerClassName="rounded-b-none"
|
||||
tableContainerClassName="rounded-none border-x-0 border-b-0"
|
||||
title="in-app wallets"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center gap-3 rounded-b-lg border border-t-0 bg-card p-6">
|
||||
<div className="flex justify-center gap-3 border-t bg-card p-6">
|
||||
<Button
|
||||
className="gap-2 bg-background"
|
||||
disabled={activePage === 1 || walletsQuery.isPending}
|
||||
|
||||
@@ -44,11 +44,12 @@ function AnnouncementBannerUI(props: {
|
||||
}
|
||||
|
||||
export function AnnouncementBanner() {
|
||||
return (
|
||||
<AnnouncementBannerUI
|
||||
href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
|
||||
label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
|
||||
trackingLabel="product-rebrand"
|
||||
/>
|
||||
);
|
||||
// return (
|
||||
// <AnnouncementBannerUI
|
||||
// href="https://blog.thirdweb.com/the-fastest-way-to-build-web3-applications/"
|
||||
// label="We have re-branded our Engine, Payments, and Connect products. Please read the full blog post for details on changes"
|
||||
// trackingLabel="product-rebrand"
|
||||
// />
|
||||
// );
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -32,3 +32,12 @@ export const NEXT_PUBLIC_DEMO_ENGINE_URL =
|
||||
|
||||
export const NEXT_PUBLIC_THIRDWEB_AI_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_AI_HOST || "https://nebula-api.thirdweb.com";
|
||||
|
||||
export const NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_BRIDGE_PAGE_CLIENT_ID;
|
||||
|
||||
export const NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_CHAIN_PAGE_CLIENT_ID;
|
||||
|
||||
export const NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_ASSET_PAGE_CLIENT_ID;
|
||||
|
||||
@@ -25,6 +25,7 @@ import { getVercelEnv } from "@/utils/vercel";
|
||||
export function getConfiguredThirdwebClient(options: {
|
||||
secretKey: string | undefined;
|
||||
teamId: string | undefined;
|
||||
clientId?: string;
|
||||
}): ThirdwebClient {
|
||||
if (getVercelEnv() !== "production") {
|
||||
// if not on production: run this when creating a client to set the domains
|
||||
@@ -77,7 +78,8 @@ export function getConfiguredThirdwebClient(options: {
|
||||
}
|
||||
|
||||
// During build time, provide fallbacks if credentials are missing
|
||||
const clientId = NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
|
||||
const clientId =
|
||||
options.clientId || NEXT_PUBLIC_DASHBOARD_CLIENT_ID || "dummy-build-client";
|
||||
const secretKey = options.secretKey || undefined;
|
||||
|
||||
return createThirdwebClient({
|
||||
|
||||
@@ -379,12 +379,27 @@ export function useEngineTransactions(params: {
|
||||
page?: number;
|
||||
status?: EngineStatus;
|
||||
};
|
||||
id?: string;
|
||||
}) {
|
||||
const { instanceUrl, autoUpdate, authToken } = params;
|
||||
const { instanceUrl, autoUpdate, authToken, id } = params;
|
||||
|
||||
return useQuery({
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: async () => {
|
||||
if (id) {
|
||||
const res = await fetch(`${instanceUrl}transaction/status/${id}`, {
|
||||
headers: getEngineRequestHeaders(authToken),
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
const transaction = (json.result as Transaction) || {};
|
||||
return {
|
||||
transactions: [transaction],
|
||||
totalCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const url = new URL(`${instanceUrl}transaction/get-all`);
|
||||
if (params.queryParams) {
|
||||
for (const key in params.queryParams) {
|
||||
@@ -402,7 +417,6 @@ export function useEngineTransactions(params: {
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
return (json.result as TransactionResponse) || {};
|
||||
},
|
||||
queryKey: engineKeys.transactions(instanceUrl, params),
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
type ChainSeo = {
|
||||
title: string;
|
||||
description: string;
|
||||
og: {
|
||||
title: string;
|
||||
description: string;
|
||||
site_name: string;
|
||||
url: string;
|
||||
};
|
||||
faqs: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
chain: {
|
||||
chainId: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
nativeCurrency: {
|
||||
name: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
testnet: boolean;
|
||||
is_deprecated: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchChainSeo = unstable_cache(
|
||||
async (chainId: number) => {
|
||||
const url = new URL(
|
||||
`https://seo-pages-generator-5814.zeet-nftlabs.zeet.app/chain/${chainId}`,
|
||||
);
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return res.json() as Promise<ChainSeo>;
|
||||
},
|
||||
["chain-seo"],
|
||||
{ revalidate: 60 * 60 * 24 }, // 24 hours
|
||||
);
|
||||
+1
-6
@@ -1,18 +1,13 @@
|
||||
"use client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { GridPatternEmbedContainer } from "@/components/blocks/grid-pattern-embed-container";
|
||||
import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
|
||||
export function BuyFundsSection(props: {
|
||||
chain: ChainMetadata;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<GridPatternEmbedContainer>
|
||||
<BuyAndSwapEmbed
|
||||
client={props.client}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain={defineDashboardChain(props.chain.chainId, props.chain)}
|
||||
buyAmount={undefined}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Chain Overview" />
|
||||
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
|
||||
<div className="grid grid-cols-1 gap-6 rounded-xl border bg-card p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-6">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
|
||||
+8
-6
@@ -1,4 +1,3 @@
|
||||
import { CircleCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadataWithServices } from "@/types/chain";
|
||||
import { products } from "../../../../components/server/products";
|
||||
@@ -24,15 +23,18 @@ export function SupportedProductsSection(props: {
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border"
|
||||
className="relative rounded-xl border bg-card p-4 hover:border-active-border"
|
||||
key={product.id}
|
||||
>
|
||||
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
|
||||
<product.icon className="mt-0.5 size-5 shrink-0" />
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<product.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<h3 className="mb-1">
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
className="before:absolute before:inset-0 text-base font-medium"
|
||||
href={product.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export function ExplorersSection(props: {
|
||||
key={explorer.url}
|
||||
>
|
||||
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 font-semibold text-base capitalize">
|
||||
<h3 className="mb-1 font-medium text-base capitalize">
|
||||
{explorer.name}
|
||||
</h3>
|
||||
<Link
|
||||
|
||||
+1
-3
@@ -6,9 +6,7 @@ export function PrimaryInfoItem(props: {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-medium text-base text-muted-foreground">
|
||||
{props.title}
|
||||
</h3>
|
||||
<h3 className="text-base text-muted-foreground">{props.title}</h3>
|
||||
{props.titleIcon}
|
||||
</div>
|
||||
{props.children}
|
||||
|
||||
+36
-22
@@ -22,39 +22,51 @@ import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
|
||||
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
|
||||
import { StarButton } from "../../components/client/star-button";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
import { ChainPageView } from "./components/client/chain-pageview";
|
||||
import { ChainHeader } from "./components/server/chain-header";
|
||||
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
type Params = Promise<{ chain_id: string }>;
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ chain_id: string }>;
|
||||
}): Promise<Metadata> {
|
||||
params: Params;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const params = await props.params;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
const title = `${sanitizedChainName}: RPC and Chain Settings`;
|
||||
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
|
||||
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
|
||||
chain.testnet && chain.faucets?.length ? "faucet options" : "more"
|
||||
}.`;
|
||||
if (!chainSeo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
description,
|
||||
title: chainSeo.title,
|
||||
description: chainSeo.description,
|
||||
metadataBase: new URL("https://thirdweb.com"),
|
||||
openGraph: {
|
||||
description,
|
||||
title,
|
||||
title: chainSeo.og.title,
|
||||
description: chainSeo.og.description,
|
||||
siteName: "thirdweb",
|
||||
type: "website",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
twitter: {
|
||||
title: chainSeo.og.title,
|
||||
description: chainSeo.og.description,
|
||||
card: "summary_large_image",
|
||||
creator: "@thirdweb",
|
||||
site: "@thirdweb",
|
||||
},
|
||||
title,
|
||||
};
|
||||
}
|
||||
|
||||
// this is the dashboard layout file
|
||||
export default async function ChainPageLayout(props: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ chain_id: string }>;
|
||||
params: Params;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const { children } = props;
|
||||
@@ -67,8 +79,10 @@ export default async function ChainPageLayout(props: {
|
||||
redirect(chain.slug);
|
||||
}
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(chain.chainId);
|
||||
const chainSeo = await fetchChainSeo(chain.chainId);
|
||||
const client = getClientThirdwebClient(undefined);
|
||||
const description = customChainMetadata?.about || chainSeo?.description;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col">
|
||||
@@ -123,7 +137,7 @@ export default async function ChainPageLayout(props: {
|
||||
<ChainHeader
|
||||
chain={chain}
|
||||
client={client}
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
headerImageUrl={customChainMetadata?.headerImgUrl}
|
||||
logoUrl={chain.icon?.url}
|
||||
/>
|
||||
|
||||
@@ -131,7 +145,7 @@ export default async function ChainPageLayout(props: {
|
||||
|
||||
<div className="flex flex-col gap-3 md:gap-2">
|
||||
{/* Gas Sponsored badge - Mobile */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
{customChainMetadata?.gasSponsored && (
|
||||
<div className="flex md:hidden">
|
||||
<GasSponsoredBadge />
|
||||
</div>
|
||||
@@ -153,7 +167,7 @@ export default async function ChainPageLayout(props: {
|
||||
)}
|
||||
|
||||
{/* Gas Sponsored badge - Desktop */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
{customChainMetadata?.gasSponsored && (
|
||||
<div className="hidden md:block">
|
||||
<GasSponsoredBadge />
|
||||
</div>
|
||||
@@ -161,9 +175,9 @@ export default async function ChainPageLayout(props: {
|
||||
</div>
|
||||
|
||||
{/* description */}
|
||||
{chainMetadata?.about && (
|
||||
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base">
|
||||
{chainMetadata.about}
|
||||
{description && (
|
||||
<p className="mb-2 whitespace-pre-line text-muted-foreground text-sm lg:text-base text-pretty max-w-3xl">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { useId } from "react";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
|
||||
@@ -18,8 +17,8 @@ export const size = {
|
||||
export const contentType = "image/png";
|
||||
|
||||
const TWLogo: React.FC = () => {
|
||||
const clipPathId = useId();
|
||||
const linearGradientId = useId();
|
||||
const clipPathId = "og-tw-logo-clip-path";
|
||||
const linearGradientId = "og-tw-logo-linear-gradient";
|
||||
return (
|
||||
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
|
||||
<svg
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { getRawAccount } from "@/api/account/get-account";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { getChain, getCustomChainMetadata } from "../../utils";
|
||||
import { fetchChainSeo } from "./apis/chain-seo";
|
||||
import { BuyFundsSection } from "./components/client/BuyFundsSection";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
@@ -9,22 +11,27 @@ import { ExplorersSection } from "./components/server/explorer-section";
|
||||
import { FaucetSection } from "./components/server/FaucetSection";
|
||||
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
|
||||
|
||||
export default async function Page(props: {
|
||||
type Props = {
|
||||
params: Promise<{ chain_id: string }>;
|
||||
}) {
|
||||
};
|
||||
|
||||
export default async function Page(props: Props) {
|
||||
const params = await props.params;
|
||||
const chain = await getChain(params.chain_id);
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(Number(params.chain_id));
|
||||
const chainSeo = await fetchChainSeo(Number(chain.chainId)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
const client = getClientThirdwebClient();
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const account = await getRawAccount();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Custom CTA */}
|
||||
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
|
||||
<ChainCTA {...chainMetadata.cta} />
|
||||
{(customChainMetadata?.cta?.title ||
|
||||
customChainMetadata?.cta?.description) && (
|
||||
<ChainCTA {...customChainMetadata.cta} />
|
||||
)}
|
||||
|
||||
{/* Deprecated Alert */}
|
||||
@@ -44,7 +51,7 @@ export default async function Page(props: {
|
||||
{chain.testnet ? (
|
||||
<FaucetSection chain={chain} client={client} isLoggedIn={!!account} />
|
||||
) : chain.services.find((c) => c.service === "pay" && c.enabled) ? (
|
||||
<BuyFundsSection chain={chain} client={client} />
|
||||
<BuyFundsSection chain={chain} />
|
||||
) : null}
|
||||
|
||||
{/* Chain Overview */}
|
||||
@@ -58,6 +65,12 @@ export default async function Page(props: {
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
|
||||
{chainSeo?.faqs && chainSeo.faqs.length > 0 && (
|
||||
<div className="py-10">
|
||||
<FaqSection faqs={chainSeo.faqs} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-1
@@ -67,7 +67,6 @@ export async function getContractUniqueWalletAnalytics(params: {
|
||||
}
|
||||
|
||||
const json = (await res.json()) as InsightResponse;
|
||||
console.log("wallet analytics json", json);
|
||||
const aggregations = Object.values(json.aggregations[0]);
|
||||
|
||||
const returnValue: TransactionAnalyticsEntry[] = [];
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ export function NFTGetAllTable({
|
||||
includeOwners: true,
|
||||
start: currentPage * pageSize,
|
||||
tokenByIndex,
|
||||
useIndexer: false, // TEMPORARY, TODO: Remove when this is fixed in insight
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import { useId } from "react";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { API_ROUTES_CLIENT_ID } from "@/constants/server-envs";
|
||||
@@ -62,8 +61,9 @@ function shortenString(str: string) {
|
||||
return `${str.substring(0, 7)}...${str.substring(str.length - 5)}`;
|
||||
}
|
||||
|
||||
const gradientId = "og-brand-icon-gradient";
|
||||
|
||||
const OgBrandIcon: React.FC = () => {
|
||||
const gradientId = useId();
|
||||
return (
|
||||
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
|
||||
<svg
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { PublicPageConnectButton } from "./PublicPageConnectButton";
|
||||
|
||||
export function PageHeader(props: { containerClassName?: string }) {
|
||||
return (
|
||||
<div className="border-b bg-card">
|
||||
<div className="border-b">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-8xl justify-between py-3",
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export function ContractHeaderUI(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col lg:flex-row lg:items-center gap-5 py-6 relative",
|
||||
"flex flex-col lg:flex-row lg:items-center gap-5 py-8 relative",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
+9
-3
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
ArrowRightIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
@@ -73,8 +74,13 @@ function RecentTransfersUI(props: {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-lg">
|
||||
<h2 className="font-semibold text-2xl tracking-tight mb-0.5">
|
||||
<div className="p-4 lg:p-6 bg-card border rounded-b-none border-b-0 rounded-xl">
|
||||
<div className="flex mb-3">
|
||||
<div className="rounded-full border p-2 bg-background">
|
||||
<ArrowLeftRightIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight mb-1">
|
||||
Recent Transfers
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
@@ -194,7 +200,7 @@ function RecentTransfersUI(props: {
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 rounded-b-lg border-x border-b bg-card px-6 py-5">
|
||||
<div className="flex items-center justify-end gap-3 rounded-b-xl border-x border-b bg-card px-6 py-5">
|
||||
<Button
|
||||
className="gap-1.5 bg-background rounded-full"
|
||||
disabled={props.page === 0 || props.isPending}
|
||||
|
||||
+9
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart3Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
|
||||
import {
|
||||
@@ -35,10 +36,18 @@ export function ContractAnalyticsOverview(props: {
|
||||
margin={{
|
||||
bottom: 20,
|
||||
}}
|
||||
className="rounded-xl"
|
||||
header={{
|
||||
headerClassName: "p-4 lg:p-6",
|
||||
title: "Analytics",
|
||||
titleClassName: "font-semibold text-2xl mb-0.5 tracking-tight",
|
||||
icon: (
|
||||
<div className="flex mb-3">
|
||||
<div className="rounded-full border p-2 bg-background">
|
||||
<BarChart3Icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
description:
|
||||
"View trends of transactions, events and unique wallets interacting with this contract over time",
|
||||
}}
|
||||
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
export const mapChainIdToDexScreenerChainSlug = {
|
||||
8453: "base",
|
||||
1: "ethereum",
|
||||
56: "bsc",
|
||||
369: "pulsechain",
|
||||
137: "polygon",
|
||||
2741: "abstract",
|
||||
43114: "avalanche",
|
||||
999: "hyperliquid",
|
||||
480: "worldchain",
|
||||
42161: "arbitrum",
|
||||
388: "cronos",
|
||||
59144: "linea",
|
||||
1514: "story",
|
||||
397: "near",
|
||||
295: "hedera",
|
||||
146: "sonic",
|
||||
10: "optimism",
|
||||
80094: "berachain",
|
||||
57073: "ink",
|
||||
130: "unichain",
|
||||
5000: "mantle",
|
||||
324: "zksync",
|
||||
466: "apechain",
|
||||
1116: "core",
|
||||
250: "fantom",
|
||||
1868: "soneium",
|
||||
81457: "blast",
|
||||
2000: "dogechain",
|
||||
14: "flare",
|
||||
2040: "vana",
|
||||
4337: "beam",
|
||||
109: "shibarium",
|
||||
747: "flowevm",
|
||||
1088: "metis",
|
||||
1030: "conflux",
|
||||
43113: "avalanchedfk",
|
||||
534352: "scroll",
|
||||
747474: "katana",
|
||||
42220: "celo",
|
||||
1284: "moonbeam",
|
||||
4200: "merlinchain",
|
||||
2222: "kava",
|
||||
39797: "energi",
|
||||
34443: "mode",
|
||||
252: "fraxtal",
|
||||
48900: "zircuit",
|
||||
20: "elastos",
|
||||
100: "gnosischain",
|
||||
204: "opbnb",
|
||||
169: "manta",
|
||||
1313161554: "aurora",
|
||||
3073: "movement",
|
||||
4689: "iotex",
|
||||
23294: "oasissapphire",
|
||||
6001: "bouncebit",
|
||||
42170: "arbitrumnova",
|
||||
1101: "polygonzkevm",
|
||||
40: "telos",
|
||||
592: "astar",
|
||||
42262: "oasisemerald",
|
||||
1285: "moonriver",
|
||||
245022934: "neonevm",
|
||||
7777777: "zora",
|
||||
122: "fuse",
|
||||
321: "kcc",
|
||||
1234: "stepnetwork",
|
||||
106: "velas",
|
||||
167000: "taiko",
|
||||
288: "boba",
|
||||
42766: "zkfair",
|
||||
32520: "bitgert",
|
||||
82: "meter",
|
||||
} as const;
|
||||
|
||||
export type DexScreenerChainSlug =
|
||||
(typeof mapChainIdToDexScreenerChainSlug)[keyof typeof mapChainIdToDexScreenerChainSlug];
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
import { Spinner } from "@workspace/ui/components/spinner";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import type { DexScreenerChainSlug } from "./dex-screener-chains";
|
||||
|
||||
function DexScreenerIframe(props: {
|
||||
chain: DexScreenerChainSlug;
|
||||
contractAddress: string;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const iframeUrl = useMemo(() => {
|
||||
const resolvedTheme = theme === "light" ? "light" : "dark";
|
||||
const url = new URL("https://dexscreener.com");
|
||||
url.pathname = `${props.chain}/${props.contractAddress}`;
|
||||
url.searchParams.set("embed", "1");
|
||||
url.searchParams.set("loadChartSettings", "0");
|
||||
url.searchParams.set("chartTheme", resolvedTheme);
|
||||
url.searchParams.set("theme", resolvedTheme);
|
||||
url.searchParams.set("trades", "1");
|
||||
url.searchParams.set("chartStyle", "1");
|
||||
url.searchParams.set("chartLeftToolbar", "0");
|
||||
url.searchParams.set("chartType", "usd");
|
||||
url.searchParams.set("interval", "15");
|
||||
url.searchParams.set("chartDefaultOnMobile", "1");
|
||||
return url.toString();
|
||||
}, [theme, props.chain, props.contractAddress]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
className="w-full h-[500px] lg:h-[1000px] rounded-lg overflow-hidden border bg-card"
|
||||
src={iframeUrl}
|
||||
title="DexScreener"
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
|
||||
export function DexScreener(props: {
|
||||
chain: DexScreenerChainSlug;
|
||||
contractAddress: string;
|
||||
}) {
|
||||
return (
|
||||
<ClientOnly
|
||||
ssr={
|
||||
<div className="w-full h-[500px] lg:h-[1000px] rounded-lg border bg-card items-center justify-center flex">
|
||||
<Spinner className="size-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DexScreenerIframe
|
||||
chain={props.chain}
|
||||
contractAddress={props.contractAddress}
|
||||
/>
|
||||
</ClientOnly>
|
||||
);
|
||||
}
|
||||
+11
-32
@@ -1,42 +1,21 @@
|
||||
import "server-only";
|
||||
import { isProd } from "@/constants/env-utils";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { Bridge, type ThirdwebClient } from "thirdweb";
|
||||
|
||||
export async function fetchTokenInfoFromBridge(params: {
|
||||
chainId: number;
|
||||
tokenAddress: string;
|
||||
clientId: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://bridge.${isProd ? "thirdweb.com" : "thirdweb-dev.com"}/v1/tokens?chainId=${params.chainId}&tokenAddress=${params.tokenAddress}`,
|
||||
{
|
||||
headers: {
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
`Failed to fetch token info from bridge: ${await res.text()}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
data: Array<{
|
||||
iconUri: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
name: string;
|
||||
symbol: string;
|
||||
priceUsd: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
return data.data[0];
|
||||
const res = await Bridge.tokens({
|
||||
client: params.client,
|
||||
chainId: params.chainId,
|
||||
tokenAddress: params.tokenAddress,
|
||||
includePrices: true,
|
||||
limit: 1,
|
||||
});
|
||||
return res[0];
|
||||
} catch {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+99
-19
@@ -1,3 +1,4 @@
|
||||
import { BarChart3Icon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
|
||||
import { cookies } from "next/headers";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
@@ -13,8 +14,6 @@ import { PageHeader } from "../_components/PageHeader";
|
||||
import { ContractHeaderUI } from "./_components/ContractHeader";
|
||||
import { TokenDropClaim } from "./_components/claim-tokens/claim-tokens-ui";
|
||||
import { ContractAnalyticsOverview } from "./_components/contract-analytics/contract-analytics";
|
||||
import { DexScreener } from "./_components/dex-screener";
|
||||
import { mapChainIdToDexScreenerChainSlug } from "./_components/dex-screener-chains";
|
||||
import { RecentTransfers } from "./_components/RecentTransfers";
|
||||
import { fetchTokenInfoFromBridge } from "./_utils/fetch-coin-info";
|
||||
import { getCurrencyMeta } from "./_utils/getCurrencyMeta";
|
||||
@@ -40,7 +39,7 @@ export async function ERC20PublicPage(props: {
|
||||
}),
|
||||
fetchTokenInfoFromBridge({
|
||||
chainId: props.serverContract.chain.id,
|
||||
clientId: props.clientContract.client.clientId,
|
||||
client: props.serverContract.client,
|
||||
tokenAddress: props.serverContract.address,
|
||||
}),
|
||||
resolveFunctionSelectors(props.serverContract),
|
||||
@@ -80,7 +79,7 @@ export async function ERC20PublicPage(props: {
|
||||
<AssetPageView assetType="coin" chainId={props.chainMetadata.chainId} />
|
||||
<PageHeader containerClassName="max-w-7xl" />
|
||||
|
||||
<div className="border-b">
|
||||
<div className="border-b border-dashed">
|
||||
<div className="container max-w-7xl">
|
||||
<ContractHeaderUI
|
||||
chainMetadata={props.chainMetadata}
|
||||
@@ -102,6 +101,44 @@ export async function ERC20PublicPage(props: {
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
{tokenInfoFromUB && (
|
||||
<div className="container max-w-7xl mb-8">
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3 p-6 border rounded-xl bg-card relative">
|
||||
<TokenInfoSection
|
||||
icon={DollarSignIcon}
|
||||
label="Price"
|
||||
value={
|
||||
tokenInfoFromUB.prices.USD
|
||||
? formatPrice(tokenInfoFromUB.prices.USD)
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
|
||||
<TokenInfoSection
|
||||
className="border-t pt-6 lg:border-l lg:border-t-0 border-dashed lg:pl-8 lg:pt-0"
|
||||
icon={TrendingUpIcon}
|
||||
label="Market Cap"
|
||||
value={
|
||||
tokenInfoFromUB.marketCapUsd
|
||||
? formatCompactUSD(tokenInfoFromUB.marketCapUsd)
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
|
||||
<TokenInfoSection
|
||||
className="border-t pt-6 lg:border-l lg:border-t-0 border-dashed lg:pl-8 lg:pt-0"
|
||||
icon={BarChart3Icon}
|
||||
label="Volume (24h)"
|
||||
value={
|
||||
tokenInfoFromUB.volume24hUsd
|
||||
? formatCompactUSD(tokenInfoFromUB.volume24hUsd)
|
||||
: "N/A"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showBuyEmbed && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<GridPatternEmbedContainer>
|
||||
@@ -118,20 +155,6 @@ export async function ERC20PublicPage(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.chainMetadata.chainId in mapChainIdToDexScreenerChainSlug && (
|
||||
<div className="container max-w-7xl pb-10">
|
||||
<DexScreener
|
||||
chain={
|
||||
mapChainIdToDexScreenerChainSlug[
|
||||
props.chainMetadata
|
||||
.chainId as keyof typeof mapChainIdToDexScreenerChainSlug
|
||||
]
|
||||
}
|
||||
contractAddress={props.clientContract.address}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="container flex max-w-7xl grow flex-col pb-10">
|
||||
<div className="flex grow flex-col gap-8">
|
||||
<ContractAnalyticsOverview
|
||||
@@ -173,7 +196,6 @@ function BuyEmbed(props: {
|
||||
return (
|
||||
<BuyAndSwapEmbed
|
||||
chain={props.clientContract.chain}
|
||||
client={props.clientContract.client}
|
||||
tokenAddress={props.clientContract.address}
|
||||
buyAmount={undefined}
|
||||
pageType="asset"
|
||||
@@ -206,3 +228,61 @@ async function getActiveClaimConditionWithErrorHandler(
|
||||
}
|
||||
|
||||
type ActiveClaimCondition = Awaited<ReturnType<typeof getActiveClaimCondition>>;
|
||||
|
||||
function TokenInfoSection(props: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<div className="flex mb-5">
|
||||
<div className="border rounded-full bg-background p-2">
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<dl className="space-y-1.5">
|
||||
<dt className="text-xs font-medium text-muted-foreground tracking-wider uppercase">
|
||||
{props.label}
|
||||
</dt>
|
||||
<dd className="text-3xl font-bold text-foreground tracking-tight">
|
||||
{props.value}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatPrice(value: number): string {
|
||||
if (value < 100) {
|
||||
return smallValueUSDFormatter.format(value);
|
||||
}
|
||||
return largeValueUSDFormatter.format(value);
|
||||
}
|
||||
|
||||
const smallValueUSDFormatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 6,
|
||||
roundingMode: "halfEven",
|
||||
});
|
||||
|
||||
const largeValueUSDFormatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
roundingMode: "halfEven",
|
||||
});
|
||||
|
||||
const compactValueUSDFormatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 2,
|
||||
roundingMode: "halfEven",
|
||||
});
|
||||
|
||||
function formatCompactUSD(value: number): string {
|
||||
return compactValueUSDFormatter.format(value);
|
||||
}
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { getChainMetadata } from "../../../utils";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListCardProps = {
|
||||
favoriteButton: JSX.Element | undefined;
|
||||
@@ -18,7 +18,7 @@ type ChainListCardProps = {
|
||||
iconUrl?: string;
|
||||
};
|
||||
|
||||
export async function ChainListCard({
|
||||
export function ChainListCard({
|
||||
isDeprecated,
|
||||
chainId,
|
||||
chainName,
|
||||
@@ -28,7 +28,7 @@ export async function ChainListCard({
|
||||
favoriteButton,
|
||||
iconUrl,
|
||||
}: ChainListCardProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const customChainMetadata = getCustomChainMetadata(chainId);
|
||||
|
||||
return (
|
||||
<div className="relative h-full">
|
||||
@@ -90,9 +90,9 @@ export async function ChainListCard({
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{(isDeprecated || chainMetadata?.gasSponsored) && (
|
||||
{(isDeprecated || customChainMetadata?.gasSponsored) && (
|
||||
<div className="mt-5 flex gap-5 border-t pt-4">
|
||||
{!isDeprecated && chainMetadata?.gasSponsored && (
|
||||
{!isDeprecated && customChainMetadata?.gasSponsored && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TicketCheckIcon className="size-5 text-foreground" />
|
||||
<p className="text-sm">Gas Sponsored</p>
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
|
||||
import type { ChainSupportedService } from "@/types/chain";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { products } from "../../../components/server/products";
|
||||
import { getChainMetadata } from "../../../utils";
|
||||
import { getCustomChainMetadata } from "../../../utils";
|
||||
|
||||
type ChainListRowProps = {
|
||||
favoriteButton: JSX.Element | undefined;
|
||||
@@ -26,7 +26,7 @@ type ChainListRowProps = {
|
||||
iconUrl?: string;
|
||||
};
|
||||
|
||||
export async function ChainListRow({
|
||||
export function ChainListRow({
|
||||
isDeprecated,
|
||||
chainId,
|
||||
chainName,
|
||||
@@ -36,7 +36,7 @@ export async function ChainListRow({
|
||||
favoriteButton,
|
||||
iconUrl,
|
||||
}: ChainListRowProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const chainMetadata = getCustomChainMetadata(chainId);
|
||||
return (
|
||||
<TableRow className="hover:bg-accent/50" linkBox>
|
||||
{/* Name */}
|
||||
|
||||
@@ -914,9 +914,9 @@ const chainMetaRecord = {
|
||||
} satisfies Record<number, ExtraChainMetadata>;
|
||||
// END TEMPORARY
|
||||
|
||||
export async function getChainMetadata(
|
||||
export function getCustomChainMetadata(
|
||||
chainId: number,
|
||||
): Promise<(ExtraChainMetadata & { gasSponsored?: true }) | null> {
|
||||
): (ExtraChainMetadata & { gasSponsored?: true }) | null {
|
||||
// TODO: fetch this from the API
|
||||
if (chainId in chainMetaRecord) {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { cn } from "@workspace/ui/lib/utils";
|
||||
import { ActivityIcon, SearchIcon, TrendingUpIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Bridge } from "thirdweb";
|
||||
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -15,11 +15,22 @@ const client = getClientThirdwebClient();
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
type Filter =
|
||||
| { type: "filter"; value: "volume" | "market_cap" }
|
||||
| { type: "search"; value: string };
|
||||
|
||||
export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [chainId, setChainId] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
const [sortBy, setSortBy] = useState<"volume" | "market_cap">("volume");
|
||||
const [filter, _setFilter] = useState<Filter>({
|
||||
type: "filter",
|
||||
value: "volume",
|
||||
});
|
||||
|
||||
const setFilter = useCallback((filter: Filter) => {
|
||||
_setFilter(filter);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
const tokensQuery = useQuery({
|
||||
queryKey: [
|
||||
@@ -27,8 +38,7 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
{
|
||||
page,
|
||||
chainId,
|
||||
sortBy,
|
||||
search,
|
||||
filter,
|
||||
},
|
||||
],
|
||||
queryFn: () => {
|
||||
@@ -37,8 +47,8 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
chainId: chainId,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
sortBy: search ? undefined : sortBy,
|
||||
query: search ? search : undefined,
|
||||
sortBy: filter.type === "filter" ? filter.value : undefined,
|
||||
query: filter.type === "search" ? filter.value : undefined,
|
||||
});
|
||||
},
|
||||
refetchOnMount: false,
|
||||
@@ -62,19 +72,19 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
<SortButton
|
||||
label="Popular"
|
||||
onClick={() => {
|
||||
setSortBy("market_cap");
|
||||
setSearch("");
|
||||
setFilter({ type: "filter", value: "market_cap" });
|
||||
}}
|
||||
isSelected={sortBy === "market_cap" && !search}
|
||||
isSelected={
|
||||
filter.type === "filter" && filter.value === "market_cap"
|
||||
}
|
||||
icon={ActivityIcon}
|
||||
/>
|
||||
<SortButton
|
||||
label="Trending"
|
||||
onClick={() => {
|
||||
setSortBy("volume");
|
||||
setSearch("");
|
||||
setFilter({ type: "filter", value: "volume" });
|
||||
}}
|
||||
isSelected={sortBy === "volume" && !search}
|
||||
isSelected={filter.type === "filter" && filter.value === "volume"}
|
||||
icon={TrendingUpIcon}
|
||||
/>
|
||||
</div>
|
||||
@@ -83,9 +93,11 @@ export function TokenPage(props: { chains: Bridge.chains.Result }) {
|
||||
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search Tokens"
|
||||
value={search}
|
||||
value={filter.type === "search" ? filter.value : ""}
|
||||
className="rounded-xl bg-card flex-1 pl-9"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) =>
|
||||
setFilter({ type: "search", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { useId } from "react";
|
||||
import { download } from "thirdweb/storage";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
|
||||
@@ -19,8 +18,8 @@ export const size = {
|
||||
export const contentType = "image/png";
|
||||
|
||||
const TWLogo: React.FC = () => {
|
||||
const cipId = useId();
|
||||
const linearGradientId = useId();
|
||||
const cipId = "og-tw-logo-clip-path";
|
||||
const linearGradientId = "og-tw-logo-linear-gradient";
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
|
||||
|
||||
+29
-22
@@ -25,6 +25,34 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { ThirdwebMiniLogo } from "../../../../../../../components/ThirdwebMiniLogo";
|
||||
import { searchParams } from "../search-params";
|
||||
|
||||
function getStatusBadge(invoice: Stripe.Invoice) {
|
||||
switch (invoice.status) {
|
||||
case "paid":
|
||||
return <Badge variant="success">Paid</Badge>;
|
||||
case "open":
|
||||
// we treat "uncollectible" as unpaid
|
||||
case "uncollectible": {
|
||||
// if the invoice has a due date, use that, otherwise use the effective at date (which is the date the invoice was created)
|
||||
const effectiveDueDate = invoice.due_date || invoice.effective_at;
|
||||
|
||||
// if the invoice due date is in the past, we want to display it as past due
|
||||
if (effectiveDueDate && effectiveDueDate * 1000 < Date.now()) {
|
||||
return <Badge variant="destructive">Past Due</Badge>;
|
||||
}
|
||||
return <Badge variant="outline">Open</Badge>;
|
||||
}
|
||||
case "void":
|
||||
return <Badge variant="secondary">Void</Badge>;
|
||||
|
||||
default:
|
||||
return <Badge variant="outline">Unknown</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
function isInvoicePayable(invoice: Stripe.Invoice) {
|
||||
return invoice.status === "open" || invoice.status === "uncollectible";
|
||||
}
|
||||
|
||||
export function BillingHistory(props: {
|
||||
teamSlug: string;
|
||||
invoices: Stripe.Invoice[];
|
||||
@@ -57,27 +85,6 @@ export function BillingHistory(props: {
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusBadge = (invoice: Stripe.Invoice) => {
|
||||
switch (invoice.status) {
|
||||
case "paid":
|
||||
return <Badge variant="success">Paid</Badge>;
|
||||
case "open":
|
||||
// we treate "uncollectible" as unpaid
|
||||
case "uncollectible": {
|
||||
// if the invoice due date is in the past, we want to display it as past due
|
||||
if (invoice.due_date && invoice.due_date * 1000 < Date.now()) {
|
||||
return <Badge variant="destructive">Past Due</Badge>;
|
||||
}
|
||||
return <Badge variant="outline">Open</Badge>;
|
||||
}
|
||||
case "void":
|
||||
return <Badge variant="secondary">Void</Badge>;
|
||||
|
||||
default:
|
||||
return <Badge variant="outline">Unknown</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (props.invoices.length === 0) {
|
||||
if (props.status === "open") {
|
||||
return (
|
||||
@@ -126,7 +133,7 @@ export function BillingHistory(props: {
|
||||
<TableCell>{getStatusBadge(invoice)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end space-x-2">
|
||||
{invoice.status === "open" && (
|
||||
{isInvoicePayable(invoice) && (
|
||||
<>
|
||||
{/* always show the crypto payment button */}
|
||||
<ToolTipLabel
|
||||
|
||||
+1
-1
@@ -552,7 +552,7 @@ export function AccountAbstractionSettingsPage(
|
||||
you build your own rules.{" "}
|
||||
<UnderlineLink
|
||||
className="text-primary-500"
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet/sponsorship-rules#setting-up-a-server-verifier"
|
||||
href="https://portal.thirdweb.com/transactions/policies#server-verifier"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
+34
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, InfoIcon } from "lucide-react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NebulaIcon } from "@/icons/NebulaIcon";
|
||||
@@ -24,6 +24,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden py-10 lg:py-16">
|
||||
<AIUsageBanner />
|
||||
{props.showAurora && (
|
||||
<Aurora className="top-0 left-1/2 h-[800px] w-[1000px] text-[hsl(var(--primary-foreground)/8%)] lg:w-[150%] dark:text-[hsl(var(--primary-foreground)/10%)]" />
|
||||
)}
|
||||
@@ -165,3 +166,35 @@ const Aurora: React.FC<AuroraProps> = ({ className }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function AIUsageBanner() {
|
||||
return (
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex justify-center px-4 pt-4">
|
||||
<div className="max-w-4xl">
|
||||
<div className="relative overflow-hidden rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-blue-50/50 p-3 shadow-sm dark:border-blue-800 dark:from-blue-950/50 dark:to-blue-950/20">
|
||||
<div className="relative flex items-center gap-3 px-2">
|
||||
<div className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
<InfoIcon className="size-3.5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-sm">
|
||||
<p className="text-blue-800 dark:text-blue-200">
|
||||
thirdweb AI usage is billed based on number of tokens used. See
|
||||
the{" "}
|
||||
<a
|
||||
href="https://thirdweb.com/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-700 underline decoration-blue-300 underline-offset-2 transition-colors hover:text-blue-800 hover:decoration-blue-500 dark:text-blue-300 dark:decoration-blue-600 dark:hover:text-blue-200 dark:hover:decoration-blue-400"
|
||||
>
|
||||
pricing page
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+13
@@ -1,3 +1,4 @@
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
@@ -110,6 +111,7 @@ export function TransactionsTable(props: {
|
||||
const [autoUpdate, setAutoUpdate] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<EngineStatus | undefined>(undefined);
|
||||
const [filterId, setFilterId] = useState<string | undefined>(undefined);
|
||||
const autoUpdateId = useId();
|
||||
const pageSize = 10;
|
||||
const transactionsQuery = useEngineTransactions({
|
||||
@@ -121,6 +123,7 @@ export function TransactionsTable(props: {
|
||||
page: page,
|
||||
status,
|
||||
},
|
||||
id: filterId,
|
||||
});
|
||||
|
||||
const transactions = transactionsQuery.data?.transactions ?? [];
|
||||
@@ -160,6 +163,16 @@ export function TransactionsTable(props: {
|
||||
onCheckedChange={(v) => setAutoUpdate(!!v)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
className="max-w-[250px]"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
setFilterId(value || undefined);
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="Filter by Queue ID"
|
||||
value={filterId || ""}
|
||||
/>
|
||||
<StatusSelector
|
||||
setStatus={(v) => {
|
||||
setStatus(v);
|
||||
|
||||
+4
@@ -74,6 +74,10 @@ export type CreateNFTCollectionFunctions = {
|
||||
count: number;
|
||||
};
|
||||
gasless: boolean;
|
||||
onNotEnoughFunds: (data: {
|
||||
requiredAmount: string;
|
||||
balance: string;
|
||||
}) => void;
|
||||
}) => Promise<void>;
|
||||
lazyMintNFTs: (params: {
|
||||
values: CreateNFTCollectionAllValues;
|
||||
|
||||
+97
-3
@@ -1,6 +1,14 @@
|
||||
"use client";
|
||||
import { useRef } from "react";
|
||||
import { encode, getContract, type ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
encode,
|
||||
estimateGasCost,
|
||||
getContract,
|
||||
getGasPrice,
|
||||
type PreparedTransaction,
|
||||
type ThirdwebClient,
|
||||
toEther,
|
||||
} from "thirdweb";
|
||||
import { deployERC721Contract, deployERC1155Contract } from "thirdweb/deploys";
|
||||
import { multicall } from "thirdweb/extensions/common";
|
||||
import {
|
||||
@@ -14,7 +22,8 @@ import {
|
||||
} from "thirdweb/extensions/erc1155";
|
||||
import { grantRole } from "thirdweb/extensions/permissions";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import { maxUint256, resolvePromisedValue } from "thirdweb/utils";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import { create7702MinimalAccount } from "thirdweb/wallets/smart";
|
||||
import { revalidatePathAction } from "@/actions/revalidate";
|
||||
import { reportContractDeployed } from "@/analytics/report";
|
||||
@@ -39,6 +48,10 @@ export function CreateNFTPage(props: {
|
||||
const contractAddressRef = useRef<string | undefined>(undefined);
|
||||
const getChain = useGetV5DashboardChain();
|
||||
const sendAndConfirmTx = useSendAndConfirmTx();
|
||||
const sendAndConfirmTxNoPayModal = useSendAndConfirmTx({
|
||||
payModal: false,
|
||||
});
|
||||
|
||||
function getAccount(params: { gasless: boolean }) {
|
||||
if (!activeAccount) {
|
||||
throw new Error("Wallet is not connected");
|
||||
@@ -218,6 +231,10 @@ export function CreateNFTPage(props: {
|
||||
count: number;
|
||||
};
|
||||
gasless: boolean;
|
||||
onNotEnoughFunds: (data: {
|
||||
requiredAmount: string;
|
||||
balance: string;
|
||||
}) => void;
|
||||
}) {
|
||||
const { values, batch } = params;
|
||||
const contract = getDeployedContract({
|
||||
@@ -277,7 +294,46 @@ export function CreateNFTPage(props: {
|
||||
data: encodedTransactions,
|
||||
});
|
||||
|
||||
await sendAndConfirmTx.mutateAsync(tx);
|
||||
// if there are more than one batches, on the first batch, we check if the user has enough funds to cover the cost of all the batches ->
|
||||
// calculate the cost of one batch, multiply by the number of batches to get the total cost
|
||||
// if the user does not have enough funds, call the onNotEnoughFunds callback
|
||||
|
||||
const totalBatches = Math.ceil(values.nfts.length / batch.count);
|
||||
|
||||
if (batch.startIndex === 0 && totalBatches > 1 && !params.gasless) {
|
||||
if (!activeAccount) {
|
||||
throw new Error("Wallet is not connected");
|
||||
}
|
||||
|
||||
const costPerBatch = await getTotalTransactionCost({
|
||||
tx: tx,
|
||||
from: activeAccount.address,
|
||||
});
|
||||
|
||||
const totalCost = costPerBatch * BigInt(totalBatches);
|
||||
|
||||
const walletBalance = await getWalletBalance({
|
||||
address: activeAccount.address,
|
||||
chain: contract.chain,
|
||||
client: contract.client,
|
||||
});
|
||||
|
||||
if (walletBalance.value < totalCost) {
|
||||
params.onNotEnoughFunds({
|
||||
balance: toEther(walletBalance.value),
|
||||
requiredAmount: toEther(totalCost),
|
||||
});
|
||||
throw new Error(
|
||||
`Not enough funds: Required ${toEther(totalCost)}, Balance ${toEther(walletBalance.value)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
|
||||
} else if (totalBatches > 1) {
|
||||
await sendAndConfirmTxNoPayModal.mutateAsync(tx);
|
||||
} else {
|
||||
await sendAndConfirmTx.mutateAsync(tx);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetAdmins(params: {
|
||||
@@ -390,3 +446,41 @@ function transformSocialUrls(
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
async function getTransactionGasCost(tx: PreparedTransaction, from?: string) {
|
||||
try {
|
||||
const gasCost = await estimateGasCost({
|
||||
from,
|
||||
transaction: tx,
|
||||
});
|
||||
|
||||
const bufferCost = gasCost.wei / 10n;
|
||||
|
||||
// Note: get tx.value AFTER estimateGasCost
|
||||
// add 10% extra gas cost to the estimate to ensure user buys enough to cover the tx cost
|
||||
return gasCost.wei + bufferCost;
|
||||
} catch {
|
||||
if (from) {
|
||||
// try again without passing from
|
||||
return await getTransactionGasCost(tx);
|
||||
}
|
||||
// fallback if both fail, use the tx value + 1M * gas price
|
||||
const gasPrice = await getGasPrice({
|
||||
chain: tx.chain,
|
||||
client: tx.client,
|
||||
});
|
||||
|
||||
return 1_000_000n * gasPrice;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTotalTransactionCost(params: {
|
||||
tx: PreparedTransaction;
|
||||
from?: string;
|
||||
}) {
|
||||
const [txValue, txGasCost] = await Promise.all([
|
||||
resolvePromisedValue(params.tx.value),
|
||||
getTransactionGasCost(params.tx, params.from),
|
||||
]);
|
||||
return (txValue || 0n) + txGasCost;
|
||||
}
|
||||
|
||||
+176
-32
@@ -1,14 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpFromLineIcon,
|
||||
ImageOffIcon,
|
||||
RefreshCcwIcon,
|
||||
ShoppingBagIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
BuyWidget,
|
||||
TokenProvider,
|
||||
TokenSymbol,
|
||||
useActiveAccount,
|
||||
@@ -37,6 +42,7 @@ import {
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { parseError } from "@/utils/errorParser";
|
||||
import { getSDKTheme } from "@/utils/sdk-component-theme";
|
||||
import { ChainOverview } from "../../_common/chain-overview";
|
||||
import { StepCard } from "../../_common/step-card";
|
||||
import { StorageErrorPlanUpsell } from "../../_common/storage-error-upsell";
|
||||
@@ -71,6 +77,10 @@ export function LaunchNFT(props: {
|
||||
const walletRequiresApproval = activeWallet?.id !== "inApp";
|
||||
const account = useActiveAccount();
|
||||
const contractAddressRef = useRef<string | null>(null);
|
||||
const { theme } = useTheme();
|
||||
const chainMeta = useV5DashboardChain(
|
||||
Number(formValues.collectionInfo.chain),
|
||||
);
|
||||
|
||||
function updateStatus(
|
||||
index: number,
|
||||
@@ -161,6 +171,18 @@ export function LaunchNFT(props: {
|
||||
const [isGasless, setIsGasless] = useState(canEnableGasless);
|
||||
const showGaslessSection = false; // activeWallet?.id === "inApp";
|
||||
|
||||
const [notEnoughFunds, setNotEnoughFunds] = useState<
|
||||
| false
|
||||
| {
|
||||
values: {
|
||||
requiredAmount: string;
|
||||
balance: string;
|
||||
};
|
||||
isBuySuccess: boolean;
|
||||
showBuyWidget: boolean;
|
||||
}
|
||||
>(false);
|
||||
|
||||
const contractLink = contractAddressRef.current
|
||||
? `/team/${props.teamSlug}/${props.projectSlug}/contract/${formValues.collectionInfo.chain}/${contractAddressRef.current}`
|
||||
: null;
|
||||
@@ -204,6 +226,16 @@ export function LaunchNFT(props: {
|
||||
},
|
||||
gasless: isGasless,
|
||||
values: formValues,
|
||||
onNotEnoughFunds: (data) => {
|
||||
setNotEnoughFunds({
|
||||
values: {
|
||||
balance: data.balance,
|
||||
requiredAmount: data.requiredAmount,
|
||||
},
|
||||
isBuySuccess: false,
|
||||
showBuyWidget: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
batchesProcessedRef.current += 1;
|
||||
@@ -216,6 +248,16 @@ export function LaunchNFT(props: {
|
||||
},
|
||||
gasless: isGasless,
|
||||
values: formValues,
|
||||
onNotEnoughFunds: (data) => {
|
||||
setNotEnoughFunds({
|
||||
values: {
|
||||
balance: data.balance,
|
||||
requiredAmount: data.requiredAmount,
|
||||
},
|
||||
isBuySuccess: false,
|
||||
showBuyWidget: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -357,41 +399,143 @@ export function LaunchNFT(props: {
|
||||
className="gap-0 overflow-hidden p-0 md:max-w-[480px]"
|
||||
dialogCloseClassName="hidden"
|
||||
>
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<DialogHeader className="space-y-0.5">
|
||||
<DialogTitle className="font-semibold text-xl tracking-tight">
|
||||
Status
|
||||
</DialogTitle>
|
||||
{walletRequiresApproval && (
|
||||
<DialogDescription>
|
||||
Each step will prompt a signature request in your wallet
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
{/** biome-ignore lint/complexity/useOptionalChain: can't change it to optional chain */}
|
||||
{notEnoughFunds && notEnoughFunds.showBuyWidget ? (
|
||||
<div className="bg-card">
|
||||
<div className="border-b py-2 px-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setNotEnoughFunds({
|
||||
...notEnoughFunds,
|
||||
showBuyWidget: false,
|
||||
})
|
||||
}
|
||||
className="gap-2 px-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<BuyWidget
|
||||
client={props.client}
|
||||
chain={chain}
|
||||
amount={String(
|
||||
Number(notEnoughFunds.values.requiredAmount) -
|
||||
Number(notEnoughFunds.values.balance),
|
||||
)}
|
||||
buttonLabel={`Buy ${chainMeta?.nativeCurrency?.symbol || "ETH"}`}
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
className="!border-none !w-full !rounded-none"
|
||||
onSuccess={() => {
|
||||
setNotEnoughFunds({
|
||||
...notEnoughFunds,
|
||||
isBuySuccess: true,
|
||||
showBuyWidget: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-6 p-6 mb-2">
|
||||
<DialogHeader className="space-y-0.5">
|
||||
<DialogTitle className="font-semibold text-xl tracking-tight">
|
||||
Status
|
||||
</DialogTitle>
|
||||
{walletRequiresApproval && (
|
||||
<DialogDescription>
|
||||
Each step will prompt a signature request in your wallet
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<MultiStepStatus
|
||||
onRetry={handleRetry}
|
||||
renderError={(step, errorMessage) => {
|
||||
if (
|
||||
props.teamPlan === "free" &&
|
||||
errorMessage.toLowerCase().includes("storage limit")
|
||||
) {
|
||||
return (
|
||||
<StorageErrorPlanUpsell
|
||||
onRetry={() => handleRetry(step)}
|
||||
teamSlug={props.teamSlug}
|
||||
trackingCampaign="create-nft"
|
||||
/>
|
||||
);
|
||||
}
|
||||
<MultiStepStatus
|
||||
onRetry={handleRetry}
|
||||
renderError={(step, errorMessage) => {
|
||||
if (
|
||||
props.teamPlan === "free" &&
|
||||
errorMessage.toLowerCase().includes("storage limit")
|
||||
) {
|
||||
return (
|
||||
<StorageErrorPlanUpsell
|
||||
onRetry={() => handleRetry(step)}
|
||||
teamSlug={props.teamSlug}
|
||||
trackingCampaign="create-nft"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
steps={steps}
|
||||
/>
|
||||
</div>
|
||||
if (notEnoughFunds) {
|
||||
return (
|
||||
<div>
|
||||
{notEnoughFunds.isBuySuccess ? (
|
||||
<p className="text-success-text text-sm mt-1.5 mb-2">
|
||||
Funds bought successfully
|
||||
</p>
|
||||
) : (
|
||||
<div className="mt-2 mb-2 space-y-1">
|
||||
<p className="text-destructive-text text-sm">
|
||||
Not enough funds
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Required Funds:{" "}
|
||||
{notEnoughFunds.values.requiredAmount}{" "}
|
||||
{chainMeta?.nativeCurrency?.symbol || "ETH"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Balance: {notEnoughFunds.values.balance}{" "}
|
||||
{chainMeta?.nativeCurrency?.symbol || "ETH"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex justify-end gap-4 border-border border-t bg-card p-6">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleRetry(step);
|
||||
setNotEnoughFunds(false);
|
||||
}}
|
||||
size="sm"
|
||||
variant={
|
||||
notEnoughFunds.isBuySuccess
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCcwIcon className="size-3.5" />
|
||||
Retry
|
||||
</Button>
|
||||
|
||||
{!notEnoughFunds.isBuySuccess && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setNotEnoughFunds({
|
||||
...notEnoughFunds,
|
||||
showBuyWidget: true,
|
||||
});
|
||||
}}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<ShoppingBagIcon className="size-3.5" />
|
||||
Buy Funds
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
steps={steps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-4 border-border border-t bg-card p-6">
|
||||
{!isComplete ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
+48
-18
@@ -13,6 +13,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
@@ -49,6 +50,8 @@ export function TransactionsTableUI(props: {
|
||||
getData: (params: {
|
||||
page: number;
|
||||
status: TransactionStatus | undefined;
|
||||
id: string | undefined;
|
||||
from: string | undefined;
|
||||
}) => Promise<TransactionsResponse>;
|
||||
project: Project;
|
||||
teamSlug: string;
|
||||
@@ -60,13 +63,15 @@ export function TransactionsTableUI(props: {
|
||||
const [status, setStatus] = useState<TransactionStatus | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [id, setId] = useState<string | undefined>(undefined);
|
||||
const [from, setFrom] = useState<string | undefined>(undefined);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const pageSize = 10;
|
||||
const transactionsQuery = useQuery({
|
||||
placeholderData: keepPreviousData,
|
||||
queryFn: () => props.getData({ page, status }),
|
||||
queryKey: ["transactions", props.project.id, page, status],
|
||||
queryFn: () => props.getData({ page, status, id, from }),
|
||||
queryKey: ["transactions", props.project.id, page, status, id, from],
|
||||
refetchInterval: autoUpdate ? 4_000 : false,
|
||||
});
|
||||
|
||||
@@ -84,18 +89,18 @@ export function TransactionsTableUI(props: {
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6 lg:flex-row lg:justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
Transaction History
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Transactions sent from server wallets
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 rounded-lg rounded-b-none px-6 py-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
Transaction History
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Transactions sent from server wallets
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-5 border-border border-t pt-4 lg:border-none lg:pt-0">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2 border-border border-t pt-4 lg:border-none lg:pt-0">
|
||||
<Label htmlFor={autoUpdateId}>Auto Update</Label>
|
||||
<Switch
|
||||
checked={autoUpdate}
|
||||
@@ -103,14 +108,39 @@ export function TransactionsTableUI(props: {
|
||||
onCheckedChange={(v) => setAutoUpdate(!!v)}
|
||||
/>
|
||||
</div>
|
||||
<StatusSelector
|
||||
setStatus={(v) => {
|
||||
setStatus(v);
|
||||
// reset page
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-3">
|
||||
<Input
|
||||
className="max-w-[250px]"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
setId(value || undefined);
|
||||
setPage(1);
|
||||
}}
|
||||
status={status}
|
||||
placeholder="Filter by Queue ID"
|
||||
value={id || ""}
|
||||
/>
|
||||
<Input
|
||||
className="max-w-[250px]"
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
setFrom(value || undefined);
|
||||
setPage(1);
|
||||
}}
|
||||
placeholder="Filter by wallet address"
|
||||
value={from || ""}
|
||||
/>
|
||||
<div>
|
||||
<StatusSelector
|
||||
setStatus={(v) => {
|
||||
setStatus(v);
|
||||
// reset page
|
||||
setPage(1);
|
||||
}}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+9
-1
@@ -16,11 +16,13 @@ export function TransactionsTable(props: {
|
||||
return (
|
||||
<TransactionsTableUI
|
||||
client={props.client}
|
||||
getData={async ({ page, status }) => {
|
||||
getData={async ({ page, status, id, from }) => {
|
||||
return await getTransactions({
|
||||
page,
|
||||
project: props.project,
|
||||
status,
|
||||
id,
|
||||
from,
|
||||
});
|
||||
}}
|
||||
project={props.project}
|
||||
@@ -34,10 +36,14 @@ async function getTransactions({
|
||||
project,
|
||||
page,
|
||||
status,
|
||||
id,
|
||||
from,
|
||||
}: {
|
||||
project: Project;
|
||||
page: number;
|
||||
status: TransactionStatus | undefined;
|
||||
id: string | undefined;
|
||||
from: string | undefined;
|
||||
}) {
|
||||
const transactions = await engineCloudProxy<{ result: TransactionsResponse }>(
|
||||
{
|
||||
@@ -52,6 +58,8 @@ async function getTransactions({
|
||||
limit: "20",
|
||||
page: page.toString(),
|
||||
status: status ?? undefined,
|
||||
id: id ?? undefined,
|
||||
from: from ?? undefined,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
+19
-27
@@ -96,15 +96,22 @@ export function ServerWalletsTableUI({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start lg:items-end gap-5 border-t lg:border-t-0 pt-5 lg:pt-0 border-dashed">
|
||||
<SingleNetworkSelector
|
||||
chainId={selectedChainId}
|
||||
onChange={setSelectedChainId}
|
||||
client={client}
|
||||
disableChainId
|
||||
className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50"
|
||||
placeholder="Select network"
|
||||
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
|
||||
/>
|
||||
<div className="flex flex-row gap-2.5">
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
/>
|
||||
<SingleNetworkSelector
|
||||
chainId={selectedChainId}
|
||||
onChange={setSelectedChainId}
|
||||
client={client}
|
||||
disableChainId
|
||||
className="w-fit min-w-[180px] rounded-full bg-background hover:bg-accent/50"
|
||||
placeholder="Select network"
|
||||
popoverContentClassName="!w-[320px] rounded-xl overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Label
|
||||
@@ -187,25 +194,10 @@ export function ServerWalletsTableUI({
|
||||
<XIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-muted-foreground">No server wallets found</p>
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TableContainer>
|
||||
|
||||
{wallets.length > 0 && (
|
||||
<div className="flex justify-end items-center p-4 py-5 lg:px-6 border-t">
|
||||
<CreateServerWallet
|
||||
managementAccessToken={managementAccessToken}
|
||||
project={project}
|
||||
teamSlug={teamSlug}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex flex-col items-center border-t p-6">
|
||||
<div className="mb-4 text-muted-foreground text-sm">
|
||||
@@ -215,7 +207,7 @@ export function ServerWalletsTableUI({
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage > 1 ? currentPage - 1 : 1
|
||||
}`}
|
||||
legacyBehavior
|
||||
@@ -232,7 +224,7 @@ export function ServerWalletsTableUI({
|
||||
(pageNumber) => (
|
||||
<PaginationItem key={`page-${pageNumber}`}>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${pageNumber}`}
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${pageNumber}`}
|
||||
passHref
|
||||
>
|
||||
<PaginationLink isActive={currentPage === pageNumber}>
|
||||
@@ -244,7 +236,7 @@ export function ServerWalletsTableUI({
|
||||
)}
|
||||
<PaginationItem>
|
||||
<Link
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions/server-wallets?page=${
|
||||
href={`/team/${teamSlug}/${project.slug}/transactions?page=${
|
||||
currentPage < totalPages ? currentPage + 1 : totalPages
|
||||
}`}
|
||||
passHref
|
||||
|
||||
+69
-7
@@ -1,3 +1,4 @@
|
||||
import { createVaultClient, listEoas } from "@thirdweb-dev/vault-sdk";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ResponsiveSearchParamsProvider } from "responsive-rsc";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
@@ -6,10 +7,13 @@ import type { DurationId } from "@/components/analytics/date-range-selector";
|
||||
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
|
||||
import { ProjectPage } from "@/components/blocks/project-page/project-page";
|
||||
import { InAppWalletUsersPageContent } from "@/components/in-app-wallet-users-content/in-app-wallet-users-content";
|
||||
import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
import { getFiltersFromSearchParams } from "@/lib/time";
|
||||
import { loginRedirect } from "@/utils/redirects";
|
||||
import type { Wallet } from "../transactions/server-wallets/wallet-table/types";
|
||||
import { ServerWalletsTable } from "../transactions/server-wallets/wallet-table/wallet-table";
|
||||
import { InAppWalletAnalytics } from "./analytics/chart";
|
||||
import { InAppWalletsSummary } from "./analytics/chart/Summary";
|
||||
|
||||
@@ -20,6 +24,7 @@ export default async function Page(props: {
|
||||
to?: string;
|
||||
type?: string;
|
||||
interval?: string;
|
||||
page?: string;
|
||||
}>;
|
||||
}) {
|
||||
const [searchParams, params] = await Promise.all([
|
||||
@@ -40,12 +45,47 @@ export default async function Page(props: {
|
||||
to: searchParams.to,
|
||||
});
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
const [vaultClient, project] = await Promise.all([
|
||||
createVaultClient({
|
||||
baseUrl: NEXT_PUBLIC_THIRDWEB_VAULT_URL,
|
||||
}).catch(() => undefined),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
}
|
||||
|
||||
const projectEngineCloudService = project.services.find(
|
||||
(service) => service.name === "engineCloud",
|
||||
);
|
||||
|
||||
const managementAccessToken =
|
||||
projectEngineCloudService?.managementAccessToken;
|
||||
|
||||
// Fetch server wallets with pagination (5 per page)
|
||||
const pageSize = 5;
|
||||
const currentPage = Number.parseInt(searchParams.page ?? "1");
|
||||
|
||||
const eoas =
|
||||
vaultClient && managementAccessToken
|
||||
? await listEoas({
|
||||
client: vaultClient,
|
||||
request: {
|
||||
auth: {
|
||||
accessToken: managementAccessToken,
|
||||
},
|
||||
options: {
|
||||
page: currentPage - 1,
|
||||
// @ts-expect-error - TODO: fix this
|
||||
page_size: pageSize,
|
||||
},
|
||||
},
|
||||
})
|
||||
: { data: { items: [], totalRecords: 0 }, error: null, success: true };
|
||||
|
||||
const serverWallets = eoas.data?.items as Wallet[] | undefined;
|
||||
|
||||
const client = getClientThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: project.teamId,
|
||||
@@ -103,12 +143,34 @@ export default async function Page(props: {
|
||||
authToken={authToken}
|
||||
/>
|
||||
|
||||
<InAppWalletUsersPageContent
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
projectClientId={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
{/* Server Wallets Section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{eoas.error ? null : (
|
||||
<ServerWalletsTable
|
||||
client={client}
|
||||
currentPage={currentPage}
|
||||
managementAccessToken={managementAccessToken ?? undefined}
|
||||
project={project}
|
||||
teamSlug={params.team_slug}
|
||||
totalPages={Math.ceil(eoas.data.totalRecords / pageSize)}
|
||||
totalRecords={eoas.data.totalRecords}
|
||||
wallets={serverWallets ?? []}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Wallets Section */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
User wallets
|
||||
</h2>
|
||||
<InAppWalletUsersPageContent
|
||||
authToken={authToken}
|
||||
client={client}
|
||||
projectClientId={project.publishableKey}
|
||||
teamId={project.teamId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ProjectPage>
|
||||
</ResponsiveSearchParamsProvider>
|
||||
|
||||
+2
@@ -30,11 +30,13 @@ function unclaimedFeesStub(token0Amount: bigint, token1Amount: bigint) {
|
||||
address: "0x1234567890123456789012345678901234567890",
|
||||
amount: token0Amount,
|
||||
symbol: "FOO",
|
||||
decimals: 18,
|
||||
},
|
||||
token1: {
|
||||
address: "0x0987654321098765432109876543210987654321",
|
||||
amount: token1Amount,
|
||||
symbol: "BAR",
|
||||
decimals: 18,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+7
-1
@@ -29,11 +29,13 @@ export function ClaimRewardsPage(props: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
token1: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
};
|
||||
chainSlug: string;
|
||||
@@ -98,11 +100,13 @@ export function ClaimRewardsPageUI(props: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
token1: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
};
|
||||
recipient: string;
|
||||
@@ -275,6 +279,7 @@ function TokenReward(props: {
|
||||
address: string;
|
||||
amount: bigint;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
};
|
||||
client: ThirdwebClient;
|
||||
chain: Chain;
|
||||
@@ -303,7 +308,8 @@ function TokenReward(props: {
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<p className="font-bold text-sm">
|
||||
{toTokens(props.token.amount, 18)} {props.token.symbol}
|
||||
{toTokens(props.token.amount, props.token.decimals)}{" "}
|
||||
{props.token.symbol}
|
||||
</p>
|
||||
<Link
|
||||
target="_blank"
|
||||
|
||||
+2
@@ -31,6 +31,7 @@ export default async function Page(props: {
|
||||
const chain = info.clientContract.chain;
|
||||
const assetContractServer = info.serverContract;
|
||||
const serverClient = assetContractServer.client;
|
||||
const chainMetadata = info.chainMetadata;
|
||||
|
||||
const { address: entrypointContractAddress } =
|
||||
await getDeployedEntrypointERC20({
|
||||
@@ -56,6 +57,7 @@ export default async function Page(props: {
|
||||
|
||||
// Note: must use server contract/client here
|
||||
const unclaimedFees = await getUnclaimedFees({
|
||||
chainMetadata,
|
||||
positionManager: getContract({
|
||||
address: reward.positionManager,
|
||||
chain,
|
||||
|
||||
+47
-17
@@ -1,11 +1,14 @@
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getContract, readContract } from "thirdweb";
|
||||
import { getAddress, getContract, readContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { symbol } from "thirdweb/extensions/common";
|
||||
import { decimals } from "thirdweb/extensions/erc20";
|
||||
|
||||
const maxUint128 = 2n ** 128n - 1n;
|
||||
|
||||
export async function getUnclaimedFees(params: {
|
||||
positionManager: ThirdwebContract;
|
||||
chainMetadata: ChainMetadata;
|
||||
reward: {
|
||||
tokenId: bigint;
|
||||
recipient: string;
|
||||
@@ -56,33 +59,60 @@ export async function getUnclaimedFees(params: {
|
||||
const token0Address = positionsResult[2];
|
||||
const token1Address = positionsResult[3];
|
||||
|
||||
const [token0Symbol, token1Symbol] = await Promise.all([
|
||||
symbol({
|
||||
contract: getContract({
|
||||
address: token0Address,
|
||||
chain,
|
||||
client,
|
||||
}),
|
||||
}),
|
||||
symbol({
|
||||
contract: getContract({
|
||||
address: token1Address,
|
||||
chain,
|
||||
client,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
const token0Contract = getContract({
|
||||
address: token0Address,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
|
||||
const token1Contract = getContract({
|
||||
address: token1Address,
|
||||
chain,
|
||||
client,
|
||||
});
|
||||
|
||||
const isToken0Native = getAddress(token0Address) === getAddress(ZERO_ADDRESS);
|
||||
const isToken1Native = getAddress(token1Address) === getAddress(ZERO_ADDRESS);
|
||||
|
||||
const nativeSymbol = params.chainMetadata.nativeCurrency.symbol;
|
||||
const nativeDecimals = params.chainMetadata.nativeCurrency.decimals;
|
||||
|
||||
const [token0Symbol, token1Symbol, token0Decimals, token1Decimals] =
|
||||
await Promise.all([
|
||||
isToken0Native
|
||||
? nativeSymbol
|
||||
: symbol({
|
||||
contract: token0Contract,
|
||||
}),
|
||||
isToken1Native
|
||||
? nativeSymbol
|
||||
: symbol({
|
||||
contract: token1Contract,
|
||||
}),
|
||||
isToken0Native
|
||||
? nativeDecimals
|
||||
: decimals({
|
||||
contract: token0Contract,
|
||||
}),
|
||||
isToken1Native
|
||||
? nativeDecimals
|
||||
: decimals({
|
||||
contract: token1Contract,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
token0: {
|
||||
address: token0Address,
|
||||
amount: collectResult[0],
|
||||
symbol: token0Symbol,
|
||||
decimals: token0Decimals,
|
||||
},
|
||||
token1: {
|
||||
address: token1Address,
|
||||
amount: collectResult[1],
|
||||
symbol: token1Symbol,
|
||||
decimals: token1Decimals,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { DarkVeil } from "@/components/DarkVeil";
|
||||
|
||||
const bgBackground: Record<string, [number, number, number]> = {
|
||||
dark: [0, 0, 0],
|
||||
light: [250 / 255, 250 / 255, 250 / 255],
|
||||
};
|
||||
|
||||
const hues = {
|
||||
light: 65,
|
||||
dark: -85,
|
||||
};
|
||||
|
||||
const patternLightnesss = {
|
||||
light: -1,
|
||||
dark: 0.8,
|
||||
};
|
||||
|
||||
export function BridgeVeil() {
|
||||
const theme = useTheme();
|
||||
const isLight = theme.theme === "light";
|
||||
const patternLightness = isLight
|
||||
? patternLightnesss.light
|
||||
: patternLightnesss.dark;
|
||||
const hueShift = isLight ? hues.light : hues.dark;
|
||||
return (
|
||||
<DarkVeil
|
||||
backgroundColor={bgBackground[isLight ? "light" : "dark"]}
|
||||
patternLightness={patternLightness}
|
||||
hueShift={hueShift}
|
||||
warpAmount={0.5}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { TokenInfo } from "thirdweb/react";
|
||||
import { BuyAndSwapEmbed } from "@/components/blocks/BuyAndSwapEmbed";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
import { bridgeAppThirdwebClient } from "../../constants";
|
||||
|
||||
export function UniversalBridgeEmbed({
|
||||
chainId,
|
||||
@@ -17,14 +16,11 @@ export function UniversalBridgeEmbed({
|
||||
const chain = useV5DashboardChain(chainId || 1);
|
||||
|
||||
return (
|
||||
<div className="w-full lg:w-[400px]">
|
||||
<BuyAndSwapEmbed
|
||||
client={bridgeAppThirdwebClient}
|
||||
chain={chain}
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
pageType="bridge"
|
||||
/>
|
||||
</div>
|
||||
<BuyAndSwapEmbed
|
||||
chain={chain}
|
||||
buyAmount={amount}
|
||||
tokenAddress={token?.address}
|
||||
pageType="bridge"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
import { ChevronRightIcon, DollarSignIcon, TrendingUpIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { reportBridgePageLinkClick } from "@/analytics/report";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PillLink(props: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
linkType: "integrate-bridge" | "trending-tokens";
|
||||
}) {
|
||||
const Icon =
|
||||
props.linkType === "integrate-bridge" ? DollarSignIcon : TrendingUpIcon;
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
target="_blank"
|
||||
onClick={() => reportBridgePageLinkClick({ linkType: props.linkType })}
|
||||
className={cn(
|
||||
"shadow-sm text-center justify-center inline-flex items-center gap-4 text-foreground group",
|
||||
"text-sm bg-card/50 backdrop-blur-lg border border-border/70",
|
||||
"rounded-full px-4 py-2.5 transition-colors duration-300 text-pretty leading-5",
|
||||
"hover:bg-pink-300/20 dark:hover:bg-pink-950/30 hover:text-foreground hover:border-pink-400 dark:hover:border-pink-800",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Icon className="size-4 shrink-0 text-pink-600" />
|
||||
{props.children}
|
||||
</div>
|
||||
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground/50 group-hover:translate-x-1 transition-all duration-300 group-hover:text-foreground" />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import { ThirdwebMiniLogo } from "../../(app)/components/ThirdwebMiniLogo";
|
||||
|
||||
export function PageHeader(props: { containerClassName?: string }) {
|
||||
return (
|
||||
<div className="border-b border-dashed">
|
||||
<div className="border-b border-border/50 border-dashed">
|
||||
<header
|
||||
className={cn(
|
||||
"container flex max-w-7xl justify-between py-3",
|
||||
"container flex max-w-7xl justify-between py-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
@@ -30,7 +30,7 @@ export function PageHeader(props: { containerClassName?: string }) {
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<ToggleThemeButton />
|
||||
<ToggleThemeButton className="bg-transparent" />
|
||||
<PublicPageConnectButton connectButtonClassName="!rounded-full" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@@ -4,14 +4,17 @@ import type { Address } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import { getCurrencyMetadata } from "thirdweb/extensions/erc20";
|
||||
import { FaqSection } from "@/components/blocks/faq-section";
|
||||
import { AppFooter } from "@/components/footers/app-footer";
|
||||
import { BridgeVeil } from "./components/BridgeVeil";
|
||||
import { PillLink } from "./components/client/pill-link";
|
||||
import { UniversalBridgeEmbed } from "./components/client/UniversalBridgeEmbed";
|
||||
import { PageHeader } from "./components/header";
|
||||
import { bridgeAppThirdwebClient } from "./constants";
|
||||
|
||||
const title = "thirdweb Payments: Swap, Bridge, and Onramp";
|
||||
const title = "thirdweb Bridge: Buy, Bridge & Swap Crypto on 85+ Chains";
|
||||
const description =
|
||||
"Swap, bridge, and on-ramp to any EVM chain with thirdweb's Payments.";
|
||||
"Bridge, swap, and on-ramp across 9M+ token routes on 85+ chains (Ethereum, Base, Optimism, Arbitrum, BNB & more). Best-price routing and near-instant fast finality";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description,
|
||||
@@ -52,37 +55,97 @@ export default async function BridgePage({
|
||||
|
||||
return (
|
||||
<div className="grow flex flex-col relative overflow-hidden">
|
||||
<PageHeader />
|
||||
|
||||
<div className="flex grow flex-col items-center justify-center py-36 px-4 min-h-dvh relative overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed
|
||||
amount={amount as string}
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
token={
|
||||
symbol && decimals && tokenName
|
||||
? {
|
||||
address: tokenAddress as Address,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="absolute top-0 left-0 right-0 h-dvh md:h-[1200px] fade-in-0 animate-in duration-700">
|
||||
<BridgeVeil />
|
||||
</div>
|
||||
|
||||
<div className="bg-background relative z-10">
|
||||
<AppFooter />
|
||||
<div className="relative z-10">
|
||||
<PageHeader />
|
||||
|
||||
<div className="h-20" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="container">
|
||||
<h1 className="text-5xl md:text-6xl font-bold mb-6 tracking-tighter text-center leading-none text-pretty">
|
||||
Bridge and Swap tokens <br className="max-sm:hidden" /> across any
|
||||
chain, instantly
|
||||
</h1>
|
||||
|
||||
<div className="flex-wrap flex gap-3 items-center justify-center">
|
||||
<DataPill>85+ Chains Supported</DataPill>
|
||||
<DataPill>3200+ Tokens Supported</DataPill>
|
||||
<DataPill>9+ Million Routes Available</DataPill>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-16" />
|
||||
|
||||
<div className="flex grow items-center justify-center px-4 relative">
|
||||
<DotsBackgroundPattern />
|
||||
<UniversalBridgeEmbed
|
||||
amount={amount as string}
|
||||
chainId={chainId ? Number(chainId) : undefined}
|
||||
token={
|
||||
symbol && decimals && tokenName
|
||||
? {
|
||||
address: tokenAddress as Address,
|
||||
name: tokenName,
|
||||
symbol,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="flex flex-col gap-4 items-center container">
|
||||
<PillLink
|
||||
href="https://portal.thirdweb.com/bridge"
|
||||
linkType="integrate-bridge"
|
||||
>
|
||||
Integrate Bridge in your apps in minutes, and start generating
|
||||
revenue
|
||||
</PillLink>
|
||||
|
||||
<PillLink
|
||||
href="https://thirdweb.com/tokens"
|
||||
linkType="trending-tokens"
|
||||
>
|
||||
Discover Trending Tokens
|
||||
</PillLink>
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="container max-w-2xl">
|
||||
<BridgeFaqSection />
|
||||
</div>
|
||||
|
||||
<div className="h-32" />
|
||||
|
||||
<div className="relative">
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DataPill(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p className="flex items-center gap-1.5 text-foreground/50 text-xs bg-accent/30 backdrop-blur-lg border border-border/70 rounded-full px-3 py-1.5 hover:text-foreground transition-colors duration-300">
|
||||
{props.children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function DotsBackgroundPattern(props: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 text-foreground/30 dark:text-foreground/10",
|
||||
"pointer-events-none absolute -inset-x-36 -inset-y-24 text-pink-700/30 dark:text-pink-500/20",
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
@@ -94,3 +157,35 @@ function DotsBackgroundPattern(props: { className?: string }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const bridgeFaqs: Array<{ title: string; description: string }> = [
|
||||
{
|
||||
title: "What is bridging in crypto?",
|
||||
description:
|
||||
"Crypto bridging (cross-chain bridging) moves tokens between blockchains so you can use assets across networks. In thirdweb Bridge, connect your wallet, choose the source token/network and destination token/network, review the route and price, then confirm. Assets arrive after finality, often under ~10 seconds on fast routes, though timing depends on networks and congestion.",
|
||||
},
|
||||
{
|
||||
title: "How does crypto bridging work?",
|
||||
description:
|
||||
"Bridge smart contracts lock or burn tokens on the source chain and mint or release equivalents on the destination via verified cross-chain providers. thirdweb Bridge automatically finds the fastest, lowest-cost route and may use different mechanisms based on networks and liquidity. Arrival can range from seconds to minutes depending on finality; many routes complete in ~10 seconds",
|
||||
},
|
||||
{
|
||||
title: "What is a crypto asset swap?",
|
||||
description:
|
||||
"A crypto swap exchanges one token for another via a DEX or aggregator. thirdweb Bridge lets you bridge + swap in one step. For example, ETH on Ethereum to USDC on Base, by selecting your start and end tokens/networks and confirming.",
|
||||
},
|
||||
{
|
||||
title: "How can I get stablecoins like USDC or USDT?",
|
||||
description:
|
||||
"Use thirdweb Bridge to convert assets you hold into USDC or USDT on your chosen network: select your current token/network, pick the stablecoin (USDC, USDT, etc) on the destination, and confirm. You can also buy stablecoins with fiat in the Buy flow and bridge if needed. Always verify official token contract addresses.",
|
||||
},
|
||||
{
|
||||
title: "What is the cost of bridging and swapping?",
|
||||
description:
|
||||
"Costs include gas on each chain, bridge/liquidity provider fees, and any DEX swap fees or price impact. thirdweb Bridge compares routes and selects the best price route. Save by using lower-gas times or combining bridge + swap in one flow.",
|
||||
},
|
||||
];
|
||||
|
||||
function BridgeFaqSection() {
|
||||
return <FaqSection faqs={bridgeFaqs} />;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,20 @@ import { defineDashboardChain } from "@/lib/defineDashboardChain";
|
||||
import { isLoginRequired } from "@/utils/auth";
|
||||
import { LAST_VISITED_TEAM_PAGE_PATH } from "./app/(app)/team/components/last-visited-page/consts";
|
||||
|
||||
type CookiesToSet = Record<
|
||||
string,
|
||||
| [string]
|
||||
| [
|
||||
string,
|
||||
{
|
||||
httpOnly: boolean;
|
||||
sameSite?: "lax" | "strict" | "none";
|
||||
secure: boolean;
|
||||
maxAge: number;
|
||||
},
|
||||
]
|
||||
>;
|
||||
|
||||
// ignore assets, api - only intercept page routes
|
||||
export const config = {
|
||||
matcher: [
|
||||
@@ -24,12 +38,12 @@ export const config = {
|
||||
};
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const { pathname, searchParams } = request.nextUrl;
|
||||
|
||||
// nebula subdomain handling
|
||||
const paths = pathname.slice(1).split("/");
|
||||
|
||||
let cookiesToSet: Record<string, string> | undefined;
|
||||
let cookiesToSet: CookiesToSet = {};
|
||||
|
||||
const activeAccount = request.cookies.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const authCookie = activeAccount
|
||||
@@ -56,12 +70,27 @@ export async function middleware(request: NextRequest) {
|
||||
cookiesToSet = {};
|
||||
}
|
||||
|
||||
cookiesToSet[key] = value;
|
||||
cookiesToSet[key] = [value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle gclid (if it exists in search params)
|
||||
const gclid = searchParams.get("gclid");
|
||||
if (gclid) {
|
||||
cookiesToSet.gclid = [
|
||||
gclid,
|
||||
{
|
||||
// TODO: define conversion window, for now 7d should do fine
|
||||
maxAge: 7 * 24 * 60 * 60,
|
||||
httpOnly: false,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// logged in paths
|
||||
if (isLoginRequired(pathname)) {
|
||||
// check if the user is logged in (has a valid auth cookie)
|
||||
@@ -146,7 +175,7 @@ export async function middleware(request: NextRequest) {
|
||||
if (cookiesToSet) {
|
||||
const defaultResponse = NextResponse.next();
|
||||
for (const entry of Object.entries(cookiesToSet)) {
|
||||
defaultResponse.cookies.set(entry[0], entry[1]);
|
||||
defaultResponse.cookies.set(entry[0], entry[1][0], entry[1][1]);
|
||||
}
|
||||
|
||||
return defaultResponse;
|
||||
@@ -162,7 +191,7 @@ function isPossibleAddressOrENSName(address: string) {
|
||||
function rewrite(
|
||||
request: NextRequest,
|
||||
relativePath: string,
|
||||
cookiesToSet: Record<string, string> | undefined,
|
||||
cookiesToSet: CookiesToSet,
|
||||
) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = relativePath;
|
||||
@@ -170,7 +199,7 @@ function rewrite(
|
||||
|
||||
if (cookiesToSet) {
|
||||
for (const entry of Object.entries(cookiesToSet)) {
|
||||
res.cookies.set(entry[0], entry[1]);
|
||||
res.cookies.set(entry[0], entry[1][0], entry[1][1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +213,7 @@ function redirect(
|
||||
| {
|
||||
searchParams?: string;
|
||||
permanent?: boolean;
|
||||
cookiesToSet?: Record<string, string> | undefined;
|
||||
cookiesToSet?: CookiesToSet;
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
@@ -196,7 +225,7 @@ function redirect(
|
||||
|
||||
if (options?.cookiesToSet) {
|
||||
for (const entry of Object.entries(options.cookiesToSet)) {
|
||||
res.cookies.set(entry[0], entry[1]);
|
||||
res.cookies.set(entry[0], entry[1][0], entry[1][1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,15 @@ const baseNextConfig: NextConfig = {
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
reactStrictMode: true,
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
destination: "https://thirdweb.com/ai",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ export function EmptyStateChatPageContent(props: {
|
||||
onLoginClick: undefined | (() => void);
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden py-10 lg:py-16">
|
||||
<div className="relative overflow-hidden py-10 lg:py-16">
|
||||
{props.showAurora && (
|
||||
<Aurora className="top-0 left-1/2 h-[800px] w-[1000px] text-[hsl(var(--nebula-pink-foreground)/8%)] lg:w-[150%] dark:text-[hsl(var(--nebula-pink-foreground)/10%)]" />
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"geist": "^1.5.1",
|
||||
"lucide-react": "0.525.0",
|
||||
"next": "15.3.5",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -48,7 +49,6 @@
|
||||
"thirdweb": "workspace:*",
|
||||
"use-debounce": "^10.0.5",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"x402-next": "^0.6.1",
|
||||
"zod": "3.25.75"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,6 +5,6 @@ export const maxDuration = 300;
|
||||
export async function GET(_req: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Congratulations! You have accessed the protected route.",
|
||||
message: "Payment successful. You have accessed the protected route.",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BotIcon,
|
||||
BoxIcon,
|
||||
BringToFrontIcon,
|
||||
CircleDollarSignIcon,
|
||||
CircleUserIcon,
|
||||
CreditCardIcon,
|
||||
DollarSignIcon,
|
||||
@@ -186,6 +187,13 @@ export const paymentsFeatureCards: FeatureCardMetadata[] = [
|
||||
description:
|
||||
"Enable users to pay for onchain transactions with fiat or crypto",
|
||||
},
|
||||
{
|
||||
icon: CircleDollarSignIcon,
|
||||
title: "x402",
|
||||
link: "/payments/x402",
|
||||
description:
|
||||
"Use the x402 payment protocol to pay for API calls using any web3 wallet",
|
||||
},
|
||||
];
|
||||
|
||||
export const accountAbstractionsFeatureCards: FeatureCardMetadata[] = [
|
||||
|
||||
@@ -1,26 +1,15 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AppSidebarLayout } from "./AppSidebar";
|
||||
import { Providers } from "./providers";
|
||||
import "@workspace/ui/global.css";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
const monoFont = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
weight: "variable",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "thirdweb playground",
|
||||
metadataBase,
|
||||
@@ -37,8 +26,8 @@ export default async function RootLayout({
|
||||
<body
|
||||
className={cn(
|
||||
"bg-background font-sans antialiased ",
|
||||
sansFont.variable,
|
||||
monoFont.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
|
||||
@@ -18,6 +18,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
|
||||
sellerAddress: "0x0000000000000000000000000000000000000000",
|
||||
title: "",
|
||||
transactionData: "",
|
||||
receiverAddress: undefined,
|
||||
currency: "USD",
|
||||
showThirdwebBranding: true,
|
||||
},
|
||||
|
||||
@@ -118,8 +118,12 @@ tokenId: 2n,
|
||||
tokenAddress: options.payOptions.buyTokenAddress
|
||||
? quotes(options.payOptions.buyTokenAddress)
|
||||
: undefined,
|
||||
seller: options.payOptions.sellerAddress
|
||||
? quotes(options.payOptions.sellerAddress)
|
||||
seller:
|
||||
widget === "checkout"
|
||||
? quotes(options.payOptions.sellerAddress)
|
||||
: undefined,
|
||||
receiverAddress: options.payOptions.receiverAddress
|
||||
? quotes(options.payOptions.receiverAddress)
|
||||
: undefined,
|
||||
buttonLabel: options.payOptions.buttonLabel
|
||||
? quotes(options.payOptions.buttonLabel)
|
||||
|
||||
@@ -140,6 +140,7 @@ export function LeftSection(props: {
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Token</Label>
|
||||
<TokenSelector
|
||||
disableAddress
|
||||
chainId={selectedChain}
|
||||
client={THIRDWEB_CLIENT}
|
||||
enabled={true}
|
||||
@@ -154,10 +155,10 @@ export function LeftSection(props: {
|
||||
)}
|
||||
|
||||
{/* Mode-specific form fields */}
|
||||
<div className="my-2">
|
||||
<div className="">
|
||||
{/* Buy Mode - Amount and Payment Methods */}
|
||||
{props.widget === "buy" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={buyTokenAmountId}>Amount</Label>
|
||||
<Input
|
||||
@@ -177,6 +178,30 @@ export function LeftSection(props: {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{props.widget === "buy" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="receiver-address">Receiver Address</Label>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Receive the tokens in a different wallet address
|
||||
</p>
|
||||
<Input
|
||||
className="bg-card"
|
||||
id="receiver-address"
|
||||
onChange={(e) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
payOptions: {
|
||||
...v.payOptions,
|
||||
receiverAddress: e.target.value as Address,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
placeholder="0x..."
|
||||
value={payOptions.receiverAddress || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Methods */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Payment Methods</Label>
|
||||
@@ -206,6 +231,7 @@ export function LeftSection(props: {
|
||||
/>
|
||||
<Label htmlFor={cryptoPaymentId}>Crypto</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={payOptions.paymentMethods.includes("card")}
|
||||
@@ -257,7 +283,6 @@ export function LeftSection(props: {
|
||||
value={payOptions.sellerAddress || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={paymentAmountId}>Price</Label>
|
||||
<Input
|
||||
|
||||
@@ -67,6 +67,12 @@ export function RightSection(props: {
|
||||
tokenAddress={props.options.payOptions.buyTokenAddress}
|
||||
currency={props.options.payOptions.currency}
|
||||
showThirdwebBranding={props.options.payOptions.showThirdwebBranding}
|
||||
receiverAddress={props.options.payOptions.receiverAddress}
|
||||
key={JSON.stringify({
|
||||
amount: props.options.payOptions.buyTokenAmount,
|
||||
chain: props.options.payOptions.buyTokenChain,
|
||||
tokenAddress: props.options.payOptions.buyTokenAddress,
|
||||
})}
|
||||
{...(props.options.payOptions.buttonLabel && {
|
||||
buttonLabel: props.options.payOptions.buttonLabel,
|
||||
})}
|
||||
@@ -89,7 +95,6 @@ export function RightSection(props: {
|
||||
}
|
||||
name={props.options.payOptions.title || "Your Product Name"}
|
||||
paymentMethods={props.options.payOptions.paymentMethods}
|
||||
presetOptions={[1, 2, 3]}
|
||||
seller={props.options.payOptions.sellerAddress}
|
||||
theme={themeObj}
|
||||
tokenAddress={props.options.payOptions.buyTokenAddress}
|
||||
|
||||
@@ -48,6 +48,7 @@ export type BridgeComponentsPlaygroundOptions = {
|
||||
|
||||
// direct_payment mode options
|
||||
sellerAddress: Address;
|
||||
receiverAddress: Address | undefined;
|
||||
|
||||
// transaction mode options
|
||||
transactionData?: string; // Simplified for demo; could be more complex in real implementation
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
|
||||
paymentMethods: ["crypto", "card"],
|
||||
sellerAddress: "0x0000000000000000000000000000000000000000",
|
||||
title: "",
|
||||
receiverAddress: undefined,
|
||||
transactionData: "",
|
||||
currency: "USD",
|
||||
showThirdwebBranding: true,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { OverviewPage } from "@/components/blocks/OverviewPage";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { paymentsFeatureCards } from "../data/pages-metadata";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<OverviewPage
|
||||
title="Payments"
|
||||
description="thirdweb Payments allow developers to create advanced payment flows to monetize their apps through product sales, peer to peer payments, token sales, and more"
|
||||
featureCards={paymentsFeatureCards}
|
||||
icon={PayIcon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ const defaultOptions: BridgeComponentsPlaygroundOptions = {
|
||||
transactionData: "",
|
||||
currency: "USD",
|
||||
showThirdwebBranding: true,
|
||||
receiverAddress: undefined,
|
||||
},
|
||||
theme: {
|
||||
darkColorOverrides: {},
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TokenSelector } from "@/components/ui/TokenSelector";
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import type { TokenMetadata } from "@/lib/types";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
|
||||
export function X402LeftSection(props: {
|
||||
options: X402PlaygroundOptions;
|
||||
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
|
||||
}) {
|
||||
const { options, setOptions } = props;
|
||||
|
||||
// Local state for chain and token selection
|
||||
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
|
||||
return options.chain?.id;
|
||||
});
|
||||
|
||||
const [selectedToken, setSelectedToken] = useState<
|
||||
{ chainId: number; address: string } | undefined
|
||||
>(() => {
|
||||
if (options.tokenAddress && options.chain?.id) {
|
||||
return {
|
||||
address: options.tokenAddress,
|
||||
chainId: options.chain.id,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const chainId = useId();
|
||||
const tokenId = useId();
|
||||
const amountId = useId();
|
||||
const waitUntilId = useId();
|
||||
|
||||
const handleChainChange = (chainId: number) => {
|
||||
setSelectedChain(chainId);
|
||||
// Clear token selection when chain changes
|
||||
setSelectedToken(undefined);
|
||||
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
chain: defineChain(chainId),
|
||||
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
|
||||
tokenSymbol: "",
|
||||
tokenDecimals: 18,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTokenChange = (token: TokenMetadata) => {
|
||||
setSelectedToken({
|
||||
address: token.address,
|
||||
chainId: selectedChain!,
|
||||
});
|
||||
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
tokenAddress: token.address as `0x${string}`,
|
||||
tokenSymbol: token.symbol ?? "",
|
||||
tokenDecimals: token.decimals ?? 18,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
amount: e.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleWaitUntilChange = (
|
||||
value: "simulated" | "submitted" | "confirmed",
|
||||
) => {
|
||||
setOptions((v) => ({
|
||||
...v,
|
||||
waitUntil: value,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
|
||||
<div className="space-y-4">
|
||||
{/* Chain selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={chainId}>Chain</Label>
|
||||
<BridgeNetworkSelector
|
||||
chainId={selectedChain}
|
||||
onChange={handleChainChange}
|
||||
placeholder="Select a chain"
|
||||
className="bg-card"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Token selection - only show if chain is selected */}
|
||||
{selectedChain && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={tokenId}>Token</Label>
|
||||
<TokenSelector
|
||||
chainId={selectedChain}
|
||||
client={THIRDWEB_CLIENT}
|
||||
enabled={true}
|
||||
onChange={handleTokenChange}
|
||||
placeholder="Select a token"
|
||||
selectedToken={selectedToken}
|
||||
className="bg-card"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Amount input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={amountId}>Amount</Label>
|
||||
<Input
|
||||
id={amountId}
|
||||
type="text"
|
||||
placeholder="0.01"
|
||||
value={options.amount}
|
||||
onChange={handleAmountChange}
|
||||
className="bg-card"
|
||||
/>
|
||||
{options.tokenSymbol && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Amount in {options.tokenSymbol}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wait Until selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor={waitUntilId}>Wait Until</Label>
|
||||
<Select
|
||||
value={options.waitUntil}
|
||||
onValueChange={handleWaitUntilChange}
|
||||
>
|
||||
<SelectTrigger className="bg-card">
|
||||
<SelectValue placeholder="Select wait condition" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="simulated">Simulated</SelectItem>
|
||||
<SelectItem value="submitted">Submitted</SelectItem>
|
||||
<SelectItem value="confirmed">Confirmed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
When to consider the payment settled: simulated (fastest),
|
||||
submitted (medium), or confirmed (most secure)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { chain, token } from "./constants";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
import { X402LeftSection } from "./X402LeftSection";
|
||||
import { X402RightSection } from "./X402RightSection";
|
||||
|
||||
const defaultOptions: X402PlaygroundOptions = {
|
||||
chain: chain,
|
||||
tokenAddress: token.address as `0x${string}`,
|
||||
tokenSymbol: token.symbol,
|
||||
tokenDecimals: token.decimals,
|
||||
amount: "0.01",
|
||||
payTo: "0x0000000000000000000000000000000000000000",
|
||||
waitUntil: "simulated",
|
||||
};
|
||||
|
||||
export function X402Playground() {
|
||||
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
|
||||
const activeAccount = useActiveAccount();
|
||||
|
||||
// Update payTo address when wallet connects, but only if it's still the default
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
activeAccount?.address &&
|
||||
options.payTo === "0x0000000000000000000000000000000000000000"
|
||||
) {
|
||||
setOptions((prev) => ({
|
||||
...prev,
|
||||
payTo: activeAccount.address as `0x${string}`,
|
||||
}));
|
||||
}
|
||||
}, [activeAccount?.address, options.payTo]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
|
||||
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
|
||||
<X402LeftSection options={options} setOptions={setOptions} />
|
||||
</div>
|
||||
<X402RightSection options={options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ConnectButton,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { X402PlaygroundOptions } from "./types";
|
||||
|
||||
type Tab = "ui" | "client-code" | "server-code";
|
||||
|
||||
export function X402RightSection(props: { options: X402PlaygroundOptions }) {
|
||||
const pathname = usePathname();
|
||||
const [previewTab, _setPreviewTab] = useState<Tab>(() => {
|
||||
return "ui";
|
||||
});
|
||||
|
||||
function setPreviewTab(tab: "ui" | "client-code" | "server-code") {
|
||||
_setPreviewTab(tab);
|
||||
window.history.replaceState({}, "", `${pathname}?tab=${tab}`);
|
||||
}
|
||||
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeAccount = useActiveAccount();
|
||||
|
||||
const paidApiCall = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!activeWallet) {
|
||||
throw new Error("No active wallet");
|
||||
}
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
BigInt(1 * 10 ** 18),
|
||||
);
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("chainId", props.options.chain.id.toString());
|
||||
searchParams.set("payTo", props.options.payTo);
|
||||
searchParams.set("amount", props.options.amount);
|
||||
searchParams.set("tokenAddress", props.options.tokenAddress);
|
||||
searchParams.set("decimals", props.options.tokenDecimals.toString());
|
||||
searchParams.set("waitUntil", props.options.waitUntil);
|
||||
|
||||
const url =
|
||||
"/api/paywall" +
|
||||
(searchParams.size > 0 ? "?" + searchParams.toString() : "");
|
||||
const response = await fetchWithPay(url.toString());
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handlePayClick = async () => {
|
||||
paidApiCall.mutate();
|
||||
};
|
||||
|
||||
const clientCode = `import { createThirdwebClient } from "thirdweb";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
|
||||
export default function Page() {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
const onClick = async () => {
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('/api/paid-endpoint');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick}>Pay Now</Button>
|
||||
);
|
||||
}`;
|
||||
|
||||
const serverCode = `// Usage in a Next.js API route
|
||||
import { settlePayment, facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { defineChain } from "thirdweb/chains";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY,
|
||||
});
|
||||
|
||||
const thirdwebFacilitator = facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0xYourServerWalletAddress",
|
||||
waitUtil: "${props.options.waitUntil}",
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const paymentData = request.headers.get("x-payment");
|
||||
|
||||
// verify and process the payment
|
||||
const result = await settlePayment({
|
||||
resourceUrl: "https://api.example.com/premium-content",
|
||||
method: "POST",
|
||||
paymentData,
|
||||
payTo: "${props.options.payTo}",
|
||||
network: defineChain(${props.options.chain.id}),
|
||||
price: {
|
||||
amount: "${Number(props.options.amount) * 10 ** props.options.tokenDecimals}",
|
||||
asset: {
|
||||
address: "${props.options.tokenAddress}",
|
||||
},
|
||||
},
|
||||
facilitator: thirdwebFacilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// Payment verified and settled successfully
|
||||
return Response.json({ data: "premium content" });
|
||||
} else {
|
||||
// Payment required
|
||||
return Response.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:w-[764px]">
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
isActive: previewTab === "ui",
|
||||
name: "UI",
|
||||
onClick: () => setPreviewTab("ui"),
|
||||
},
|
||||
{
|
||||
isActive: previewTab === "client-code",
|
||||
name: "Client Code",
|
||||
onClick: () => setPreviewTab("client-code"),
|
||||
},
|
||||
{
|
||||
isActive: previewTab === "server-code",
|
||||
name: "Server Code",
|
||||
onClick: () => setPreviewTab("server-code"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex min-h-[200px] grow justify-center rounded-lg",
|
||||
previewTab === "ui" && "items-center",
|
||||
)}
|
||||
>
|
||||
<BackgroundPattern />
|
||||
|
||||
{previewTab === "ui" && (
|
||||
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
|
||||
<ConnectButton
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={props.options.chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken:
|
||||
props.options.tokenAddress !==
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
? {
|
||||
[props.options.chain.id]: props.options.tokenAddress,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
supportedTokens={
|
||||
props.options.tokenAddress !==
|
||||
"0x0000000000000000000000000000000000000000"
|
||||
? {
|
||||
[props.options.chain.id]: [
|
||||
{
|
||||
address: props.options.tokenAddress,
|
||||
symbol: props.options.tokenSymbol,
|
||||
name: props.options.tokenSymbol,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LockIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">Paid API Call</span>
|
||||
<Badge variant="success">
|
||||
<span className="text-xl font-bold">
|
||||
{props.options.amount}{" "}
|
||||
{props.options.tokenSymbol || "tokens"}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
className="w-full mb-4"
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Access Premium Content
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Pay for access with {props.options.tokenSymbol || "tokens"} on{" "}
|
||||
{props.options.chain.name || `chain ${props.options.chain.id}`}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CodeIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">API Call Response</span>
|
||||
</div>
|
||||
{paidApiCall.isPending && (
|
||||
<div className="text-center">Loading...</div>
|
||||
)}
|
||||
{paidApiCall.isError && (
|
||||
<div className="text-center">
|
||||
Error: {paidApiCall.error.message}
|
||||
</div>
|
||||
)}
|
||||
{paidApiCall.data && (
|
||||
<CodeClient
|
||||
code={JSON.stringify(paidApiCall.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewTab === "client-code" && (
|
||||
<div className="w-full">
|
||||
<CodeClient
|
||||
className="h-full rounded-none border-none"
|
||||
code={clientCode}
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{previewTab === "server-code" && (
|
||||
<div className="w-full">
|
||||
<CodeClient
|
||||
className="h-full rounded-none border-none"
|
||||
code={serverCode}
|
||||
lang="typescript"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BackgroundPattern() {
|
||||
const color = "hsl(var(--foreground)/15%)";
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-[-1]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(${color} 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 100% 100% at 50% 50%, black 30%, transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabButtons(props: {
|
||||
tabs: Array<{
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-start gap-1 rounded-lg border bg-card p-2 shadow-md md:inline-flex">
|
||||
{props.tabs.map((tab) => (
|
||||
<Button
|
||||
className={cn(
|
||||
"gap-2 px-4 text-base",
|
||||
tab.isActive
|
||||
? "bg-accent text-foreground"
|
||||
: "bg-transparent text-muted-foreground",
|
||||
)}
|
||||
key={tab.name}
|
||||
onClick={tab.onClick}
|
||||
variant="ghost"
|
||||
>
|
||||
{tab.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { base } from "thirdweb/chains";
|
||||
|
||||
export const chain = base;
|
||||
export const token = {
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
decimals: 6,
|
||||
};
|
||||
// export const chain = base;
|
||||
// export const token = {
|
||||
// address: "0x0578d8A44db98B23BF096A382e016e29a5Ce0ffe",
|
||||
// decimals: 18,
|
||||
// name: "Higher",
|
||||
// symbol: "HIGHER",
|
||||
// version: "1",
|
||||
// };
|
||||
// export const token = {
|
||||
// address: "0xfdcC3dd6671eaB0709A4C0f3F53De9a333d80798",
|
||||
// decimals: 18,
|
||||
// name: "Stable Coin",
|
||||
// symbol: "SBC",
|
||||
// version: "1",
|
||||
// // primaryType: "Permit",
|
||||
// }
|
||||
// export const chain = defineChain(3338);
|
||||
// export const token = {
|
||||
// address: "0xbbA60da06c2c5424f03f7434542280FCAd453d10",
|
||||
// decimals: 6,
|
||||
// name: "USDC",
|
||||
// symbol: "USDC",
|
||||
// version: "2",
|
||||
// }
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Chain } from "thirdweb/chains";
|
||||
import type { Address } from "thirdweb/utils";
|
||||
|
||||
export type X402PlaygroundOptions = {
|
||||
chain: Chain;
|
||||
tokenAddress: Address;
|
||||
tokenSymbol: string;
|
||||
tokenDecimals: number;
|
||||
amount: string;
|
||||
payTo: Address;
|
||||
waitUntil: "simulated" | "submitted" | "confirmed";
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
import { CodeIcon, LockIcon } from "lucide-react";
|
||||
import { baseSepolia } from "thirdweb/chains";
|
||||
import {
|
||||
ConnectButton,
|
||||
getDefaultToken,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
} from "thirdweb/react";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { THIRDWEB_CLIENT } from "../../../../lib/client";
|
||||
|
||||
const chain = baseSepolia;
|
||||
const token = getDefaultToken(chain, "USDC");
|
||||
|
||||
export function X402ClientPreview() {
|
||||
const activeWallet = useActiveWallet();
|
||||
const activeAccount = useActiveAccount();
|
||||
const paidApiCall = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!activeWallet) {
|
||||
throw new Error("No active wallet");
|
||||
}
|
||||
const fetchWithPay = wrapFetchWithPayment(
|
||||
fetch,
|
||||
THIRDWEB_CLIENT,
|
||||
activeWallet,
|
||||
);
|
||||
const response = await fetchWithPay("/api/paywall");
|
||||
return response.json();
|
||||
},
|
||||
});
|
||||
|
||||
const handlePayClick = async () => {
|
||||
paidApiCall.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full p-4 md:p-12 max-w-lg mx-auto">
|
||||
<ConnectButton
|
||||
client={THIRDWEB_CLIENT}
|
||||
chain={chain}
|
||||
detailsButton={{
|
||||
displayBalanceToken: {
|
||||
[chain.id]: token!.address,
|
||||
},
|
||||
}}
|
||||
supportedTokens={{
|
||||
[chain.id]: [token!],
|
||||
}}
|
||||
/>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<LockIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">Paid API Call</span>
|
||||
<span className="text-xl font-bold text-red-600">$0.01</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayClick}
|
||||
className="w-full mb-4"
|
||||
size="lg"
|
||||
disabled={paidApiCall.isPending || !activeAccount}
|
||||
>
|
||||
Pay Now
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{" "}
|
||||
<a
|
||||
className="underline"
|
||||
href={"https://faucet.circle.com/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Click here to get USDC on {chain.name}
|
||||
</a>
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<CodeIcon className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">API Call Response</span>
|
||||
</div>
|
||||
{paidApiCall.isPending && <div className="text-center">Loading...</div>}
|
||||
{paidApiCall.isError && (
|
||||
<div className="text-center">Error: {paidApiCall.error.message}</div>
|
||||
)}
|
||||
{paidApiCall.data && (
|
||||
<CodeClient
|
||||
code={JSON.stringify(paidApiCall.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { CodeServer } from "@workspace/ui/components/code/code.server";
|
||||
import { CircleDollarSignIcon, Code2Icon } from "lucide-react";
|
||||
import { CodeExample, TabName } from "@/components/code/code-example";
|
||||
import { CircleDollarSignIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
import { X402ClientPreview } from "./components/x402-client-preview";
|
||||
import { X402Playground } from "./components/X402Playground";
|
||||
|
||||
const title = "x402 Payments";
|
||||
const description =
|
||||
@@ -30,98 +28,8 @@ export default function Page() {
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments/x402?utm_source=playground"
|
||||
>
|
||||
<X402Example />
|
||||
<div className="h-8" />
|
||||
<ServerCodeExample />
|
||||
<X402Playground />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ServerCodeExample() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h2 className="font-semibold text-xl tracking-tight">
|
||||
Next.js Server Code Example
|
||||
</h2>
|
||||
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
|
||||
Use any x402 middleware + the thirdweb facilitator to settle
|
||||
transactions with our server wallet.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
<div className="flex grow flex-col border-b md:border-r md:border-b-0">
|
||||
<TabName icon={Code2Icon} name="Server Code" />
|
||||
<CodeServer
|
||||
className="h-full rounded-none border-none"
|
||||
code={`// src/middleware.ts
|
||||
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
|
||||
const client = createThirdwebClient({ secretKey: "your-secret-key" });
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
"0xYourWalletAddress",
|
||||
{
|
||||
"/api/paid-endpoint": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
},
|
||||
},
|
||||
facilitator({
|
||||
client,
|
||||
serverWalletAddress: "0xYourServerWalletAddress",
|
||||
}),
|
||||
);
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
matcher: ["/api/paid-endpoint"],
|
||||
};
|
||||
|
||||
`}
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function X402Example() {
|
||||
return (
|
||||
<CodeExample
|
||||
header={{
|
||||
title: "Client Code Example",
|
||||
description:
|
||||
"Wrap your fetch requests with the `wrapFetchWithPayment` function to enable x402 payments.",
|
||||
}}
|
||||
code={`import { createThirdwebClient } from "thirdweb";
|
||||
import { wrapFetchWithPayment } from "thirdweb/x402";
|
||||
import { useActiveWallet } from "thirdweb/react";
|
||||
|
||||
const client = createThirdwebClient({ clientId: "your-client-id" });
|
||||
|
||||
export default function Page() {
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
const onClick = async () => {
|
||||
const fetchWithPay = wrapFetchWithPayment(fetch, client, wallet);
|
||||
const response = await fetchWithPay('/api/paid-endpoint');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={onClick}>Pay Now</Button>
|
||||
);
|
||||
}`}
|
||||
lang="tsx"
|
||||
preview={<X402ClientPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export function RightSection(props: {
|
||||
}
|
||||
auth={connectOptions.enableAuth ? playgroundAuth : undefined}
|
||||
autoConnect={false}
|
||||
className="shadow-xl !max-w-full"
|
||||
className="shadow-xl"
|
||||
client={THIRDWEB_CLIENT}
|
||||
header={{
|
||||
title: connectOptions.modalTitle,
|
||||
|
||||
@@ -19,7 +19,7 @@ export function OverviewPage(props: {
|
||||
<h1 className="mb-2 font-bold text-5xl tracking-tight">
|
||||
{props.title}
|
||||
</h1>
|
||||
<p className="text-base text-muted-foreground leading-normal text-pretty">
|
||||
<p className="text-base text-muted-foreground leading-normal text-pretty max-w-[80%]">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,6 @@ export function StyledConnectEmbed(
|
||||
abstract,
|
||||
]}
|
||||
client={THIRDWEB_CLIENT}
|
||||
className="!max-w-full"
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
wallets={WALLETS}
|
||||
{...props}
|
||||
|
||||
@@ -1,34 +1,83 @@
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { facilitator } from "thirdweb/x402";
|
||||
import { paymentMiddleware } from "x402-next";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { createThirdwebClient, defineChain } from "thirdweb";
|
||||
import { toUnits } from "thirdweb/utils";
|
||||
import { facilitator, settlePayment } from "thirdweb/x402";
|
||||
import { token } from "./app/payments/x402/components/constants";
|
||||
|
||||
const client = createThirdwebClient({
|
||||
secretKey: process.env.THIRDWEB_SECRET_KEY as string,
|
||||
});
|
||||
|
||||
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;
|
||||
// const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_SMART_WALLET as string;
|
||||
const ENGINE_VAULT_ACCESS_TOKEN = process.env
|
||||
.ENGINE_VAULT_ACCESS_TOKEN as string;
|
||||
const API_URL = `https://${process.env.NEXT_PUBLIC_API_URL || "api.thirdweb.com"}`;
|
||||
|
||||
export const middleware = paymentMiddleware(
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024",
|
||||
{
|
||||
"/api/paywall": {
|
||||
price: "$0.01",
|
||||
network: "base-sepolia",
|
||||
config: {
|
||||
description: "Access to paid content",
|
||||
const twFacilitator = facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
serverWalletAddress: BACKEND_WALLET_ADDRESS,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const pathname = request.nextUrl.pathname;
|
||||
const method = request.method.toUpperCase();
|
||||
const resourceUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}${pathname}`;
|
||||
const paymentData = request.headers.get("X-PAYMENT");
|
||||
const queryParams = request.nextUrl.searchParams;
|
||||
|
||||
const chainId = queryParams.get("chainId");
|
||||
|
||||
if (!chainId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required parameters" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const amount = queryParams.get("amount") || "0.01";
|
||||
const tokenAddress = queryParams.get("tokenAddress") || token.address;
|
||||
const decimals = queryParams.get("decimals") || token.decimals.toString();
|
||||
const waitUntil =
|
||||
(queryParams.get("waitUntil") as "simulated" | "submitted" | "confirmed") ||
|
||||
"simulated";
|
||||
|
||||
const result = await settlePayment({
|
||||
resourceUrl,
|
||||
method,
|
||||
paymentData,
|
||||
network: defineChain(Number(chainId)),
|
||||
price: {
|
||||
amount: toUnits(amount, parseInt(decimals)).toString(),
|
||||
asset: {
|
||||
address: tokenAddress as `0x${string}`,
|
||||
decimals: decimals ? parseInt(decimals) : token.decimals,
|
||||
},
|
||||
},
|
||||
},
|
||||
facilitator({
|
||||
baseUrl: `${API_URL}/v1/payments/x402`,
|
||||
client,
|
||||
serverWalletAddress: BACKEND_WALLET_ADDRESS,
|
||||
vaultAccessToken: ENGINE_VAULT_ACCESS_TOKEN,
|
||||
}),
|
||||
);
|
||||
routeConfig: {
|
||||
description: "Access to paid content",
|
||||
},
|
||||
waitUntil,
|
||||
facilitator: twFacilitator,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
// payment successful, execute the request
|
||||
const response = NextResponse.next();
|
||||
for (const [key, value] of Object.entries(result.responseHeaders)) {
|
||||
response.headers.set(key, value);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// otherwise, request payment
|
||||
return NextResponse.json(result.responseBody, {
|
||||
status: result.status,
|
||||
headers: result.responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure which paths the middleware should run on
|
||||
export const config = {
|
||||
|
||||
@@ -3,6 +3,16 @@ import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
...tailwindConfig,
|
||||
theme: {
|
||||
...tailwindConfig.theme,
|
||||
extend: {
|
||||
...tailwindConfig.theme?.extend,
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-sans)"],
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
},
|
||||
},
|
||||
},
|
||||
content: [
|
||||
"./src/**/*.{ts,tsx}",
|
||||
// add contents of ui package
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"flexsearch": "^0.7.43",
|
||||
"geist": "^1.5.1",
|
||||
"github-slugger": "^2.0.0",
|
||||
"he": "^1.2.0",
|
||||
"lucide-react": "0.525.0",
|
||||
|
||||
@@ -414,7 +414,7 @@ const reactNativeRedirects = {
|
||||
|
||||
const unityRedirects = {
|
||||
// top level
|
||||
"/unity": "/unity/v5",
|
||||
"/unity": "/unity/v6",
|
||||
// blocks
|
||||
"/unity/blocks/getblock": "/unity/v4/blocks/getblock",
|
||||
"/unity/blocks/getblockwithtransactions":
|
||||
|
||||
@@ -213,133 +213,129 @@ export function Header() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<header className="flex w-full flex-col gap-2 border-b bg-background p-4 xl:pb-0 lg:px-8 overflow-hidden">
|
||||
{/* Top row */}
|
||||
<div className="container flex items-center justify-between gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden xl:block">
|
||||
<header className="flex w-full border-b overflow-hidden relative">
|
||||
<div className="container flex-col">
|
||||
{/* Top row */}
|
||||
<div className="flex items-center justify-between gap-6 relative z-10 py-4 lg:pt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-2"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
<ThirdwebIcon className="size-8" />
|
||||
<span className="font-bold text-[23px] text-foreground leading-none tracking-tight">
|
||||
Docs
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden xl:block">
|
||||
<Link
|
||||
className="text-foreground"
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="size-6 lg:size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
</div>
|
||||
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
<div className="hidden xl:block">
|
||||
<DocSearch variant="search" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
<div className="hidden xl:block">
|
||||
<ChatButton />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 xl:hidden">
|
||||
<ThemeSwitcher className="border-none bg-transparent" />
|
||||
<DocSearch variant="icon" />
|
||||
<ChatButton />
|
||||
<Button
|
||||
className="p-2"
|
||||
onClick={() => setShowBurgerMenu(!showBurgerMenu)}
|
||||
variant="ghost"
|
||||
>
|
||||
<MenuIcon className="size-7" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="container hidden items-center justify-between gap-6 xl:flex mt-1">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center py-2 relative px-2.5 hover:text-foreground"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink href={link.href} name={link.name} />
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-violet-700 h-[2px] inset-x-0 rounded-full absolute -bottom-1" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
{/* Bottom row - hidden on mobile */}
|
||||
<div className="hidden items-start justify-between gap-6 xl:flex relative z-10">
|
||||
<nav className="flex grow gap-5">
|
||||
<ul className="flex flex-row items-center gap-0 mb-1.5">
|
||||
{links.map((link) => {
|
||||
return (
|
||||
<li
|
||||
className="flex items-center relative"
|
||||
key={link.href}
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
onKeyDown={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
name={link.name}
|
||||
className="py-2.5 px-3 hover:bg-accent rounded-lg hover:text-foreground font-normal"
|
||||
/>
|
||||
{pathname?.startsWith(link.href) && (
|
||||
<div className="bg-foreground h-[2px] inset-x-0 rounded-full absolute -bottom-1.5" />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="px-1">
|
||||
<div className="flex items-center">
|
||||
<DropdownLinks
|
||||
category="AI"
|
||||
links={aiLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="SDKs"
|
||||
links={sdkLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="APIs"
|
||||
links={apisLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Tools"
|
||||
links={toolLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-1">
|
||||
<DropdownLinks
|
||||
category="Support"
|
||||
links={supportLinks}
|
||||
onLinkClick={() => setShowBurgerMenu(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
<NavLink
|
||||
href="/changelog"
|
||||
className="px-3 py-2.5 hover:bg-accent hover:text-foreground rounded-lg"
|
||||
name="Changelog"
|
||||
onClick={() => {
|
||||
setShowBurgerMenu(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -428,6 +424,8 @@ export function Header() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 bg-card/70 backdrop-blur-xl z-0" />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -448,31 +446,29 @@ function DropdownLinks(props: {
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="inline-flex items-center gap-1 p-0 font-medium text-muted-foreground text-sm hover:bg-transparent hover:text-foreground"
|
||||
className="inline-flex items-center gap-1.5 font-normal text-muted-foreground text-sm hover:bg-accent hover:text-foreground rounded-lg px-3 py-2.5"
|
||||
variant="ghost"
|
||||
>
|
||||
{props.category}
|
||||
<ChevronDownIcon className="size-4 text-muted-foreground opacity-70" />
|
||||
<ChevronDownIcon className="size-3.5 text-muted-foreground opacity-70" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex flex-col gap-1 bg-card p-3"
|
||||
style={{
|
||||
minWidth: "150px",
|
||||
}}
|
||||
className="flex flex-col gap-1 bg-card p-1 rounded-xl min-w-[200px]"
|
||||
sideOffset={14}
|
||||
>
|
||||
{props.links.map((info) => {
|
||||
return (
|
||||
<DropdownMenuItem asChild key={info.name}>
|
||||
<div
|
||||
className={clsx(
|
||||
"relative flex cursor-pointer gap-2 font-medium text-foreground",
|
||||
"relative flex cursor-pointer gap-3 font-medium text-foreground !rounded-lg px-3 py-2",
|
||||
"hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
{info.icon && (
|
||||
<info.icon className="size-5 text-foreground" />
|
||||
<info.icon className="size-5 text-muted-foreground" />
|
||||
)}
|
||||
<Link
|
||||
className="before:absolute before:inset-0"
|
||||
@@ -524,6 +520,7 @@ function NavLink(props: {
|
||||
name: string;
|
||||
onClick?: () => void;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
className?: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
@@ -534,6 +531,7 @@ function NavLink(props: {
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground",
|
||||
props.icon ? "flex flex-row gap-3" : "",
|
||||
props.className,
|
||||
)}
|
||||
href={props.href}
|
||||
onClick={props.onClick}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@@ -0,0 +1,467 @@
|
||||
import {
|
||||
Details,
|
||||
createMetadata,
|
||||
DocImage
|
||||
} from "@doc";
|
||||
import bridgeWidgetDark from "./bridge-widget-dark.png";
|
||||
import bridgeWidgetLight from "./bridge-widget-light.png";
|
||||
|
||||
|
||||
export const metadata = createMetadata({
|
||||
image: {
|
||||
title: "BridgeWidget Script",
|
||||
icon: "payments",
|
||||
},
|
||||
title: "Bridge Widget",
|
||||
description: "Add a widget to add cross swaps and fiat onramp to your app",
|
||||
});
|
||||
|
||||
# BridgeWidget Script
|
||||
|
||||
The BridgeWidget Script makes it easy to embed cross-chain swaps and fiat onramp UI into your app. Just add a script tag to your HTML and get a fully customizable widget — no build setup required.
|
||||
|
||||
## Key Features
|
||||
- Cross-chain token swaps across 85+ blockchains
|
||||
- Fiat onramp support to buy tokens with credit/debit cards
|
||||
- Customizable UI - use prebuilt themes or override with your brand colors
|
||||
- Prefill token selections for a smoother user experience
|
||||
- Display fiat values in multiple currencies
|
||||
- Event callbacks to track user actions (success, error, cancel, disconnect)
|
||||
|
||||
|
||||
<div className='dark-only'>
|
||||
<DocImage src={bridgeWidgetDark} />
|
||||
</div>
|
||||
<div className='light-only'>
|
||||
<DocImage src={bridgeWidgetLight} />
|
||||
</div>
|
||||
|
||||
|
||||
## Script Integration
|
||||
|
||||
```html
|
||||
<!-- Add this script in <head> -->
|
||||
<script src="https://unpkg.com/thirdweb/dist/scripts/bridge-widget.js"></script>
|
||||
|
||||
<!-- Add a container element where you want to render the UI -->
|
||||
<div id="bridge-widget-container"></div>
|
||||
|
||||
<!-- Initialize the widget at the end of <body> -->
|
||||
<script>
|
||||
const container = document.querySelector('#bridge-widget-container');
|
||||
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: "dark",
|
||||
buy: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
You can customize theme, currencies, token selections, and more. See the full list of options below
|
||||
|
||||
<Details summary="View full type">
|
||||
|
||||
```ts
|
||||
type Options = {
|
||||
clientId: string;
|
||||
theme?: "dark" | "light" | {
|
||||
type: "dark" | "light";
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success?: string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
};
|
||||
showThirdwebBranding?: boolean;
|
||||
currency?: SupportedFiatCurrency;
|
||||
swap?: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
buy: {
|
||||
amount: string;
|
||||
chainId: number;
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
purchaseData?: PurchaseData;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
### clientId
|
||||
Set your Project's client ID to identify your project. You create a project for free on [thirdweb dashboard](https://thirdweb.com/team)
|
||||
|
||||
|
||||
### swap
|
||||
The options to configure the "Swap" tab
|
||||
|
||||
```ts
|
||||
type Swap = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onSuccess?: (quote: SwapPreparedQuote) => void;
|
||||
onError?: (error: Error, quote: SwapPreparedQuote) => void;
|
||||
onCancel?: (quote: SwapPreparedQuote) => void;
|
||||
onDisconnect?: () => void;
|
||||
persistTokenSelections?: boolean;
|
||||
prefill?: {
|
||||
buyToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
sellToken?: {
|
||||
tokenAddress?: string;
|
||||
chainId: number;
|
||||
amount?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="prefill">
|
||||
|
||||
You can prefill Buy and/or Sell tokens for the swap tab. If `tokenAddress` is not provided, the native token will be used. Here are some examples:
|
||||
|
||||
|
||||
#### Set an ERC20 token as the buy token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set a native token as the sell token
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base native token (ETH)
|
||||
sellToken: {
|
||||
chainId: 8453,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set amount and token to Buy by default
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
amount: "0.1",
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Set both buy and sell tokens by default
|
||||
|
||||
```ts
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
swap: {
|
||||
prefill: {
|
||||
// Base USDC
|
||||
buyToken: {
|
||||
chainId: 8453,
|
||||
tokenAddress: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
||||
},
|
||||
// Polygon native token (POL)
|
||||
sellToken: {
|
||||
chainId: 137,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="persistTokenSelections">
|
||||
|
||||
Whether to persist the token selections to localStorage so that if the user revisits the widget, the last used tokens are pre-selected.
|
||||
|
||||
The last used tokens do not override the tokens specified in the `prefill` option. By default, it is set to `true`.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onDisconnect">
|
||||
|
||||
Pass a callback to be called when the user disconnects the active wallet.
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
### buy
|
||||
|
||||
The options to configure the "Buy" tab UI.
|
||||
|
||||
|
||||
```ts
|
||||
type Buy = {
|
||||
amount: string; // Required
|
||||
chainId: number; // Required
|
||||
tokenAddress?: string;
|
||||
buttonLabel?: string;
|
||||
onCancel?: (quote: BuyOrOnrampPrepareResult | undefined) => void;
|
||||
onError?: (
|
||||
error: Error,
|
||||
quote: BuyOrOnrampPrepareResult | undefined,
|
||||
) => void;
|
||||
onSuccess?: (quote: BuyOrOnrampPrepareResult) => void;
|
||||
className?: string;
|
||||
country?: string;
|
||||
presetOptions?: [number, number, number];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<Details summary="amount" tags={["Required"]}>
|
||||
|
||||
Set the default token amount shown in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="chainId" tags={["Required"]}>
|
||||
|
||||
Specify which chain should be selected in the "Buy" tab.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="tokenAddress">
|
||||
|
||||
Specify which token should be selected in the "Buy" tab. If not specified, the native token of the selected chain will be selected.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="buttonLabel">
|
||||
|
||||
Override the label for main action button
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onCancel">
|
||||
|
||||
Pass a callback to be called when the user cancels the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onError">
|
||||
|
||||
Pass a callback to be called when the user encounters an error during the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="onSuccess">
|
||||
|
||||
Pass a callback to be called when the user successfully completes the purchase.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="className">
|
||||
|
||||
Apply a custom class name to the "Buy" tab content container.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="country">
|
||||
|
||||
The user's ISO 3166 alpha-2 country code. This is used to determine onramp provider support.
|
||||
|
||||
</Details>
|
||||
|
||||
<Details summary="presetOptions">
|
||||
|
||||
Preset fiat amounts to display in the UI. Defaults to [5, 10, 20].
|
||||
|
||||
</Details>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### theme
|
||||
|
||||
Set the theme for the widget. You can either set it to `"dark"`, `"light"` or overrides some or all the colors by passing an object. By default it set to `"dark"`.
|
||||
|
||||
When overriding the colors, you must set the `type` to either `"dark"` or `"light"` so that the widget knows which theme to apply the overrides to.
|
||||
|
||||
|
||||
```ts
|
||||
type Theme = "dark" | "light" | {
|
||||
type: "dark" | "light"; // required
|
||||
accentButtonBg?: string;
|
||||
accentButtonText?: string;
|
||||
accentText?: string;
|
||||
borderColor?: string;
|
||||
connectedButtonBg?: string;
|
||||
connectedButtonBgHover?: string;
|
||||
danger?: string;
|
||||
inputAutofillBg?: string;
|
||||
modalBg?: string;
|
||||
modalOverlayBg?: string;
|
||||
primaryButtonBg?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryText?: string;
|
||||
scrollbarBg?: string;
|
||||
secondaryButtonBg?: string;
|
||||
secondaryButtonHoverBg?: string;
|
||||
secondaryButtonText?: string;
|
||||
secondaryIconColor?: string;
|
||||
secondaryIconHoverBg?: string;
|
||||
secondaryIconHoverColor?: string;
|
||||
secondaryText?: string;
|
||||
selectedTextBg?: string;
|
||||
selectedTextColor: string;
|
||||
separatorLine?: string;
|
||||
skeletonBg: string;
|
||||
success? : string;
|
||||
tertiaryBg?: string;
|
||||
tooltipBg?: string;
|
||||
tooltipText?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
BridgeWidget.render(container, {
|
||||
clientId: "your-thirdweb-client-id",
|
||||
theme: {
|
||||
type: "dark",
|
||||
colors: {
|
||||
modalBg: "black",
|
||||
primaryText: "white",
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### currency
|
||||
|
||||
The token amounts fiat values will be displayed in this currency. By default, it is set to `"USD"`.
|
||||
|
||||
|
||||
```ts
|
||||
type Currency = "USD" | "EUR" | "GBP" | "JPY" | "KRW" | "CNY" | "INR" | "NOK" | "SEK" | "CHF" | "AUD" | "CAD" | "NZD" | "MXN" | "BRL" | "CLP" | "CZK" | "DKK" | "HKD" | "HUF" | "IDR" | "ILS" | "ISK";
|
||||
```
|
||||
|
||||
|
||||
### showThirdwebBranding
|
||||
|
||||
Whether to show thirdweb branding at the bottom of the widget. By default, it is set to `true`.
|
||||
@@ -102,17 +102,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="react">
|
||||
The quickest way to setup Bridge in your React app is with the [`BuyWidget`](/references/typescript/v5/widgets/BuyWidget) component.
|
||||
|
||||
The quickest way to setup Bridge in your React app is with the [`SwapWidget`](/references/typescript/v5/widgets/SwapWidget) component.
|
||||
|
||||
### Live Playground
|
||||
|
||||
<ArticleIconCard
|
||||
title="Payment Widgets Playground"
|
||||
description="Try out the Payment Widgets in our live playground"
|
||||
title="Swap Widget Playground"
|
||||
description="Try out the Swap Widget in our live playground"
|
||||
icon={ReactIcon}
|
||||
href="https://playground.thirdweb.com/connect/pay/transactions"
|
||||
href="https://playground.thirdweb.com/bridge/swap-widget"
|
||||
/>
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
Install the thirdweb SDK in your React project:
|
||||
@@ -127,7 +127,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Create a Client
|
||||
|
||||
First, create a client file (e.g., `thirdwebClient.ts`) for reuse throughout your app:
|
||||
First, create a client file for reuse throughout your app:
|
||||
|
||||
```typescript
|
||||
// thirdwebClient.ts
|
||||
@@ -140,7 +140,7 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
|
||||
### Setup the Provider
|
||||
|
||||
Wrap your application with the ThirdwebProvider:
|
||||
Wrap your application with the `ThirdwebProvider`
|
||||
|
||||
```tsx
|
||||
// app.tsx / _app.tsx
|
||||
@@ -155,21 +155,17 @@ const preparedQuote = await Bridge.Buy.prepare({
|
||||
}
|
||||
```
|
||||
|
||||
### Buy Widget
|
||||
### Add Swap Widget
|
||||
|
||||
Use the BuyWidget to let users purchase tokens:
|
||||
Use the SwapWidget to let users swap tokens
|
||||
|
||||
```tsx
|
||||
import { BuyWidget } from "thirdweb/react";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { SwapWidget } from "thirdweb/react";
|
||||
|
||||
function PaymentComponent() {
|
||||
function Example() {
|
||||
return (
|
||||
<BuyWidget
|
||||
<SwapWidget
|
||||
client={client}
|
||||
chain={base}
|
||||
amount="10" // 10 USDC
|
||||
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ export const sidebar: SideBar = {
|
||||
href: `${bridgeSlug}/routes`,
|
||||
name: "Get Routes",
|
||||
},
|
||||
{
|
||||
href: `${bridgeSlug}/bridge-widget-script`,
|
||||
name: "BridgeWidget Script",
|
||||
},
|
||||
],
|
||||
name: "Guides",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<main className="container max-w-3xl py-10" data-noindex>
|
||||
<main className="container max-w-4xl py-10" data-noindex>
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user