diff --git a/.changeset/tidy-seas-sing.md b/.changeset/tidy-seas-sing.md new file mode 100644 index 00000000000..3a4a8ebc255 --- /dev/null +++ b/.changeset/tidy-seas-sing.md @@ -0,0 +1,79 @@ +--- +"thirdweb": patch +--- + +Added Bridge.Onramp.prepare and Bridge.Onramp.status functions + +## Bridge.Onramp.prepare + +Prepares an onramp transaction, returning a link from the specified provider to onramp to the specified token. + +```typescript +import { Bridge } from "thirdweb"; +import { ethereum } from "thirdweb/chains"; +import { NATIVE_TOKEN_ADDRESS, toWei } from "thirdweb/utils"; + +const preparedOnramp = await Bridge.Onramp.prepare({ + client: thirdwebClient, + onramp: "stripe", + chainId: ethereum.id, + tokenAddress: NATIVE_TOKEN_ADDRESS, + receiver: "0x...", // receiver's address + amount: toWei("10"), // 10 of the destination token + // Optional params: + // sender: "0x...", // sender's address + // onrampTokenAddress: NATIVE_TOKEN_ADDRESS, // token to initially onramp to + // onrampChainId: 1, // chain to initially onramp to + // currency: "USD", + // maxSteps: 2, + // purchaseData: { customId: "123" } +}); + +console.log(preparedOnramp.link); // URL to redirect the user to +console.log(preparedOnramp.currencyAmount); // Price in fiat currency +``` + +## Bridge.Onramp.status + +Retrieves the status of an Onramp session created via Bridge.Onramp.prepare. + +```typescript +import { Bridge } from "thirdweb"; + +const onrampStatus = await Bridge.Onramp.status({ + id: "022218cc-96af-4291-b90c-dadcb47571ec", + client: thirdwebClient, +}); + +// Possible results: +// { +// status: "CREATED", +// transactions: [], +// purchaseData: { +// orderId: "abc-123", +// }, +// } +// +// or +// { +// status: "PENDING", +// transactions: [], +// purchaseData: { +// orderId: "abc-123", +// }, +// } +// +// or +// { +// status: "COMPLETED", +// transactions: [ +// { +// chainId: 1, +// transactionHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", +// }, +// ], +// purchaseData: { +// orderId: "abc-123", +// }, +// } +``` diff --git a/apps/portal/src/app/pay/customization/payembed/page.mdx b/apps/portal/src/app/pay/customization/payembed/page.mdx index 7487ef7141c..c9d2601ce9c 100644 --- a/apps/portal/src/app/pay/customization/payembed/page.mdx +++ b/apps/portal/src/app/pay/customization/payembed/page.mdx @@ -92,7 +92,7 @@ You can specify which onramp provider to present to your users. By default, we c ``` diff --git a/apps/portal/src/app/pay/customization/send-transaction/page.mdx b/apps/portal/src/app/pay/customization/send-transaction/page.mdx index d92e7e83625..b8936b09ff2 100644 --- a/apps/portal/src/app/pay/customization/send-transaction/page.mdx +++ b/apps/portal/src/app/pay/customization/send-transaction/page.mdx @@ -88,7 +88,7 @@ You can specify which onramp provider to present to your users. By default, we c ```tsx const { mutate: sendTransaction } = useSendTransaction({ payModal: { - preferredProvider: "STRIPE" | "KADO" | "TRANSAK", + preferredProvider: "COINBASE" | "STRIPE" | "TRANSAK", }, }); ``` diff --git a/apps/portal/src/app/pay/testing-pay/page.mdx b/apps/portal/src/app/pay/testing-pay/page.mdx index 1d88ab73481..f1822136957 100644 --- a/apps/portal/src/app/pay/testing-pay/page.mdx +++ b/apps/portal/src/app/pay/testing-pay/page.mdx @@ -16,7 +16,7 @@ Developers can turn on Test Mode to test both fiat-to-crypto transactions and cr ## Buy With Fiat -By setting `testMode` to `true` for Buy With Fiat, you can enable test experiences for our underlying providers (Stripe, Kado, and Transak). +By setting `testMode` to `true` for Buy With Fiat, you can enable test experiences for our underlying providers (Coinbase, Stripe, and Transak). diff --git a/packages/thirdweb/src/bridge/Onramp.test.ts b/packages/thirdweb/src/bridge/Onramp.test.ts new file mode 100644 index 00000000000..17f61014017 --- /dev/null +++ b/packages/thirdweb/src/bridge/Onramp.test.ts @@ -0,0 +1,118 @@ +import { toWei } from "src/utils/units.js"; +import { describe, expect, it } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import * as Onramp from "./Onramp.js"; + +// Use the same receiver address as other bridge tests +const RECEIVER_ADDRESS = "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709"; +const NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + +/** + * These tests call the real Bridge Onramp API. They are executed only when a + * `TW_SECRET_KEY` environment variable is present, mirroring the behaviour of + * the other bridge tests in this package. + */ +describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Onramp.prepare", () => { + it("should prepare an onramp successfully", async () => { + const prepared = await Onramp.prepare({ + client: TEST_CLIENT, + onramp: "stripe", + chainId: 1, + tokenAddress: NATIVE_TOKEN_ADDRESS, + receiver: RECEIVER_ADDRESS, + amount: toWei("0.01"), + }); + + expect(prepared).toBeDefined(); + + // The destinationAmount should be a bigint and greater than zero + expect(typeof prepared.destinationAmount).toBe("bigint"); + expect(prepared.destinationAmount > 0n).toBe(true); + + // A redirect link for the user should be provided + expect(prepared.link).toBeDefined(); + expect(typeof prepared.link).toBe("string"); + + // Intent must be present and reference the correct receiver + expect(prepared.intent).toBeDefined(); + expect(prepared.intent.receiver.toLowerCase()).toBe( + RECEIVER_ADDRESS.toLowerCase(), + ); + + // Steps array should be defined (it may be empty if the provider supports the destination token natively) + expect(Array.isArray(prepared.steps)).toBe(true); + }); + + it("should surface any errors", async () => { + await expect( + Onramp.prepare({ + client: TEST_CLIENT, + onramp: "stripe", + chainId: 444, // Unsupported chain ID + tokenAddress: NATIVE_TOKEN_ADDRESS, + receiver: RECEIVER_ADDRESS, + amount: toWei("0.01"), + }), + ).rejects.toThrowError(); + }); + + it("should prepare a Coinbase onramp successfully", async () => { + const prepared = await Onramp.prepare({ + client: TEST_CLIENT, + onramp: "coinbase", + chainId: 1, + tokenAddress: NATIVE_TOKEN_ADDRESS, + receiver: RECEIVER_ADDRESS, + amount: toWei("0.01"), + }); + + expect(prepared).toBeDefined(); + + // The destinationAmount should be a bigint and greater than zero + expect(typeof prepared.destinationAmount).toBe("bigint"); + expect(prepared.destinationAmount > 0n).toBe(true); + + // A redirect link for the user should be provided + expect(prepared.link).toBeDefined(); + expect(typeof prepared.link).toBe("string"); + + // Intent must be present and reference the correct receiver + expect(prepared.intent).toBeDefined(); + expect(prepared.intent.receiver.toLowerCase()).toBe( + RECEIVER_ADDRESS.toLowerCase(), + ); + + // Steps array should be defined (it may be empty if the provider supports the destination token natively) + expect(Array.isArray(prepared.steps)).toBe(true); + }); + + it("should prepare a Transak onramp successfully", async () => { + const prepared = await Onramp.prepare({ + client: TEST_CLIENT, + onramp: "transak", + chainId: 1, + tokenAddress: NATIVE_TOKEN_ADDRESS, + receiver: RECEIVER_ADDRESS, + amount: toWei("0.01"), + }); + + expect(prepared).toBeDefined(); + + // The destinationAmount should be a bigint and greater than zero + expect(typeof prepared.destinationAmount).toBe("bigint"); + expect(prepared.destinationAmount > 0n).toBe(true); + + // A redirect link for the user should be provided + expect(prepared.link).toBeDefined(); + expect(typeof prepared.link).toBe("string"); + + // Intent must be present and reference the correct receiver + expect(prepared.intent).toBeDefined(); + expect(prepared.intent.receiver.toLowerCase()).toBe( + RECEIVER_ADDRESS.toLowerCase(), + ); + + // Steps array should be defined (it may be empty if the provider supports the destination token natively) + expect(Array.isArray(prepared.steps)).toBe(true); + }); +}); diff --git a/packages/thirdweb/src/bridge/Onramp.ts b/packages/thirdweb/src/bridge/Onramp.ts new file mode 100644 index 00000000000..8add6929c72 --- /dev/null +++ b/packages/thirdweb/src/bridge/Onramp.ts @@ -0,0 +1,244 @@ +import type { Address as ox__Address } from "ox"; +import type { ThirdwebClient } from "../client/client.js"; +import { getThirdwebBaseUrl } from "../utils/domains.js"; +import { getClientFetch } from "../utils/fetch.js"; +import { stringify } from "../utils/json.js"; +import type { RouteStep } from "./types/Route.js"; +import type { Token } from "./types/Token.js"; + +// export status within the Onramp module +export { status } from "./OnrampStatus.js"; + +type OnrampIntent = { + onramp: "stripe" | "coinbase" | "transak"; + chainId: number; + tokenAddress: ox__Address.Address; + receiver: ox__Address.Address; + amount?: string; // Corresponds to buyAmountWei in some other contexts + purchaseData?: unknown; + sender?: ox__Address.Address; + onrampTokenAddress?: ox__Address.Address; + onrampChainId?: number; + currency?: string; + maxSteps?: number; + excludeChainIds?: string | string[]; +}; + +type OnrampPrepareQuoteResponseData = { + id: string; + link: string; + currency: string; + currencyAmount: number; + destinationAmount: bigint; + destinationToken: Token; + timestamp?: number; + expiration?: number; + steps: RouteStep[]; + intent: OnrampIntent; +}; + +// Explicit type for the API request body +interface OnrampApiRequestBody { + onramp: "stripe" | "coinbase" | "transak"; + chainId: number; + tokenAddress: ox__Address.Address; + receiver: ox__Address.Address; + amount?: string; + purchaseData?: unknown; + sender?: ox__Address.Address; + onrampTokenAddress?: ox__Address.Address; + onrampChainId?: number; + currency?: string; + maxSteps?: number; + excludeChainIds?: string; +} + +/** + * Prepares an onramp transaction, returning a link from the specified provider to onramp to the specified token. + * + * @example + * ```typescript + * import { Bridge } from "thirdweb"; + * import { ethereum } from "thirdweb/chains"; + * import { NATIVE_TOKEN_ADDRESS, toWei } from "thirdweb/utils"; + * + * const preparedOnramp = await Bridge.Onramp.prepare({ + * client: thirdwebClient, + * onramp: "stripe", + * chainId: ethereum.id, + * tokenAddress: NATIVE_TOKEN_ADDRESS, + * receiver: "0x...", // receiver's address + * amount: toWei("10"), // 10 of the destination token + * // Optional params: + * // sender: "0x...", // sender's address + * // onrampTokenAddress: NATIVE_TOKEN_ADDRESS, // token to initially onramp to + * // onrampChainId: 1, // chain to initially onramp to + * // currency: "USD", + * // maxSteps: 2, + * // purchaseData: { customId: "123" } + * }); + * + * console.log(preparedOnramp.link); // URL to redirect the user to + * console.log(preparedOnramp.currencyAmount); // Amount in fiat the user will pay + * ``` + * + * This function returns a quote that might look like: + * ```typescript + * { + * id: "123e4567-e89b-12d3-a456-426614174000", + * link: "https://onramp.example.com/session?id=...", + * currency: "USD", + * currencyAmount: 10.52, + * destinationAmount: 10000000000000000000n, // 10 ETH if decimals 18 + * timestamp: 1689812800, + * expiration: 1689842800, + * steps: [ + * // ... further steps if any post-onramp swaps are needed + * ], + * intent: { + * onramp: "stripe", + * chainId: 1, + * tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + * receiver: "0x...", + * amount: "10000000000000000000" + * } + * } + * ``` + * + * @param options - The options for preparing the onramp. + * @param options.client - Your thirdweb client. + * @param options.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak"). + * @param options.chainId - The destination chain ID. + * @param options.tokenAddress - The destination token address. + * @param options.receiver - The address that will receive the output token. + * @param [options.amount] - The desired token amount in wei. + * @param [options.purchaseData] - Arbitrary purchase data. + * @param [options.sender] - An optional address to associate as the onramp sender. + * @param [options.onrampTokenAddress] - The token to initially onramp to if the destination token is not supported by the provider. + * @param [options.onrampChainId] - The chain ID to initially onramp to if the destination chain is not supported. + * @param [options.currency] - The currency for the onramp (e.g., "USD", "GBP"). Defaults to user's preferred or "USD". + * @param [options.maxSteps] - Maximum number of post-onramp steps. + * @param [options.excludeChainIds] - Chain IDs to exclude from the route (string or array of strings). + * + * @returns A promise that resolves to the prepared onramp details, including the link and quote. + * @throws Will throw an error if there is an issue preparing the onramp. + * @bridge Onramp + * @beta + */ +export async function prepare( + options: prepare.Options, +): Promise { + const { + client, + onramp, + chainId, + tokenAddress, + receiver, + amount, + purchaseData, + sender, + onrampTokenAddress, + onrampChainId, + currency, + maxSteps, + excludeChainIds, + } = options; + + const clientFetch = getClientFetch(client); + const url = `${getThirdwebBaseUrl("bridge")}/v1/onramp/prepare`; + + const apiRequestBody: OnrampApiRequestBody = { + onramp, + chainId: Number(chainId), + tokenAddress, + receiver, + }; + + if (amount !== undefined) { + apiRequestBody.amount = amount.toString(); + } + if (purchaseData !== undefined) { + apiRequestBody.purchaseData = purchaseData; + } + if (sender !== undefined) { + apiRequestBody.sender = sender; + } + if (onrampTokenAddress !== undefined) { + apiRequestBody.onrampTokenAddress = onrampTokenAddress; + } + if (onrampChainId !== undefined) { + apiRequestBody.onrampChainId = Number(onrampChainId); + } + if (currency !== undefined) { + apiRequestBody.currency = currency; + } + if (maxSteps !== undefined) { + apiRequestBody.maxSteps = maxSteps; + } + if (excludeChainIds !== undefined) { + apiRequestBody.excludeChainIds = Array.isArray(excludeChainIds) + ? excludeChainIds.join(",") + : excludeChainIds; + } + + const response = await clientFetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: stringify(apiRequestBody), + }); + + if (!response.ok) { + const errorJson = await response.json(); + throw new Error( + `${errorJson.code || response.status} | ${errorJson.message || response.statusText} - ${errorJson.correlationId || "N/A"}`, + ); + } + + const { data }: { data: OnrampPrepareQuoteResponseData } = + await response.json(); + + // Transform amounts from string to bigint where appropriate + const transformedSteps = data.steps.map((step) => ({ + ...step, + originAmount: BigInt(step.originAmount), + destinationAmount: BigInt(step.destinationAmount), + transactions: step.transactions.map((tx) => ({ + ...tx, + value: tx.value ? BigInt(tx.value) : undefined, + })), + })); + + const intentFromResponse = { + ...data.intent, + amount: data.intent.amount ? data.intent.amount : undefined, + }; + + return { + ...data, + destinationAmount: BigInt(data.destinationAmount), + steps: transformedSteps, + intent: intentFromResponse, + }; +} + +export declare namespace prepare { + export type Options = { + client: ThirdwebClient; + onramp: "stripe" | "coinbase" | "transak"; + chainId: number; + tokenAddress: ox__Address.Address; + receiver: ox__Address.Address; + amount?: bigint; + purchaseData?: unknown; + sender?: ox__Address.Address; + onrampTokenAddress?: ox__Address.Address; + onrampChainId?: number; + currency?: string; + maxSteps?: number; + excludeChainIds?: string | string[]; + }; + + export type Result = OnrampPrepareQuoteResponseData; +} diff --git a/packages/thirdweb/src/bridge/OnrampStatus.ts b/packages/thirdweb/src/bridge/OnrampStatus.ts new file mode 100644 index 00000000000..33a3d08c5c6 --- /dev/null +++ b/packages/thirdweb/src/bridge/OnrampStatus.ts @@ -0,0 +1,133 @@ +import type { Hex as ox__Hex } from "ox"; +import type { ThirdwebClient } from "../client/client.js"; +import { getThirdwebBaseUrl } from "../utils/domains.js"; +import { getClientFetch } from "../utils/fetch.js"; + +/** + * Retrieves the status of an Onramp session created via {@link Bridge.Onramp.prepare}. The + * status will include any on-chain transactions that have occurred as a result of the onramp + * as well as any arbitrary `purchaseData` that was supplied when the onramp was + * prepared. + * + * @example + * ```typescript + * import { Bridge } from "thirdweb"; + * + * const onrampStatus = await Bridge.Onramp.status({ + * id: "022218cc-96af-4291-b90c-dadcb47571ec", + * client: thirdwebClient, + * }); + * + * // Possible results: + * // { + * // status: "CREATED", + * // transactions: [], + * // purchaseData: { + * // orderId: "abc-123", + * // }, + * // } + * // + * // or + * // { + * // status: "PENDING", + * // transactions: [], + * // purchaseData: { + * // orderId: "abc-123", + * // }, + * // } + * // + * // or + * // { + * // status: "COMPLETED", + * // transactions: [ + * // { + * // chainId: 1, + * // transactionHash: + * // "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + * // }, + * // ], + * // purchaseData: { + * // orderId: "abc-123", + * // }, + * // } + * ``` + * + * @param options - The options for fetching the onramp status. + * @param options.id - The UUID returned from {@link Bridge.Onramp.prepare}. + * @param options.client - Your thirdweb client instance. + * + * @returns A promise that resolves to the status of the onramp session. + * + * @throws Will throw an error if there is an issue fetching the status. + * @bridge Onramp + * @beta + */ +export async function status(options: status.Options): Promise { + const { id, client } = options; + + const clientFetch = getClientFetch(client); + const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/onramp/status`); + url.searchParams.set("id", id); + + const response = await clientFetch(url.toString()); + if (!response.ok) { + const errorJson = await response.json(); + throw new Error( + `${errorJson.code || response.status} | ${errorJson.message || response.statusText} - ${errorJson.correlationId || "N/A"}`, + ); + } + + const { data }: { data: status.Result } = await response.json(); + return data; +} + +export declare namespace status { + /** + * Input parameters for {@link Bridge.Onramp.status}. + */ + export type Options = { + /** + * The Onramp session ID returned by {@link Bridge.Onramp.prepare}. + */ + id: string; + /** Your {@link ThirdwebClient} instance. */ + client: ThirdwebClient; + }; + + /** + * The result returned from {@link Bridge.Onramp.status}. + */ + export type Result = + | { + status: "COMPLETED"; + transactions: Array<{ + chainId: number; + transactionHash: ox__Hex.Hex; + }>; + purchaseData?: unknown; + } + | { + status: "PENDING"; + transactions: Array<{ + chainId: number; + transactionHash: ox__Hex.Hex; + }>; + purchaseData?: unknown; + } + | { + status: "CREATED"; + transactions: Array<{ + chainId: number; + transactionHash: ox__Hex.Hex; + }>; + purchaseData?: unknown; + } + | { + status: "FAILED"; + transactions: Array<{ + chainId: number; + transactionHash: ox__Hex.Hex; + }>; + purchaseData?: unknown; + }; +} diff --git a/packages/thirdweb/src/bridge/index.ts b/packages/thirdweb/src/bridge/index.ts index ac1a2e48741..4626b6e5078 100644 --- a/packages/thirdweb/src/bridge/index.ts +++ b/packages/thirdweb/src/bridge/index.ts @@ -1,11 +1,19 @@ export * as Buy from "./Buy.js"; export * as Sell from "./Sell.js"; export * as Transfer from "./Transfer.js"; +export * as Onramp from "./Onramp.js"; export { status } from "./Status.js"; export { routes } from "./Routes.js"; export { chains } from "./Chains.js"; -export type { Status } from "./types/Status.js"; -export type { Route } from "./types/Route.js"; -export type { Quote, PreparedQuote } from "./types/Quote.js"; export type { Chain } from "./types/Chain.js"; +export type { Quote, PreparedQuote } from "./types/Quote.js"; +export type { + Route, + RouteQuoteStep, + RouteStep, + RouteTransaction, +} from "./types/Route.js"; +export type { Status } from "./types/Status.js"; +export type { Token } from "./types/Token.js"; +export type { BridgeAction } from "./types/BridgeAction.js"; diff --git a/packages/thirdweb/src/bridge/types/BridgeAction.ts b/packages/thirdweb/src/bridge/types/BridgeAction.ts new file mode 100644 index 00000000000..4c743d4d1b5 --- /dev/null +++ b/packages/thirdweb/src/bridge/types/BridgeAction.ts @@ -0,0 +1 @@ +export type BridgeAction = "approval" | "transfer" | "buy" | "sell"; diff --git a/packages/thirdweb/src/bridge/types/Quote.ts b/packages/thirdweb/src/bridge/types/Quote.ts index 38c6d24ea4a..080a8e41f7a 100644 --- a/packages/thirdweb/src/bridge/types/Quote.ts +++ b/packages/thirdweb/src/bridge/types/Quote.ts @@ -1,6 +1,4 @@ -import type { Hex as ox__Hex } from "ox"; -import type { Chain } from "../../chains/types.js"; -import type { ThirdwebClient } from "../../client/client.js"; +import type { RouteQuoteStep, RouteStep } from "./Route.js"; export type Quote = { /** @@ -26,29 +24,7 @@ export type Quote = { /** * The steps required to complete the quote. */ - steps: Array<{ - originToken: { - chainId: number; - address: ox__Hex.Hex; - symbol: string; - name: string; - decimals: number; - priceUsd: number; - iconUri: string; - }; - destinationToken: { - chainId: number; - address: ox__Hex.Hex; - symbol: string; - name: string; - decimals: number; - priceUsd: number; - iconUri: string; - }; - originAmount: bigint; - destinationAmount: bigint; - estimatedExecutionTimeMs: number; - }>; + steps: RouteQuoteStep[]; }; export type PreparedQuote = { @@ -79,43 +55,5 @@ export type PreparedQuote = { /** * A series of steps required to complete the quote, along with the transactions to execute in order. */ - steps: Array<{ - originToken: { - chainId: number; - address: ox__Hex.Hex; - symbol: string; - name: string; - decimals: number; - priceUsd: number; - iconUri: string; - }; - destinationToken: { - chainId: number; - address: ox__Hex.Hex; - symbol: string; - name: string; - decimals: number; - priceUsd: number; - iconUri: string; - }; - originAmount: bigint; - destinationAmount: bigint; - estimatedExecutionTimeMs: number; - transactions: Array<{ - data: ox__Hex.Hex; - to: ox__Hex.Hex; - value?: bigint | undefined; - chainId: number; - /** - * The action this transaction performs. This can be "approval", "transfer", "buy", or "sell". - */ - action: "approval" | "transfer" | "buy" | "sell"; - /** - * The transaction ID, used for tracking purposes. - */ - id: ox__Hex.Hex; - client: ThirdwebClient; - chain: Chain; - }>; - }>; + steps: RouteStep[]; }; diff --git a/packages/thirdweb/src/bridge/types/Route.ts b/packages/thirdweb/src/bridge/types/Route.ts index e4be7b4f6c5..ac23656fbde 100644 --- a/packages/thirdweb/src/bridge/types/Route.ts +++ b/packages/thirdweb/src/bridge/types/Route.ts @@ -1,6 +1,45 @@ +import type { Hex as ox__Hex } from "ox"; +import type { Chain } from "../../chains/types.js"; +import type { ThirdwebClient } from "../../client/client.js"; +import type { BridgeAction } from "./BridgeAction.js"; import type { Token } from "./Token.js"; export type Route = { originToken: Token; destinationToken: Token; }; + +export type RouteQuoteStep = { + originToken: Token; + destinationToken: Token; + originAmount: bigint; + destinationAmount: bigint; + estimatedExecutionTimeMs: number; +}; + +export type RouteStep = { + originToken: Token; + destinationToken: Token; + originAmount: bigint; + destinationAmount: bigint; + estimatedExecutionTimeMs: number; + transactions: RouteTransaction[]; +}; + +export type RouteTransaction = { + data: ox__Hex.Hex; + to: ox__Hex.Hex; + value?: bigint | undefined; + chainId: number; + + /** + * The action this transaction performs. This can be "approval", "transfer", "buy", or "sell". + */ + action: BridgeAction; + /** + * The transaction ID, used for tracking purposes. + */ + id: ox__Hex.Hex; + client: ThirdwebClient; + chain: Chain; +}; diff --git a/packages/thirdweb/src/bridge/types/Token.ts b/packages/thirdweb/src/bridge/types/Token.ts index 40ce31a8316..bf32dea7680 100644 --- a/packages/thirdweb/src/bridge/types/Token.ts +++ b/packages/thirdweb/src/bridge/types/Token.ts @@ -7,4 +7,5 @@ export type Token = { symbol: string; name: string; iconUri?: string; + priceUsd: number; }; diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index d30b22406c0..0c070cd8452 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -1,9 +1,12 @@ +import { prepare as prepareOnramp } from "../../bridge/Onramp.js"; +import { getCachedChain } from "../../chains/utils.js"; import type { ThirdwebClient } from "../../client/client.js"; +import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; +import { getContract } from "../../contract/contract.js"; +import { decimals } from "../../extensions/erc20/read/decimals.js"; import type { CurrencyMeta } from "../../react/web/ui/ConnectWallet/screens/Buy/fiat/currencies.js"; -import { getClientFetch } from "../../utils/fetch.js"; -import { stringify } from "../../utils/json.js"; +import { toTokens, toUnits } from "../../utils/units.js"; import type { FiatProvider, PayTokenInfo } from "../utils/commonTypes.js"; -import { getPayBuyWithFiatQuoteEndpoint } from "../utils/definitions.js"; /** * Parameters for [`getBuyWithFiatQuote`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatQuote) function * @deprecated @@ -274,41 +277,189 @@ export async function getBuyWithFiatQuote( params: GetBuyWithFiatQuoteParams, ): Promise { try { - const clientFetch = getClientFetch(params.client); + // map preferred provider (FiatProvider) → onramp string expected by Onramp.prepare + const mapProviderToOnramp = ( + provider?: FiatProvider, + ): "stripe" | "coinbase" | "transak" => { + switch (provider) { + case "STRIPE": + return "stripe"; + case "TRANSAK": + return "transak"; + default: // default to coinbase when undefined or any other value + return "coinbase"; + } + }; - const response = await clientFetch(getPayBuyWithFiatQuoteEndpoint(), { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: stringify({ - toAddress: params.toAddress, - fromCurrencySymbol: params.fromCurrencySymbol, - toChainId: params.toChainId.toString(), - toTokenAddress: params.toTokenAddress, - fromAmount: params.fromAmount, - toAmount: params.toAmount, - maxSlippageBPS: params.maxSlippageBPS, - isTestMode: params.isTestMode, - purchaseData: params.purchaseData, - fromAddress: params.fromAddress, - toGasAmountWei: params.toGasAmountWei, - preferredProvider: params.preferredProvider, - multiHopSupported: true, - }), + // Choose provider or default to STRIPE + const onrampProvider = mapProviderToOnramp(params.preferredProvider); + + const d = + params.toTokenAddress !== NATIVE_TOKEN_ADDRESS + ? await decimals({ + contract: getContract({ + client: params.client, + address: params.toTokenAddress, + chain: getCachedChain(params.toChainId), + }), + }) + : 18; + + // Prepare amount in wei if provided + const amountWei = params.toAmount ? toUnits(params.toAmount, d) : undefined; + + // Call new Onramp.prepare to get the quote & link + const prepared = await prepareOnramp({ + client: params.client, + onramp: onrampProvider, + chainId: params.toChainId, + tokenAddress: params.toTokenAddress, + receiver: params.toAddress, + sender: params.fromAddress, + amount: amountWei, + purchaseData: params.purchaseData, + currency: params.fromCurrencySymbol, + maxSteps: 2, + onrampTokenAddress: NATIVE_TOKEN_ADDRESS, // force onramp to native token to avoid missing gas issues }); - // Assuming the response directly matches the SwapResponse interface - if (!response.ok) { - const errorObj = await response.json(); - if (errorObj && "error" in errorObj) { - throw errorObj; - } - throw new Error(`HTTP error! status: ${response.status}`); + // Determine tokens based on steps rules + const hasSteps = prepared.steps.length > 0; + const firstStep = hasSteps + ? (prepared.steps[0] as (typeof prepared.steps)[number]) + : undefined; + + // Estimated duration in seconds – sum of all step durations + const estimatedDurationSeconds = Math.max( + 120, + Math.ceil( + prepared.steps.reduce((acc, s) => acc + s.estimatedExecutionTimeMs, 0) / + 1000, + ), + ); + + const estimatedToAmountMinWeiBigInt = prepared.destinationAmount; + + const maxSlippageBPS = params.maxSlippageBPS ?? 0; + const slippageWei = + (estimatedToAmountMinWeiBigInt * BigInt(maxSlippageBPS)) / 10000n; + const toAmountMinWeiBigInt = estimatedToAmountMinWeiBigInt - slippageWei; + + const estimatedToAmountMin = toTokens(estimatedToAmountMinWeiBigInt, d); + const toAmountMin = toTokens(toAmountMinWeiBigInt, d); + + // Helper to convert a Token → PayTokenInfo + const tokenToPayTokenInfo = (token: { + chainId: number; + address: string; + decimals: number; + symbol: string; + name: string; + priceUsd: number; + }): PayTokenInfo => ({ + chainId: token.chainId, + tokenAddress: token.address, + decimals: token.decimals, + priceUSDCents: Math.round(token.priceUsd * 100), + name: token.name, + symbol: token.symbol, + }); + + // Determine the raw token objects using new simplified rules + // 1. toToken is always the destination token + const toTokenRaw = prepared.destinationToken; + + // 2. onRampToken: if exactly one step -> originToken of that step, else toTokenRaw + const onRampTokenRaw = + prepared.steps.length > 0 && firstStep + ? firstStep.originToken + : toTokenRaw; + + // 3. routingToken: if exactly two steps -> originToken of second step, else undefined + const routingTokenRaw = + prepared.steps.length > 1 + ? (prepared.steps[1] as (typeof prepared.steps)[number]).originToken + : undefined; + + // Amounts for onRampToken/raw + const onRampTokenAmountWei: bigint = + prepared.steps.length > 0 && firstStep + ? firstStep.originAmount + : prepared.destinationAmount; + + const onRampTokenAmount = toTokens( + onRampTokenAmountWei, + onRampTokenRaw.decimals, + ); + + // Build info objects + const onRampTokenObject = { + amount: onRampTokenAmount, + amountWei: onRampTokenAmountWei.toString(), + amountUSDCents: Math.round( + Number(onRampTokenAmount) * onRampTokenRaw.priceUsd * 100, + ), + token: tokenToPayTokenInfo(onRampTokenRaw), + }; + + let routingTokenObject: + | { + amount: string; + amountWei: string; + amountUSDCents: number; + token: PayTokenInfo; + } + | undefined; + + if (routingTokenRaw) { + const routingAmountWei = ( + prepared.steps[1] as (typeof prepared.steps)[number] + ).originAmount; + const routingAmount = toTokens( + routingAmountWei, + routingTokenRaw.decimals, + ); + routingTokenObject = { + amount: routingAmount, + amountWei: routingAmountWei.toString(), + amountUSDCents: Math.round( + Number(routingAmount) * routingTokenRaw.priceUsd * 100, + ), + token: tokenToPayTokenInfo(routingTokenRaw), + }; } - return (await response.json()).result; + const buyWithFiatQuote: BuyWithFiatQuote = { + estimatedDurationSeconds, + estimatedToAmountMin: estimatedToAmountMin, + estimatedToAmountMinWei: estimatedToAmountMinWeiBigInt.toString(), + toAmountMinWei: toAmountMinWeiBigInt.toString(), + toAmountMin: toAmountMin, + fromCurrency: { + amount: prepared.currencyAmount.toString(), + amountUnits: Number(prepared.currencyAmount).toFixed(2), + decimals: 2, + currencySymbol: prepared.currency, + }, + fromCurrencyWithFees: { + amount: prepared.currencyAmount.toString(), + amountUnits: Number(prepared.currencyAmount).toFixed(2), + decimals: 2, + currencySymbol: prepared.currency, + }, + toToken: tokenToPayTokenInfo(toTokenRaw), + toAddress: params.toAddress, + fromAddress: params.fromAddress, + maxSlippageBPS: maxSlippageBPS, + intentId: prepared.id, + processingFees: [], + onRampToken: onRampTokenObject, + routingToken: routingTokenObject, + onRampLink: prepared.link, + provider: (params.preferredProvider ?? "COINBASE") as FiatProvider, + }; + + return buyWithFiatQuote; } catch (error) { console.error("Error getting buy with fiat quote", error); throw error; diff --git a/packages/thirdweb/src/pay/buyWithFiat/getStatus.ts b/packages/thirdweb/src/pay/buyWithFiat/getStatus.ts index 082dc4003a5..f60d48ad17e 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getStatus.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getStatus.ts @@ -1,10 +1,9 @@ +import { status as onrampStatus } from "../../bridge/OnrampStatus.js"; import type { ThirdwebClient } from "../../client/client.js"; -import { getClientFetch } from "../../utils/fetch.js"; import type { PayOnChainTransactionDetails, PayTokenInfo, } from "../utils/commonTypes.js"; -import { getPayBuyWithFiatStatusEndpoint } from "../utils/definitions.js"; /** * Parameters for the [`getBuyWithFiatStatus`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatStatus) function @@ -25,11 +24,6 @@ export type GetBuyWithFiatStatusParams = { intentId: string; }; -export type ValidBuyWithFiatStatus = Exclude< - BuyWithFiatStatus, - { status: "NOT_FOUND" } ->; - /** * The returned object from [`getBuyWithFiatStatus`](https://portal.thirdweb.com/references/typescript/v5/getBuyWithFiatStatus) function * @@ -50,28 +44,13 @@ export type BuyWithFiatStatus = * - `NONE` - No status * - `PENDING_PAYMENT` - Payment is not done yet in the on-ramp provider * - `PAYMENT_FAILED` - Payment failed in the on-ramp provider - * - `PENDING_ON_RAMP_TRANSFER` - Payment is done but the on-ramp provider is yet to transfer the tokens to the user's wallet - * - `ON_RAMP_TRANSFER_IN_PROGRESS` - On-ramp provider is transferring the tokens to the user's wallet * - `ON_RAMP_TRANSFER_COMPLETED` - On-ramp provider has transferred the tokens to the user's wallet - * - `ON_RAMP_TRANSFER_FAILED` - On-ramp provider failed to transfer the tokens to the user's wallet - * - `CRYPTO_SWAP_REQUIRED` - On-ramp provider has sent the tokens to the user's wallet but a swap is required to convert it to the desired token - * - `CRYPTO_SWAP_IN_PROGRESS` - Swap is in progress - * - `CRYPTO_SWAP_COMPLETED` - Swap is completed and the user has received the desired token - * - `CRYPTO_SWAP_FALLBACK` - Swap failed and the user has received a fallback token which is not the desired token */ status: | "NONE" | "PENDING_PAYMENT" | "PAYMENT_FAILED" - | "PENDING_ON_RAMP_TRANSFER" - | "ON_RAMP_TRANSFER_IN_PROGRESS" - | "ON_RAMP_TRANSFER_COMPLETED" - | "ON_RAMP_TRANSFER_FAILED" - | "CRYPTO_SWAP_REQUIRED" - | "CRYPTO_SWAP_COMPLETED" - | "CRYPTO_SWAP_FALLBACK" - | "CRYPTO_SWAP_IN_PROGRESS" - | "CRYPTO_SWAP_FAILED"; + | "ON_RAMP_TRANSFER_COMPLETED"; /** * The wallet address to which the desired tokens are sent to */ @@ -171,7 +150,6 @@ export type BuyWithFiatStatus = * }) * * // when the fiatStatus.status is "ON_RAMP_TRANSFER_COMPLETED" - the process is complete - * // when the fiatStatus.status is "CRYPTO_SWAP_REQUIRED" - start the swap process * ``` * @deprecated * @buyCrypto @@ -179,26 +157,107 @@ export type BuyWithFiatStatus = export async function getBuyWithFiatStatus( params: GetBuyWithFiatStatusParams, ): Promise { - try { - const queryParams = new URLSearchParams({ - intentId: params.intentId, - }); + const result = await onrampStatus({ + id: params.intentId, + client: params.client, + }); - const queryString = queryParams.toString(); - const url = `${getPayBuyWithFiatStatusEndpoint()}?${queryString}`; + return toBuyWithFiatStatus({ intentId: params.intentId, result }); +} - const response = await getClientFetch(params.client)(url); +//////////////////////////////////////////////////////////////////////////////// +// Helpers +//////////////////////////////////////////////////////////////////////////////// - if (!response.ok) { - const error = await response.text().catch(() => null); - throw new Error( - `HTTP error! status: ${response.status} - ${response.statusText}: ${error || "unknown error"}`, - ); - } +function toBuyWithFiatStatus(args: { + intentId: string; + result: Awaited>; +}): BuyWithFiatStatus { + const { intentId, result } = args; + + // Map status constants from the Bridge.Onramp.status response to BuyWithFiatStatus equivalents. + const STATUS_MAP: Record< + typeof result.status, + "NONE" | "PENDING_PAYMENT" | "PAYMENT_FAILED" | "ON_RAMP_TRANSFER_COMPLETED" + > = { + CREATED: "PENDING_PAYMENT", + PENDING: "PENDING_PAYMENT", + FAILED: "PAYMENT_FAILED", + COMPLETED: "ON_RAMP_TRANSFER_COMPLETED", + } as const; + + const mappedStatus = STATUS_MAP[result.status]; + + return buildPlaceholderStatus({ + intentId, + status: mappedStatus, + purchaseData: result.purchaseData, + }); +} + +function buildPlaceholderStatus(args: { + intentId: string; + status: + | "NONE" + | "PENDING_PAYMENT" + | "PAYMENT_FAILED" + | "ON_RAMP_TRANSFER_COMPLETED"; + purchaseData?: unknown; +}): BuyWithFiatStatus { + const { intentId, status, purchaseData } = args; + + // Build a minimal—but type-complete—object that satisfies BuyWithFiatStatus. + const emptyToken: PayTokenInfo = { + chainId: 0, + tokenAddress: "", + decimals: 18, + priceUSDCents: 0, + name: "", + symbol: "", + }; + + type BuyWithFiatStatusWithData = Exclude< + BuyWithFiatStatus, + { status: "NOT_FOUND" } + >; + + const quote: BuyWithFiatStatusWithData["quote"] = { + estimatedOnRampAmount: "0", + estimatedOnRampAmountWei: "0", + + estimatedToTokenAmount: "0", + estimatedToTokenAmountWei: "0", + + fromCurrency: { + amount: "0", + amountUnits: "USD", + decimals: 2, + currencySymbol: "USD", + }, + fromCurrencyWithFees: { + amount: "0", + amountUnits: "USD", + decimals: 2, + currencySymbol: "USD", + }, + onRampToken: emptyToken, + toToken: emptyToken, + estimatedDurationSeconds: 0, + createdAt: new Date().toISOString(), + } as BuyWithFiatStatusWithData["quote"]; + + // The source/destination fields can only be filled accurately when extra context is returned + // by the API. Since Bridge.Onramp.status doesn't yet provide these details, we omit them for + // now (they are optional). + + const base: Exclude = { + intentId, + status, + toAddress: "", + fromAddress: "", + quote, + purchaseData: purchaseData as object | undefined, + }; - return (await response.json()).result; - } catch (error) { - console.error("Fetch error:", error); - throw new Error(`Fetch failed: ${error}`); - } + return base; } diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts index 6cab7d3fecd..f04faf23ebc 100644 --- a/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts +++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from "vitest"; import { TEST_CLIENT } from "~test/test-clients.js"; -import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; import { base } from "../../chains/chain-definitions/base.js"; import { ethereum } from "../../chains/chain-definitions/ethereum.js"; import { sepolia } from "../../chains/chain-definitions/sepolia.js"; -import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; +import { + NATIVE_TOKEN_ADDRESS, + ZERO_ADDRESS, +} from "../../constants/addresses.js"; import { convertCryptoToFiat } from "./cryptoToFiat.js"; describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => { @@ -49,7 +51,7 @@ describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => { expect(data.result).toBe(0); }); - it("should throw error for testnet chain (because testnets are not supported", async () => { + it("should throw error for testnet chain (because testnets are not supported)", async () => { await expect( convertCryptoToFiat({ chain: sepolia, @@ -81,13 +83,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("Pay: crypto-to-fiat", () => { await expect( convertCryptoToFiat({ chain: base, - fromTokenAddress: TEST_ACCOUNT_A.address, + fromTokenAddress: ZERO_ADDRESS, fromAmount: 1, to: "USD", client: TEST_CLIENT, }), ).rejects.toThrowError( - `Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`, + `Error: ${ZERO_ADDRESS} on chainId: ${base.id} is not a valid contract address.`, ); }); it("should throw if response is not OK", async () => { diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts index abf36412c3b..1db42013418 100644 --- a/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts +++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from "vitest"; import { TEST_CLIENT } from "~test/test-clients.js"; -import { TEST_ACCOUNT_A } from "~test/test-wallets.js"; import { base } from "../../chains/chain-definitions/base.js"; import { ethereum } from "../../chains/chain-definitions/ethereum.js"; import { sepolia } from "../../chains/chain-definitions/sepolia.js"; -import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; +import { + NATIVE_TOKEN_ADDRESS, + ZERO_ADDRESS, +} from "../../constants/addresses.js"; import { convertFiatToCrypto } from "./fiatToCrypto.js"; describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => { @@ -84,13 +86,13 @@ describe.runIf(process.env.TW_SECRET_KEY)("Pay: fiatToCrypto", () => { await expect( convertFiatToCrypto({ chain: base, - to: TEST_ACCOUNT_A.address, + to: ZERO_ADDRESS, fromAmount: 1, from: "USD", client: TEST_CLIENT, }), ).rejects.toThrowError( - `Error: ${TEST_ACCOUNT_A.address} on chainId: ${base.id} is not a valid contract address.`, + `Error: ${ZERO_ADDRESS} on chainId: ${base.id} is not a valid contract address.`, ); }); it("should throw if response is not OK", async () => { diff --git a/packages/thirdweb/src/pay/utils/commonTypes.ts b/packages/thirdweb/src/pay/utils/commonTypes.ts index bd34ff2f534..2f8fc723a4d 100644 --- a/packages/thirdweb/src/pay/utils/commonTypes.ts +++ b/packages/thirdweb/src/pay/utils/commonTypes.ts @@ -19,4 +19,4 @@ export type PayOnChainTransactionDetails = { export type FiatProvider = (typeof FiatProviders)[number]; -export const FiatProviders = ["COINBASE", "STRIPE", "TRANSAK", "KADO"] as const; +export const FiatProviders = ["COINBASE", "STRIPE", "TRANSAK"] as const; diff --git a/packages/thirdweb/src/pay/utils/definitions.ts b/packages/thirdweb/src/pay/utils/definitions.ts index f0300aafb69..b8731428c28 100644 --- a/packages/thirdweb/src/pay/utils/definitions.ts +++ b/packages/thirdweb/src/pay/utils/definitions.ts @@ -7,20 +7,6 @@ const getPayBaseUrl = () => { : `https://${payDomain}`; }; -/** - * Endpoint to get a "Buy with Fiat" quote. - * @internal - */ -export const getPayBuyWithFiatQuoteEndpoint = () => - `${getPayBaseUrl()}/buy-with-fiat/quote/v1`; - -/** - * Endpoint to get the status of a "Buy with Fiat" transaction status. - * @internal - */ -export const getPayBuyWithFiatStatusEndpoint = () => - `${getPayBaseUrl()}/buy-with-fiat/status/v1`; - /** * Endpoint to get history of "Buy with Fiat" transactions for given wallet address. * @internal diff --git a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatStatus.ts b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatStatus.ts index 6a54845518d..d89c0181482 100644 --- a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatStatus.ts +++ b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatStatus.ts @@ -51,9 +51,7 @@ export function useBuyWithFiatStatus( const data = query.state.data as BuyWithFiatStatus; const status = data?.status; if ( - status === "ON_RAMP_TRANSFER_FAILED" || status === "PAYMENT_FAILED" || - status === "CRYPTO_SWAP_COMPLETED" || // onRampToken and toToken being the same means there is no additional swap step (status === "ON_RAMP_TRANSFER_COMPLETED" && data?.quote.toToken.chainId === data?.quote.onRampToken.chainId && diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/TransactionsScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/TransactionsScreen.tsx index 975e85a47b0..982c7efe698 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/TransactionsScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/TransactionsScreen.tsx @@ -1,7 +1,6 @@ "use client"; import { ExternalLinkIcon } from "@radix-ui/react-icons"; -import { useState } from "react"; import type { ThirdwebClient } from "../../../../client/client.js"; import { formatExplorerAddressUrl } from "../../../../utils/url.js"; import { iconSize } from "../../../core/design-system/index.js"; @@ -14,14 +13,10 @@ import { Spacer } from "../components/Spacer.js"; import { Container, Line, ModalHeader } from "../components/basic.js"; import { ButtonLink } from "../components/buttons.js"; import type { ConnectLocale } from "./locale/types.js"; -import { TxDetailsScreen } from "./screens/Buy/pay-transactions/TxDetailsScreen.js"; -import type { TxStatusInfo } from "./screens/Buy/pay-transactions/useBuyTransactionsToShow.js"; import type { PayerInfo } from "./screens/Buy/types.js"; import { WalletTransactionHistory } from "./screens/WalletTransactionHistory.js"; import type { WalletDetailsModalScreen } from "./screens/types.js"; -// - /** * @internal */ @@ -33,10 +28,6 @@ export function TransactionsScreen(props: { locale: ConnectLocale; client: ThirdwebClient; }) { - // const [activeTab, setActiveTab] = useState("Transactions"); - // For now, you can only select pay transactions (purcahses) - const [selectedTx, setSelectedTx] = useState(null); - const activeChain = useActiveWalletChain(); const activeWallet = useActiveWallet(); const activeAccount = useActiveAccount(); @@ -51,21 +42,6 @@ export function TransactionsScreen(props: { return ; } - if (selectedTx) { - return ( - setSelectedTx(null)} - onDone={() => setSelectedTx(null)} - payer={payer} - transactionMode={false} - isEmbed={false} - /> - ); - } - return ( @@ -80,40 +56,11 @@ export function TransactionsScreen(props: { }} > - {/* - Transactions - - ), - value: "Transactions", - // }, - // TODO (UB): add back in once we have a way to show purchases with new service - // { - // label: ( - // - // Purchases - // - // ), - // value: "Purchases", - // }, - // ]} - // selected={activeTab} - // onSelect={setActiveTab} - {/* > */} - {/* {activeTab === "Purchases" && ( */} - {/* */} - {/* )} */} - {/* {activeTab === "Transactions" && ( */} - {/* })} */} - {/* */} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx index af6605aec7c..685f1340f8e 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatSteps.tsx @@ -1,519 +1,15 @@ -import { - Cross1Icon, - ExternalLinkIcon, - TriangleDownIcon, -} from "@radix-ui/react-icons"; -import { useMemo } from "react"; -import { getCachedChain } from "../../../../../../../chains/utils.js"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import { NATIVE_TOKEN_ADDRESS } from "../../../../../../../constants/addresses.js"; -import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { formatNumber } from "../../../../../../../utils/formatNumber.js"; -import { formatExplorerTxUrl } from "../../../../../../../utils/url.js"; +import { Cross1Icon } from "@radix-ui/react-icons"; import { type Theme, - fontSize, iconSize, radius, spacing, } from "../../../../../../core/design-system/index.js"; -import { - useChainExplorers, - useChainName, -} from "../../../../../../core/hooks/others/useChainQuery.js"; -import type { TokenInfo } from "../../../../../../core/utils/defaultTokens.js"; -import { Spacer } from "../../../../components/Spacer.js"; import { Spinner } from "../../../../components/Spinner.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; -import { Button, ButtonLink } from "../../../../components/buttons.js"; +import { Container } from "../../../../components/basic.js"; import { Text } from "../../../../components/text.js"; -import { TokenSymbol } from "../../../../components/token/TokenSymbol.js"; -import { type ERC20OrNativeToken, NATIVE_TOKEN } from "../../nativeToken.js"; -import { PayTokenIcon } from "../PayTokenIcon.js"; import { StepIcon } from "../Stepper.js"; -import { - type FiatStatusMeta, - getBuyWithFiatStatusMeta, -} from "../pay-transactions/statusMeta.js"; -import { getCurrencyMeta, getFiatIcon } from "./currencies.js"; - -export type BuyWithFiatPartialQuote = { - fromCurrencySymbol: string; - fromCurrencyAmount: string; - onRampTokenAmount: string; - toTokenAmount: string; - onRampToken: { - tokenAddress: string; - name?: string; - symbol?: string; - chainId: number; - }; - - toToken: { - tokenAddress: string; - name?: string; - symbol?: string; - chainId: number; - }; -}; - -export function FiatSteps(props: { - title: string; - partialQuote: BuyWithFiatPartialQuote; - status?: BuyWithFiatStatus; - onBack: () => void; - client: ThirdwebClient; - step: number; - onContinue: () => void; -}) { - const statusMeta = props.status - ? getBuyWithFiatStatusMeta(props.status) - : undefined; - - const { - toToken: toTokenMeta, - onRampToken: onRampTokenMeta, - onRampTokenAmount, - fromCurrencySymbol, - fromCurrencyAmount, - toTokenAmount, - } = props.partialQuote; - - const currency = getCurrencyMeta(fromCurrencySymbol); - const isPartialSuccess = statusMeta?.progressStatus === "partialSuccess"; - - const toChain = useMemo( - () => getCachedChain(toTokenMeta.chainId), - [toTokenMeta.chainId], - ); - - const destinationChain = useMemo(() => { - if (props.status?.status !== "NOT_FOUND" && props.status?.destination) { - return getCachedChain(props.status?.destination.token.chainId); - } - - return undefined; - }, [props.status]); - - const toToken: ERC20OrNativeToken = useMemo(() => { - if (toTokenMeta.tokenAddress === NATIVE_TOKEN_ADDRESS) { - return NATIVE_TOKEN; - } - - const tokenInfo: TokenInfo = { - address: toTokenMeta.tokenAddress, - name: toTokenMeta.name || "", - symbol: toTokenMeta.symbol || "", - // TODO: when icon is available in endpoint - // icon: toTokenMeta.icon - }; - return tokenInfo; - }, [toTokenMeta]); - - const onRampChain = useMemo( - () => getCachedChain(onRampTokenMeta.chainId), - [onRampTokenMeta.chainId], - ); - - const onRampToken: ERC20OrNativeToken = useMemo(() => { - if (onRampTokenMeta.tokenAddress === NATIVE_TOKEN_ADDRESS) { - return NATIVE_TOKEN; - } - - const tokenInfo: TokenInfo = { - address: onRampTokenMeta.tokenAddress, - name: onRampTokenMeta.name || "", - symbol: onRampTokenMeta.symbol || "", - // TODO: when icon is available in endpoint - // icon: onRampTokenMeta.icon, - }; - return tokenInfo; - }, [onRampTokenMeta]); - - const onRampName = useChainName(onRampChain); - const onRampExplorers = useChainExplorers(onRampChain); - const toChainName = useChainName(toChain); - const toChainExplorers = useChainExplorers(toChain); - const destinationName = useChainName(destinationChain); - - const onRampTokenInfo = ( -
- - {formatNumber(Number(onRampTokenAmount), 6)}{" "} - - -
- ); - - const fiatIcon = getFiatIcon(currency, "sm"); - - const onRampTokenIcon = ( - - ); - - const toTokenIcon = ( - - ); - - const onRampChainInfo = {onRampName.name}; - - const partialSuccessToTokenInfo = - props.status?.status === "CRYPTO_SWAP_FALLBACK" && - props.status.destination ? ( -
- - {formatNumber(Number(toTokenAmount), 6)}{" "} - - {" "} - - {formatNumber(Number(props.status.destination.amount), 6)}{" "} - - -
- ) : null; - - const toTokenInfo = partialSuccessToTokenInfo || ( - - {formatNumber(Number(toTokenAmount), 6)}{" "} - - - ); - - const partialSuccessToChainInfo = - props.status?.status === "CRYPTO_SWAP_FALLBACK" && - props.status.destination && - props.status.destination.token.chainId !== - props.status.quote.toToken.chainId ? ( -
- - {toChainName.name} - {" "} - - {destinationName.name} - -
- ) : null; - - const toTokehChainInfo = partialSuccessToChainInfo || ( - {toChainName.name} - ); - - const onRampTxHash = - props.status?.status !== "NOT_FOUND" - ? props.status?.source?.transactionHash - : undefined; - - const toTokenTxHash = - props.status?.status !== "NOT_FOUND" - ? props.status?.destination?.transactionHash - : undefined; - - const showContinueBtn = - !props.status || - props.status.status === "CRYPTO_SWAP_REQUIRED" || - props.status.status === "CRYPTO_SWAP_FAILED"; - - function getStep1State(): FiatStatusMeta["progressStatus"] { - if (!statusMeta) { - if (props.step === 2) { - return "completed"; - } - return "actionRequired"; - } - - if (statusMeta.step === 2) { - return "completed"; - } - - return statusMeta.progressStatus; - } - - function getStep2State(): FiatStatusMeta["progressStatus"] | undefined { - if (!statusMeta) { - if (props.step === 2) { - return "actionRequired"; - } - return undefined; - } - - if (statusMeta.step === 2) { - return statusMeta.progressStatus; - } - - return undefined; - } - - return ( - - - - - {/* Step 1 */} - - Get{" "} - {" "} - with {props.partialQuote.fromCurrencySymbol} - - } - step={1} - from={{ - icon: fiatIcon, - primaryText: ( - - {formatNumber(Number(fromCurrencyAmount), 6)} {fromCurrencySymbol} - - ), - }} - to={{ - icon: onRampTokenIcon, - primaryText: onRampTokenInfo, - secondaryText: onRampChainInfo, - }} - state={getStep1State()} - explorer={ - onRampExplorers.explorers[0]?.url && onRampTxHash - ? { - label: "View on Explorer", - url: formatExplorerTxUrl( - onRampExplorers.explorers[0]?.url, - onRampTxHash, - ), - } - : undefined - } - /> - - - - - Convert{" "} - {" "} - to - - } - step={2} - from={{ - icon: onRampTokenIcon, - primaryText: onRampTokenInfo, - secondaryText: onRampChainInfo, - }} - to={{ - icon: toTokenIcon, - primaryText: toTokenInfo, - secondaryText: toTokehChainInfo, - }} - state={getStep2State()} - explorer={ - toChainExplorers.explorers[0]?.url && toTokenTxHash - ? { - label: "View on Explorer", - url: formatExplorerTxUrl( - toChainExplorers.explorers[0]?.url, - toTokenTxHash, - ), - } - : undefined - } - /> - - {isPartialSuccess && - props.status && - props.status.status !== "NOT_FOUND" && - props.status.source && - props.status.destination && ( - <> - - - Expected {props.status.source?.token.symbol}, Got{" "} - {props.status.destination?.token.symbol} instead - - - - )} - - {showContinueBtn && ( - <> - - - - )} - - ); -} - -function PaymentStep(props: { - step: number; - title: React.ReactNode; - state?: FiatStatusMeta["progressStatus"]; - from: { - icon: React.ReactNode; - primaryText: React.ReactNode; - secondaryText?: React.ReactNode; - }; - to: { - icon: React.ReactNode; - primaryText: React.ReactNode; - secondaryText?: React.ReactNode; - }; - iconText?: string; - explorer?: { - label: string; - url: string; - }; -}) { - return ( - - Step {props.step} - - {props.title} - - - - - - - - {/* TODO - replace this with SVG */} -
- - - - - - {props.explorer && ( - <> - - - {props.explorer.label} - - - - )} - - ); -} - -function PaymentSubStep(props: { - icon: React.ReactNode; - primaryText: React.ReactNode; - secondaryText?: React.ReactNode; -}) { - return ( - - {/* icon */} - - {props.icon} - - - {props.primaryText} {props.secondaryText} - - - ); -} +import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js"; export function StepContainer(props: { state?: FiatStatusMeta["progressStatus"]; @@ -535,9 +31,6 @@ export function StepContainer(props: { } else if (props.state === "failed") { color = "danger"; text = "Failed"; - } else if (props.state === "partialSuccess") { - color = "danger"; - text = "Incomplete"; } return ( diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx deleted file mode 100644 index 218c9d26210..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/FiatTxDetailsTable.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { ExternalLinkIcon } from "@radix-ui/react-icons"; -import { getCachedChain } from "../../../../../../../chains/utils.js"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import { formatNumber } from "../../../../../../../utils/formatNumber.js"; -import { formatExplorerTxUrl } from "../../../../../../../utils/url.js"; -import { - fontSize, - iconSize, -} from "../../../../../../core/design-system/index.js"; -import { useChainExplorers } from "../../../../../../core/hooks/others/useChainQuery.js"; -import { Spacer } from "../../../../components/Spacer.js"; -import { Container, Line } from "../../../../components/basic.js"; -import { ButtonLink } from "../../../../components/buttons.js"; -import { Text } from "../../../../components/text.js"; -import { TokenInfoRow } from "../pay-transactions/TokenInfoRow.js"; -import type { FiatStatusMeta } from "../pay-transactions/statusMeta.js"; -import { getCurrencyMeta, getFiatIcon } from "./currencies.js"; - -/** - * Show a table with the details of a "OnRamp" transaction step in the "Buy with Fiat" flow. - * - Show OnRamp token as "Receive" - * - Show fiat amount as "Pay" - */ -export function OnRampTxDetailsTable(props: { - client: ThirdwebClient; - token: { - chainId: number; - address: string; - symbol: string; - amount: string; - }; - fiat: { - currencySymbol: string; - amount: string; - }; - statusMeta?: { - color: FiatStatusMeta["color"]; - text: FiatStatusMeta["status"]; - txHash?: string; - }; -}) { - const onRampExplorers = useChainExplorers( - getCachedChain(props.token.chainId), - ); - const onrampTxHash = props.statusMeta?.txHash; - const currencyMeta = getCurrencyMeta(props.fiat.currencySymbol); - - const lineSpacer = ( - <> - - - - - ); - - return ( -
- {/* Pay */} - - Pay - - - {getFiatIcon(currencyMeta, "sm")} - - {formatNumber(Number(props.fiat.amount), 2)}{" "} - {props.fiat.currencySymbol} - - - - - - {lineSpacer} - - {/* Receive */} - - - {/* Status */} - {props.statusMeta && ( - <> - {lineSpacer} - - Status - - - {props.statusMeta.text} - - - - - )} - - {lineSpacer} - - {/* Transaction Hash link */} - {onrampTxHash && onRampExplorers.explorers?.[0]?.url && ( - <> - - - View on Explorer - - - - )} -
- ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx index 971651d215b..7f69f31f824 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/OnRampScreen.tsx @@ -496,19 +496,12 @@ function useOnRampStatus(props: { switch (statusQuery.data?.status) { case "ON_RAMP_TRANSFER_COMPLETED": - case "CRYPTO_SWAP_COMPLETED": - case "CRYPTO_SWAP_REQUIRED": uiStatus = "completed"; break; - case "CRYPTO_SWAP_FALLBACK": - uiStatus = "partialSuccess"; - break; - case "ON_RAMP_TRANSFER_FAILED": case "PAYMENT_FAILED": uiStatus = "failed"; break; case "PENDING_PAYMENT": - case "ON_RAMP_TRANSFER_IN_PROGRESS": uiStatus = "pending"; break; default: @@ -522,10 +515,7 @@ function useOnRampStatus(props: { return; } - if ( - statusQuery.data && - (uiStatus === "completed" || uiStatus === "partialSuccess") - ) { + if (statusQuery.data && uiStatus === "completed") { purchaseCbCalled.current = true; props.onSuccess(statusQuery.data); } @@ -537,7 +527,7 @@ function useOnRampStatus(props: { return; } - if (uiStatus === "completed" || uiStatus === "partialSuccess") { + if (uiStatus === "completed") { try { if (props.openedWindow && !props.openedWindow.closed) { props.openedWindow.close(); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx deleted file mode 100644 index 200820251b3..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwap.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import { getCachedChain } from "../../../../../../../chains/utils.js"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import { getContract } from "../../../../../../../contract/contract.js"; -import { allowance } from "../../../../../../../extensions/erc20/__generated__/IERC20/read/allowance.js"; -import type { BuyWithCryptoQuote } from "../../../../../../../pay/buyWithCrypto/getQuote.js"; -import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; -import { getPostOnRampQuote } from "../../../../../../../pay/buyWithFiat/getPostOnRampQuote.js"; -import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { iconSize } from "../../../../../../core/design-system/index.js"; -import { Spacer } from "../../../../components/Spacer.js"; -import { Spinner } from "../../../../components/Spinner.js"; -import { Container, ModalHeader } from "../../../../components/basic.js"; -import { Button } from "../../../../components/buttons.js"; -import { Text } from "../../../../components/text.js"; -import { AccentFailIcon } from "../../../icons/AccentFailIcon.js"; -import { SwapFlow } from "../swap/SwapFlow.js"; -import type { PayerInfo } from "../types.js"; - -export function PostOnRampSwap(props: { - title: string; - client: ThirdwebClient; - buyWithFiatStatus: BuyWithFiatStatus; - onBack?: () => void; - onDone: () => void; - transactionMode: boolean; - isEmbed: boolean; - payer: PayerInfo; - onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined; -}) { - const [lockedOnRampQuote, setLockedOnRampQuote] = useState< - BuyWithCryptoQuote | undefined - >(undefined); - - const postOnRampQuoteQuery = useQuery({ - queryKey: ["getPostOnRampQuote", props.buyWithFiatStatus], - queryFn: async () => { - return await getPostOnRampQuote({ - client: props.client, - buyWithFiatStatus: props.buyWithFiatStatus, - }); - }, - // stop fetching if a quote is already locked - enabled: !lockedOnRampQuote, - refetchOnWindowFocus: false, - }); - - const allowanceQuery = useQuery({ - queryKey: [ - "allowance", - props.payer.account.address, - postOnRampQuoteQuery.data?.approvalData, - ], - queryFn: () => { - if (!postOnRampQuoteQuery.data?.approvalData) { - return null; - } - return allowance({ - contract: getContract({ - client: props.client, - address: postOnRampQuoteQuery.data.swapDetails.fromToken.tokenAddress, - chain: getCachedChain( - postOnRampQuoteQuery.data.swapDetails.fromToken.chainId, - ), - }), - spender: postOnRampQuoteQuery.data.approvalData.spenderAddress, - owner: props.payer.account.address, - }); - }, - enabled: !!postOnRampQuoteQuery.data?.approvalData, - refetchOnMount: true, - }); - - useEffect(() => { - if ( - postOnRampQuoteQuery.data && - !lockedOnRampQuote && - !postOnRampQuoteQuery.isRefetching && - !allowanceQuery.isLoading - ) { - setLockedOnRampQuote(postOnRampQuoteQuery.data); - } - }, [ - postOnRampQuoteQuery.data, - lockedOnRampQuote, - postOnRampQuoteQuery.isRefetching, - allowanceQuery.isLoading, - ]); - - if (postOnRampQuoteQuery.isError) { - return ( - - - - - - - - - Failed to get a price quote - - - - - - - - ); - } - - if (!lockedOnRampQuote) { - return ( - - - - - - - - - Getting price quote - - - - - ); - } - - return ( - { - setLockedOnRampQuote(undefined); - postOnRampQuoteQuery.refetch(); - }} - transactionMode={props.transactionMode} - isEmbed={props.isEmbed} - onSuccess={props.onSuccess} - approvalAmount={allowanceQuery.data ?? undefined} - /> - ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx deleted file mode 100644 index 1bde4e8c767..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/fiat/PostOnRampSwapFlow.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useState } from "react"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; -import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import type { PayerInfo } from "../types.js"; -import { type BuyWithFiatPartialQuote, FiatSteps } from "./FiatSteps.js"; -import { PostOnRampSwap } from "./PostOnRampSwap.js"; - -// Note: It is necessary to lock in the fiat-status in state and only pass that to so it does not suddenly change during the swap process. - -/** - * - Show 2 steps UI with step 2 highlighted, on continue button click: - * - Show swap flow - */ -export function PostOnRampSwapFlow(props: { - title: string; - status: BuyWithFiatStatus; - quote: BuyWithFiatPartialQuote; - client: ThirdwebClient; - onBack: () => void; - onDone: () => void; - onSwapFlowStarted: () => void; - transactionMode: boolean; - isEmbed: boolean; - payer: PayerInfo; - onSuccess: ((status: BuyWithCryptoStatus) => void) | undefined; -}) { - const [statusForSwap, setStatusForSwap] = useState< - BuyWithFiatStatus | undefined - >(); - - // step 2 flow - if (statusForSwap) { - return ( - - ); - } - - // show step 1 and step 2 details - return ( - { - props.onSwapFlowStarted(); - setStatusForSwap(props.status); - }} - status={props.status} - /> - ); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx deleted file mode 100644 index 3d2c8e7ed65..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/FiatDetailsScreen.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useState } from "react"; -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import type { - BuyWithFiatStatus, - ValidBuyWithFiatStatus, -} from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { useBuyWithFiatStatus } from "../../../../../../core/hooks/pay/useBuyWithFiatStatus.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; -import { OnRampTxDetailsTable } from "../fiat/FiatTxDetailsTable.js"; -import { PostOnRampSwapFlow } from "../fiat/PostOnRampSwapFlow.js"; -import type { PayerInfo } from "../types.js"; -import { getBuyWithFiatStatusMeta } from "./statusMeta.js"; - -export function FiatDetailsScreen(props: { - title: string; - status: ValidBuyWithFiatStatus; - onBack: () => void; - client: ThirdwebClient; - onDone: () => void; - transactionMode: boolean; - isEmbed: boolean; - payer: PayerInfo; -}) { - const initialStatus = props.status; - const [stopPolling, setStopPolling] = useState(false); - - const statusQuery = useBuyWithFiatStatus( - stopPolling - ? undefined - : { - client: props.client, - intentId: initialStatus.intentId, - }, - ); - - const status: ValidBuyWithFiatStatus = - (statusQuery.data?.status === "NOT_FOUND" ? undefined : statusQuery.data) || - initialStatus; - - const hasTwoSteps = isSwapRequiredAfterOnRamp(status); - const statusMeta = getBuyWithFiatStatusMeta(status); - - if (hasTwoSteps) { - const fiatQuote = status.quote; - return ( - { - setStopPolling(true); - }} - payer={props.payer} - // viewing history - ignore onSuccess - onSuccess={undefined} - /> - ); - } - - return ( - - - - - - - - - - - - ); -} - -// if the toToken is the same as the onRampToken, no swap is required -function isSwapRequiredAfterOnRamp(buyWithFiatStatus: BuyWithFiatStatus) { - if (buyWithFiatStatus.status === "NOT_FOUND") { - return false; - } - - const sameChain = - buyWithFiatStatus.quote.toToken.chainId === - buyWithFiatStatus.quote.onRampToken.chainId; - - const sameToken = - buyWithFiatStatus.quote.toToken.tokenAddress === - buyWithFiatStatus.quote.onRampToken.tokenAddress; - - return !(sameChain && sameToken); -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/SwapDetailsScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/SwapDetailsScreen.tsx index 2280b771d64..90cfebaf9bc 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/SwapDetailsScreen.tsx +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/SwapDetailsScreen.tsx @@ -13,50 +13,14 @@ import { useChainExplorers, useChainName, } from "../../../../../../core/hooks/others/useChainQuery.js"; -import { useBuyWithCryptoStatus } from "../../../../../../core/hooks/pay/useBuyWithCryptoStatus.js"; import { Spacer } from "../../../../components/Spacer.js"; -import { Container, Line, ModalHeader } from "../../../../components/basic.js"; +import { Container, Line } from "../../../../components/basic.js"; import { ButtonLink } from "../../../../components/buttons.js"; import { Text } from "../../../../components/text.js"; import { WalletRow } from "../swap/WalletRow.js"; import { TokenInfoRow } from "./TokenInfoRow.js"; import { type StatusMeta, getBuyWithCryptoStatusMeta } from "./statusMeta.js"; -export function SwapDetailsScreen(props: { - status: ValidBuyWithCryptoStatus; - onBack: () => void; - client: ThirdwebClient; -}) { - const { status: initialStatus, client } = props; - const statusQuery = useBuyWithCryptoStatus( - initialStatus.source?.transactionHash - ? { - client: client, - transactionHash: initialStatus.source.transactionHash, - chainId: initialStatus.source.token.chainId, - } - : undefined, - ); - - const status: ValidBuyWithCryptoStatus = - (statusQuery.data?.status !== "NOT_FOUND" ? statusQuery.data : undefined) || - initialStatus; - - return ( - - - - - - - - - - - - ); -} - type SwapTxDetailsData = { fromToken: { chainId: number; diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/TxDetailsScreen.tsx b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/TxDetailsScreen.tsx deleted file mode 100644 index 4ab8deee3bc..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/TxDetailsScreen.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { ThirdwebClient } from "../../../../../../../client/client.js"; -import type { PayerInfo } from "../types.js"; -import { FiatDetailsScreen } from "./FiatDetailsScreen.js"; -import { SwapDetailsScreen } from "./SwapDetailsScreen.js"; -import type { TxStatusInfo } from "./useBuyTransactionsToShow.js"; - -export function TxDetailsScreen(props: { - title: string; - client: ThirdwebClient; - statusInfo: TxStatusInfo; - onBack: () => void; - onDone: () => void; - transactionMode: boolean; - isEmbed: boolean; - payer: PayerInfo; -}) { - const { statusInfo } = props; - - if (statusInfo.type === "swap") { - return ( - - ); - } - - if (statusInfo.type === "fiat") { - return ( - - ); - } - - return null; -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.test.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.test.ts index bf681b6e55e..a4eebbfabf8 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.test.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from "vitest"; import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; -import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; -import { - getBuyWithCryptoStatusMeta, - getBuyWithFiatStatusMeta, -} from "./statusMeta.js"; +import { getBuyWithCryptoStatusMeta } from "./statusMeta.js"; describe("getBuyWithCryptoStatusMeta", () => { it('returns "Unknown" for NOT_FOUND status', () => { @@ -82,93 +78,3 @@ describe("getBuyWithCryptoStatusMeta", () => { }); }); }); - -describe("getBuyWithFiatStatusMeta", () => { - it('returns "Incomplete" for CRYPTO_SWAP_FALLBACK status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "CRYPTO_SWAP_FALLBACK", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Incomplete", - color: "danger", - step: 2, - progressStatus: "partialSuccess", - }); - }); - - it('returns "Pending" for CRYPTO_SWAP_IN_PROGRESS status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "CRYPTO_SWAP_IN_PROGRESS", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Pending", - color: "accentText", - loading: true, - step: 2, - progressStatus: "pending", - }); - }); - - it('returns "Pending" for PENDING_ON_RAMP_TRANSFER status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "PENDING_ON_RAMP_TRANSFER", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Pending", - color: "accentText", - loading: true, - step: 1, - progressStatus: "pending", - }); - }); - - it('returns "Completed" for ON_RAMP_TRANSFER_COMPLETED status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "ON_RAMP_TRANSFER_COMPLETED", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Completed", - color: "success", - loading: true, - step: 1, - progressStatus: "completed", - }); - }); - - it('returns "Action Required" for CRYPTO_SWAP_REQUIRED status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "CRYPTO_SWAP_REQUIRED", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Action Required", - color: "accentText", - step: 2, - progressStatus: "actionRequired", - }); - }); - - it('returns "Failed" for PAYMENT_FAILED status', () => { - const result = getBuyWithFiatStatusMeta({ - status: "PAYMENT_FAILED", - } as BuyWithFiatStatus); - expect(result).toEqual({ - status: "Failed", - color: "danger", - step: 1, - progressStatus: "failed", - }); - }); - - it('returns "Unknown" for unhandled status', () => { - const result = getBuyWithFiatStatusMeta({ - // @ts-ignore - status: "UNKNOWN_STATUS", - }); - expect(result).toEqual({ - status: "Unknown", - color: "secondaryText", - step: 1, - progressStatus: "unknown", - }); - }); -}); diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.ts index 5ce3ed5524c..3238d9112a5 100644 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.ts +++ b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/statusMeta.ts @@ -1,5 +1,4 @@ import type { BuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; -import type { BuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; import type { Theme } from "../../../../../../core/design-system/index.js"; export type StatusMeta = { @@ -74,73 +73,5 @@ export type FiatStatusMeta = { | "completed" | "failed" | "actionRequired" - | "partialSuccess" | "unknown"; }; -export function getBuyWithFiatStatusMeta( - fiatStatus: BuyWithFiatStatus, -): FiatStatusMeta { - const status = fiatStatus.status; - - switch (status) { - case "CRYPTO_SWAP_FALLBACK": { - return { - status: "Incomplete", - color: "danger", - step: 2, - progressStatus: "partialSuccess", - }; - } - - case "CRYPTO_SWAP_IN_PROGRESS": - case "PENDING_ON_RAMP_TRANSFER": - case "ON_RAMP_TRANSFER_IN_PROGRESS": - case "PENDING_PAYMENT": { - return { - status: "Pending", - color: "accentText", - loading: true, - step: status === "CRYPTO_SWAP_IN_PROGRESS" ? 2 : 1, - progressStatus: "pending", - }; - } - - case "ON_RAMP_TRANSFER_COMPLETED": - case "CRYPTO_SWAP_COMPLETED": { - return { - status: "Completed", // Is this actually completed though? - color: "success", - loading: true, - step: status === "CRYPTO_SWAP_COMPLETED" ? 2 : 1, - progressStatus: "completed", - }; - } - - case "CRYPTO_SWAP_FAILED": - case "CRYPTO_SWAP_REQUIRED": { - return { - status: "Action Required", - color: "accentText", - step: 2, - progressStatus: "actionRequired", - }; - } - - case "PAYMENT_FAILED": - case "ON_RAMP_TRANSFER_FAILED": { - return { - status: "Failed", - color: "danger", - step: 1, - progressStatus: "failed", - }; - } - } - - return { - status: "Unknown", - color: "secondaryText", - step: 1, - progressStatus: "unknown", - }; -} diff --git a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/useBuyTransactionsToShow.ts b/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/useBuyTransactionsToShow.ts deleted file mode 100644 index 79ecf8e2d1e..00000000000 --- a/packages/thirdweb/src/react/web/ui/ConnectWallet/screens/Buy/pay-transactions/useBuyTransactionsToShow.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ValidBuyWithCryptoStatus } from "../../../../../../../pay/buyWithCrypto/getStatus.js"; -import type { ValidBuyWithFiatStatus } from "../../../../../../../pay/buyWithFiat/getStatus.js"; - -export type TxStatusInfo = - | { - type: "swap"; - status: ValidBuyWithCryptoStatus; - } - | { - type: "fiat"; - status: ValidBuyWithFiatStatus; - };