Skip to content

Commit 35a909a

Browse files
WIP brige embed (#7222)
Co-authored-by: gregfromstl <[email protected]>
1 parent 9f6c1a4 commit 35a909a

File tree

112 files changed

+13645
-256
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+13645
-256
lines changed

.changeset/icy-eyes-show.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Updates PayEmbed with refreshed UI and multi-step support

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
2020
- Biome governs formatting and linting; its rules live in biome.json.
2121
- Run pnpm biome check --apply before committing.
2222
- Avoid editor‑specific configs; rely on the shared settings.
23+
- make sure everything builds after each file change by running `pnpm build`
2324

2425
2526

@@ -39,7 +40,8 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
3940
- Co‑locate tests: foo.ts ↔ foo.test.ts.
4041
- Use real function invocations with stub data; avoid brittle mocks.
4142
- For network interactions, use Mock Service Worker (MSW) to intercept fetch/HTTP calls, mocking only scenarios that are hard to reproduce.
42-
- Keep tests deterministic and side‑effect free; Jest is pre‑configured.
43+
- Keep tests deterministic and side‑effect free; Vitest is pre‑configured.
44+
- to run the tests: `cd packages thirdweb & pnpm test:dev <filename>`
4345

4446
4547

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/components/client/PayModal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export function PayModalButton(props: {
4444
payOptions={{
4545
onPurchaseSuccess(info) {
4646
if (
47-
info.type === "crypto" &&
48-
info.status.status !== "NOT_FOUND"
47+
info?.type === "crypto" &&
48+
info?.status.status !== "NOT_FOUND"
4949
) {
5050
trackEvent({
5151
category: "pay",
@@ -58,7 +58,7 @@ export function PayModalButton(props: {
5858
});
5959
}
6060

61-
if (info.type === "fiat" && info.status.status !== "NOT_FOUND") {
61+
if (info?.type === "fiat" && info.status.status !== "NOT_FOUND") {
6262
trackEvent({
6363
category: "pay",
6464
action: "buy",

apps/dashboard/src/app/pay/components/client/PayPageEmbed.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function PayPageEmbed({
6363
onPurchaseSuccess: (result) => {
6464
if (!redirectUri) return;
6565
const url = new URL(redirectUri);
66-
switch (result.type) {
66+
switch (result?.type) {
6767
case "crypto": {
6868
url.searchParams.set("status", result.status.status);
6969
if (

apps/dashboard/src/components/buttons/MismatchButton.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ export const MismatchButton = forwardRef<
293293
payOptions={{
294294
onPurchaseSuccess(info) {
295295
if (
296-
info.type === "crypto" &&
296+
info?.type === "crypto" &&
297297
info.status.status !== "NOT_FOUND"
298298
) {
299299
trackEvent({
@@ -308,7 +308,7 @@ export const MismatchButton = forwardRef<
308308
}
309309

310310
if (
311-
info.type === "fiat" &&
311+
info?.type === "fiat" &&
312312
info.status.status !== "NOT_FOUND"
313313
) {
314314
trackEvent({

apps/playground-web/src/app/connect/pay/commerce/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ function BuyMerch() {
6363
sellerAddress: "0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675",
6464
},
6565
metadata: {
66-
name: "Black Hoodie (Size L)",
66+
name: "Black Hoodie",
67+
description: "Size L. Ships worldwide.",
6768
image: "/drip-hoodie.png",
6869
},
6970
}}

apps/playground-web/src/app/connect/pay/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type PayEmbedPlaygroundOptions = {
1212
mode?: "fund_wallet" | "direct_payment" | "transaction";
1313
title: string | undefined;
1414
image: string | undefined;
15+
description: string | undefined;
1516

1617
// fund_wallet mode options
1718
buyTokenAddress: string | undefined;

apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,26 @@ export function LeftSection(props: {
496496
/>
497497
</div>
498498
</div>
499+
500+
{/* Modal description */}
501+
<div className="flex flex-col gap-2">
502+
<Label htmlFor="modal-description">Image</Label>
503+
<Input
504+
id="modal-description"
505+
placeholder="Your own description here"
506+
className="bg-card"
507+
value={options.payOptions.description}
508+
onChange={(e) =>
509+
setOptions((v) => ({
510+
...v,
511+
payOptions: {
512+
...payOptions,
513+
description: e.target.value,
514+
},
515+
}))
516+
}
517+
/>
518+
</div>
499519
</div>
500520
</CollapsibleSection>
501521

apps/playground-web/src/app/connect/pay/embed/RightSection.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,19 @@ export function RightSection(props: {
7070
(props.options.payOptions.mode === "transaction"
7171
? "Transaction"
7272
: props.options.payOptions.mode === "direct_payment"
73-
? "Purchase"
73+
? "Product Name"
7474
: "Buy Crypto"),
75+
description:
76+
props.options.payOptions.description || "Your own description here",
7577
image:
7678
props.options.payOptions.image ||
77-
`https://placehold.co/600x400/${
78-
props.options.theme.type === "dark"
79-
? "1d1d23/7c7a85"
80-
: "f2eff3/6f6d78"
81-
}?text=Your%20Product%20Here&font=roboto`,
79+
props.options.payOptions.mode === "direct_payment"
80+
? `https://placehold.co/600x400/${
81+
props.options.theme.type === "dark"
82+
? "1d1d23/7c7a85"
83+
: "f2eff3/6f6d78"
84+
}?text=Your%20Product%20Here&font=roboto`
85+
: undefined,
8286
},
8387

8488
// Mode-specific options

apps/playground-web/src/app/connect/pay/embed/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const defaultConnectOptions: PayEmbedPlaygroundOptions = {
1717
mode: "fund_wallet",
1818
title: "",
1919
image: "",
20+
description: "",
2021
buyTokenAddress: NATIVE_TOKEN_ADDRESS,
2122
buyTokenAmount: "0.01",
2223
buyTokenChain: base,

apps/playground-web/src/components/pay/direct-payment.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function BuyMerchPreview() {
2020
},
2121
metadata: {
2222
name: "Black Hoodie (Size L)",
23+
description: "Size L. Ships worldwide.",
2324
image: "/drip-hoodie.png",
2425
},
2526
}}

packages/thirdweb/knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"src/exports/**",
55
"scripts/**/*.{ts,mjs}",
66
"src/cli/bin.ts",
7-
"src/transaction/actions/send-batch-transaction.ts"
7+
"src/transaction/actions/send-batch-transaction.ts",
8+
"src/react/core/hooks/useBridgeRoutes.ts"
89
],
910
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.mjs"],
1011
"ignore": ["src/**/__generated__/**", "**/*.bench.ts"],

packages/thirdweb/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
"@radix-ui/react-focus-scope": "1.1.7",
236236
"@radix-ui/react-icons": "1.3.2",
237237
"@radix-ui/react-tooltip": "1.2.7",
238+
"@storybook/react": "9.0.8",
238239
"@tanstack/react-query": "5.80.7",
239240
"@thirdweb-dev/engine": "workspace:*",
240241
"@thirdweb-dev/insight": "workspace:*",

packages/thirdweb/src/bridge/Routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export async function routes(options: routes.Options): Promise<routes.Result> {
131131
sortBy,
132132
limit,
133133
offset,
134+
includePrices,
134135
} = options;
135136

136137
const clientFetch = getClientFetch(client);
@@ -159,6 +160,9 @@ export async function routes(options: routes.Options): Promise<routes.Result> {
159160
if (sortBy) {
160161
url.searchParams.set("sortBy", sortBy);
161162
}
163+
if (includePrices) {
164+
url.searchParams.set("includePrices", includePrices.toString());
165+
}
162166

163167
const response = await clientFetch(url.toString());
164168
if (!response.ok) {
@@ -185,6 +189,7 @@ export declare namespace routes {
185189
transactionHash?: ox__Hex.Hex;
186190
sortBy?: "popularity";
187191
maxSteps?: number;
192+
includePrices?: boolean;
188193
limit?: number;
189194
offset?: number;
190195
};

packages/thirdweb/src/bridge/Token.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export async function tokens(options: tokens.Options): Promise<tokens.Result> {
158158

159159
export declare namespace tokens {
160160
/**
161-
* Input parameters for {@link Bridge.tokens}.
161+
* Input parameters for {@link tokens}.
162162
*/
163163
type Options = {
164164
/** Your {@link ThirdwebClient} instance. */
@@ -182,3 +182,84 @@ export declare namespace tokens {
182182
*/
183183
type Result = Token[];
184184
}
185+
186+
/**
187+
* Adds a token to the Universal Bridge for indexing.
188+
*
189+
* This function requests the Universal Bridge to index a specific token on a given chain.
190+
* Once indexed, the token will be available for cross-chain operations.
191+
*
192+
* @example
193+
* ```typescript
194+
* import { Bridge } from "thirdweb";
195+
*
196+
* // Add a token for indexing
197+
* const result = await Bridge.add({
198+
* client: thirdwebClient,
199+
* chainId: 1,
200+
* tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
201+
* });
202+
* ```
203+
*
204+
* @param options - The options for adding a token.
205+
* @param options.client - Your thirdweb client.
206+
* @param options.chainId - The chain ID where the token is deployed.
207+
* @param options.tokenAddress - The contract address of the token to add.
208+
*
209+
* @returns A promise that resolves when the token has been successfully submitted for indexing.
210+
*
211+
* @throws Will throw an error if there is an issue adding the token.
212+
* @bridge
213+
* @beta
214+
*/
215+
export async function add(options: add.Options): Promise<add.Result> {
216+
const { client, chainId, tokenAddress } = options;
217+
218+
const clientFetch = getClientFetch(client);
219+
const url = `${getThirdwebBaseUrl("bridge")}/v1/tokens`;
220+
221+
const requestBody = {
222+
chainId,
223+
tokenAddress,
224+
};
225+
226+
const response = await clientFetch(url, {
227+
method: "POST",
228+
headers: {
229+
"Content-Type": "application/json",
230+
},
231+
body: JSON.stringify(requestBody),
232+
});
233+
234+
if (!response.ok) {
235+
const errorJson = await response.json();
236+
throw new ApiError({
237+
code: errorJson.code || "UNKNOWN_ERROR",
238+
message: errorJson.message || response.statusText,
239+
correlationId: errorJson.correlationId || undefined,
240+
statusCode: response.status,
241+
});
242+
}
243+
244+
const { data }: { data: Token } = await response.json();
245+
return data;
246+
}
247+
248+
export declare namespace add {
249+
/**
250+
* Input parameters for {@link add}.
251+
*/
252+
type Options = {
253+
/** Your {@link ThirdwebClient} instance. */
254+
client: ThirdwebClient;
255+
/** The chain ID where the token is deployed. */
256+
chainId: number;
257+
/** The contract address of the token to add. */
258+
tokenAddress: string;
259+
};
260+
261+
/**
262+
* The result returned from {@link Bridge.add}.
263+
*/
264+
type Result = Token;
265+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type Action = "approval" | "transfer" | "buy" | "sell";
1+
export type Action = "approval" | "transfer" | "buy" | "sell" | "fee";

packages/thirdweb/src/bridge/types/Errors.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { stringify } from "../../utils/json.js";
2+
13
type ErrorCode =
24
| "INVALID_INPUT"
35
| "ROUTE_NOT_FOUND"
@@ -22,4 +24,13 @@ export class ApiError extends Error {
2224
this.correlationId = args.correlationId;
2325
this.statusCode = args.statusCode;
2426
}
27+
28+
override toString() {
29+
return stringify({
30+
code: this.code,
31+
message: this.message,
32+
statusCode: this.statusCode,
33+
correlationId: this.correlationId,
34+
});
35+
}
2536
}

packages/thirdweb/src/pay/buyWithFiat/getQuote.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ export async function getBuyWithFiatQuote(
291291
provider?: FiatProvider,
292292
): "stripe" | "coinbase" | "transak" => {
293293
switch (provider) {
294-
case "STRIPE":
294+
case "stripe":
295295
return "stripe";
296-
case "TRANSAK":
296+
case "transak":
297297
return "transak";
298298
default: // default to coinbase when undefined or any other value
299299
return "coinbase";

packages/thirdweb/src/pay/convert/cryptoToFiat.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Address } from "abitype";
22
import type { Chain } from "../../chains/types.js";
33
import type { ThirdwebClient } from "../../client/client.js";
44
import { isAddress } from "../../utils/address.js";
5-
import { getTokenPrice } from "./get-token.js";
5+
import { getToken } from "./get-token.js";
66
import type { SupportedFiatCurrency } from "./type.js";
77

88
/**
@@ -73,11 +73,11 @@ export async function convertCryptoToFiat(
7373
"Invalid fromTokenAddress. Expected a valid EVM contract address",
7474
);
7575
}
76-
const price = await getTokenPrice(client, fromTokenAddress, chain.id);
77-
if (!price) {
76+
const token = await getToken(client, fromTokenAddress, chain.id);
77+
if (token.priceUsd === 0) {
7878
throw new Error(
7979
`Error: Failed to fetch price for token ${fromTokenAddress} on chainId: ${chain.id}`,
8080
);
8181
}
82-
return { result: price * fromAmount };
82+
return { result: token.priceUsd * fromAmount };
8383
}

packages/thirdweb/src/pay/convert/fiatToCrypto.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Address } from "abitype";
22
import type { Chain } from "../../chains/types.js";
33
import type { ThirdwebClient } from "../../client/client.js";
44
import { isAddress } from "../../utils/address.js";
5-
import { getTokenPrice } from "./get-token.js";
5+
import { getToken } from "./get-token.js";
66
import type { SupportedFiatCurrency } from "./type.js";
77

88
/**
@@ -72,11 +72,11 @@ export async function convertFiatToCrypto(
7272
if (!isAddress(to)) {
7373
throw new Error("Invalid `to`. Expected a valid EVM contract address");
7474
}
75-
const price = await getTokenPrice(client, to, chain.id);
76-
if (!price || price === 0) {
75+
const token = await getToken(client, to, chain.id);
76+
if (!token || token.priceUsd === 0) {
7777
throw new Error(
7878
`Error: Failed to fetch price for token ${to} on chainId: ${chain.id}`,
7979
);
8080
}
81-
return { result: fromAmount / price };
81+
return { result: fromAmount / token.priceUsd };
8282
}

0 commit comments

Comments
 (0)