From 49cfd5eeb339a8383f186e5f07d8c8c6e720f20c Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Tue, 17 Jun 2025 14:05:07 -0700 Subject: [PATCH 1/2] fix: stringify bigint erc20Value in useTransactionDetails --- .changeset/fluffy-paws-slide.md | 5 +++ .../src/components/pay/transaction-button.tsx | 33 ++++++++----------- .../react/core/hooks/useTransactionDetails.ts | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 .changeset/fluffy-paws-slide.md diff --git a/.changeset/fluffy-paws-slide.md b/.changeset/fluffy-paws-slide.md new file mode 100644 index 00000000000..34d19e6c14c --- /dev/null +++ b/.changeset/fluffy-paws-slide.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fix setting explicit amount on TransactionWidget diff --git a/apps/playground-web/src/components/pay/transaction-button.tsx b/apps/playground-web/src/components/pay/transaction-button.tsx index 3d4fd7e3b53..d080ca77d76 100644 --- a/apps/playground-web/src/components/pay/transaction-button.tsx +++ b/apps/playground-web/src/components/pay/transaction-button.tsx @@ -40,25 +40,20 @@ export function PayTransactionPreview() { }); return ( - <> - -
- {account && ( - - )} - + ); } diff --git a/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts index a6c79f48a48..50b1fef70d0 100644 --- a/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts +++ b/packages/thirdweb/src/react/core/hooks/useTransactionDetails.ts @@ -57,7 +57,7 @@ export function useTransactionDetails({ "transaction-details", transaction.to, transaction.chain.id, - transaction.erc20Value, + transaction.erc20Value?.toString(), ], queryFn: async (): Promise => { // Create contract instance for metadata fetching From 51d1bd9f086d3f26fea39cbc946517ec9b54f5fe Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Tue, 17 Jun 2025 14:09:50 -0700 Subject: [PATCH 2/2] fix: remove incorrect disableOnramp documentation --- .../react/core/hooks/useBridgeError.test.ts | 172 --------- .../core/hooks/usePaymentMethods.test.ts | 336 ------------------ .../react/web/ui/Bridge/CheckoutWidget.tsx | 11 - .../react/web/ui/Bridge/TransactionWidget.tsx | 17 - 4 files changed, 536 deletions(-) delete mode 100644 packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts delete mode 100644 packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts b/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts deleted file mode 100644 index b47fa3b8654..00000000000 --- a/packages/thirdweb/src/react/core/hooks/useBridgeError.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { ApiError } from "../../../bridge/types/Errors.js"; -import { useBridgeError } from "./useBridgeError.js"; - -describe("useBridgeError", () => { - it("should handle null error", () => { - const result = useBridgeError({ error: null }); - - expect(result).toEqual({ - mappedError: null, - isRetryable: false, - userMessage: "", - errorCode: null, - statusCode: null, - isClientError: false, - isServerError: false, - }); - }); - - it("should handle undefined error", () => { - const result = useBridgeError({ error: undefined }); - - expect(result).toEqual({ - mappedError: null, - isRetryable: false, - userMessage: "", - errorCode: null, - statusCode: null, - isClientError: false, - isServerError: false, - }); - }); - - it("should process ApiError correctly", () => { - const apiError = new ApiError({ - code: "INVALID_INPUT", - message: "Invalid parameters provided", - statusCode: 400, - }); - - const result = useBridgeError({ error: apiError }); - - expect(result.mappedError).toBeInstanceOf(ApiError); - expect(result.errorCode).toBe("INVALID_INPUT"); - expect(result.statusCode).toBe(400); - expect(result.isClientError).toBe(true); - expect(result.isServerError).toBe(false); - expect(result.isRetryable).toBe(false); // INVALID_INPUT is not retryable - expect(result.userMessage).toBe( - "Invalid input provided. Please check your parameters and try again.", - ); - }); - - it("should convert generic Error to ApiError", () => { - const genericError = new Error("Network connection failed"); - - const result = useBridgeError({ error: genericError }); - - expect(result.mappedError).toBeInstanceOf(ApiError); - expect(result.errorCode).toBe("UNKNOWN_ERROR"); - expect(result.statusCode).toBe(500); - expect(result.isClientError).toBe(false); - expect(result.isServerError).toBe(true); - expect(result.isRetryable).toBe(true); - expect(result.userMessage).toBe( - "An unexpected error occurred. Please try again.", - ); - }); - - it("should identify server errors correctly", () => { - const serverError = new ApiError({ - code: "INTERNAL_SERVER_ERROR", - message: "Server error", - statusCode: 500, - }); - - const result = useBridgeError({ error: serverError }); - - expect(result.statusCode).toBe(500); - expect(result.isClientError).toBe(false); - expect(result.isServerError).toBe(true); - expect(result.isRetryable).toBe(true); // INTERNAL_SERVER_ERROR is retryable - expect(result.userMessage).toBe( - "A temporary error occurred. Please try again in a moment.", - ); - }); - - it("should provide user-friendly messages for known error codes", () => { - // Test INVALID_INPUT - const invalidInputError = new ApiError({ - code: "INVALID_INPUT", - message: "Technical error message", - statusCode: 400, - }); - const invalidInputResult = useBridgeError({ error: invalidInputError }); - expect(invalidInputResult.userMessage).toBe( - "Invalid input provided. Please check your parameters and try again.", - ); - - // Test INTERNAL_SERVER_ERROR - const serverError = new ApiError({ - code: "INTERNAL_SERVER_ERROR", - message: "Technical error message", - statusCode: 500, - }); - const serverResult = useBridgeError({ error: serverError }); - expect(serverResult.userMessage).toBe( - "A temporary error occurred. Please try again in a moment.", - ); - }); - - it("should use original error message for unknown error codes", () => { - const unknownError = new ApiError({ - code: "UNKNOWN_ERROR", - message: "Custom error message", - statusCode: 418, - }); - - const result = useBridgeError({ error: unknownError }); - - expect(result.userMessage).toBe( - "An unexpected error occurred. Please try again.", - ); - expect(result.errorCode).toBe("UNKNOWN_ERROR"); - }); - - it("should detect client vs server errors correctly", () => { - // Client error (4xx) - const clientError = new ApiError({ - code: "INVALID_INPUT", - message: "Bad request", - statusCode: 400, - }); - - const clientResult = useBridgeError({ error: clientError }); - expect(clientResult.isClientError).toBe(true); - expect(clientResult.isServerError).toBe(false); - - // Server error (5xx) - const serverError = new ApiError({ - code: "INTERNAL_SERVER_ERROR", - message: "Internal error", - statusCode: 503, - }); - - const serverResult = useBridgeError({ error: serverError }); - expect(serverResult.isClientError).toBe(false); - expect(serverResult.isServerError).toBe(true); - - // No status code - const noStatusError = new ApiError({ - code: "UNKNOWN_ERROR", - message: "Unknown error", - statusCode: 500, - }); - - const noStatusResult = useBridgeError({ error: noStatusError }); - expect(noStatusResult.isClientError).toBe(false); - expect(noStatusResult.isServerError).toBe(true); // 500 is a server error - }); - - it("should handle Error without message", () => { - const errorWithoutMessage = new Error(); - - const result = useBridgeError({ error: errorWithoutMessage }); - - expect(result.userMessage).toBe( - "An unexpected error occurred. Please try again.", - ); - expect(result.errorCode).toBe("UNKNOWN_ERROR"); - }); -}); diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts deleted file mode 100644 index 92a68f5da1e..00000000000 --- a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.test.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * @vitest-environment happy-dom - */ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import { createElement } from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { routes } from "../../../bridge/Routes.js"; -import type { Token } from "../../../bridge/types/Token.js"; -import type { ThirdwebClient } from "../../../client/client.js"; -import { usePaymentMethods } from "./usePaymentMethods.js"; - -// Mock the routes API -vi.mock("../../../bridge/Routes.js", () => ({ - routes: vi.fn(), -})); - -const mockRoutes = vi.mocked(routes); - -// Mock data -const mockDestinationToken: Token = { - chainId: 1, - address: "0xA0b86a33E6441aA7A6fbEEc9bb27e5e8bc3b8eD7", - decimals: 6, - symbol: "USDC", - name: "USD Coin", - priceUsd: 1.0, -}; - -const mockClient = { - clientId: "test-client-id", -} as ThirdwebClient; - -const mockRouteData = [ - { - originToken: { - chainId: 1, - address: "0xA0b86a33E6441aA7A6fbEEc9bb27e5e8bc3b8eD7", - decimals: 18, - symbol: "ETH", - name: "Ethereum", - priceUsd: 2000, - }, - destinationToken: mockDestinationToken, - steps: [], - }, - { - originToken: { - chainId: 137, - address: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", - decimals: 6, - symbol: "USDC", - name: "USD Coin", - priceUsd: 1.0, - }, - destinationToken: mockDestinationToken, - steps: [], - }, - { - originToken: { - chainId: 42161, - address: "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", - decimals: 6, - symbol: "USDC", - name: "USD Coin", - priceUsd: 1.0, - }, - destinationToken: mockDestinationToken, - steps: [], - }, -]; - -// Test wrapper component -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }); - - return ({ children }: { children: React.ReactNode }) => - createElement(QueryClientProvider, { client: queryClient }, children); -}; - -describe("usePaymentMethods", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("should require destinationToken and client parameters", () => { - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - expect(result.current).toBeDefined(); - expect(result.current.isLoading).toBe(true); - }); - - it("should fetch routes and transform data correctly", async () => { - mockRoutes.mockResolvedValueOnce(mockRouteData); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - // Initially loading - expect(result.current.isLoading).toBe(true); - expect(result.current.data).toEqual([]); - - // Wait for query to resolve - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Should have transformed data - expect(result.current.data).toHaveLength(4); // 3 wallet methods + 1 fiat method - - const walletMethod = result.current.data[0]; - expect(walletMethod?.type).toBe("wallet"); - if (walletMethod?.type === "wallet") { - expect(walletMethod.originToken).toEqual(mockRouteData[0]?.originToken); - } - - const fiatMethod = result.current.data[3]; - expect(fiatMethod?.type).toBe("fiat"); - if (fiatMethod?.type === "fiat") { - expect(fiatMethod.currency).toBe("USD"); - } - }); - - it("should call routes API with correct parameters", async () => { - mockRoutes.mockResolvedValueOnce(mockRouteData); - const wrapper = createWrapper(); - - renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(mockRoutes).toHaveBeenCalledWith({ - client: mockClient, - destinationChainId: mockDestinationToken.chainId, - destinationTokenAddress: mockDestinationToken.address, - sortBy: "popularity", - limit: 50, - }); - }); - }); - - it("should handle empty routes data", async () => { - mockRoutes.mockResolvedValueOnce([]); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Should only have fiat method when no routes - expect(result.current.data).toHaveLength(1); - expect(result.current.data[0]).toEqual({ - type: "fiat", - currency: "USD", - }); - }); - - it("should handle API errors gracefully", async () => { - const mockError = new Error("API Error"); - mockRoutes.mockRejectedValueOnce(mockError); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toBeTruthy(); - expect(result.current.data).toEqual([]); - }); - - it("should deduplicate origin tokens", async () => { - // Mock data with duplicate origin tokens - const firstRoute = mockRouteData[0]; - if (!firstRoute) { - throw new Error("Mock data is invalid"); - } - - const mockDataWithDuplicates = [ - ...mockRouteData, - { - originToken: firstRoute.originToken, // Duplicate ETH - destinationToken: mockDestinationToken, - steps: [], - }, - ]; - - mockRoutes.mockResolvedValueOnce(mockDataWithDuplicates); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - // Should still have only 4 methods (3 unique wallet + 1 fiat) - expect(result.current.data).toHaveLength(4); - - // Check that ETH only appears once - const walletMethods = result.current.data.filter( - (m) => m.type === "wallet", - ); - const ethMethods = walletMethods.filter( - (m) => m.type === "wallet" && m.originToken?.symbol === "ETH", - ); - expect(ethMethods).toHaveLength(1); - }); - - it("should always include fiat payment option", async () => { - mockRoutes.mockResolvedValueOnce(mockRouteData); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - const fiatMethods = result.current.data.filter((m) => m.type === "fiat"); - expect(fiatMethods).toHaveLength(1); - expect(fiatMethods[0]).toEqual({ - type: "fiat", - currency: "USD", - }); - }); - - it("should have correct query key for caching", async () => { - mockRoutes.mockResolvedValueOnce(mockRouteData); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - // The hook should use a query key that includes chain ID and token address - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(mockRoutes).toHaveBeenCalledTimes(1); - }); - - it("should provide refetch functionality", async () => { - mockRoutes.mockResolvedValueOnce(mockRouteData); - const wrapper = createWrapper(); - - const { result } = renderHook( - () => - usePaymentMethods({ - destinationToken: mockDestinationToken, - destinationAmount: "1", - client: mockClient, - }), - { wrapper }, - ); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(typeof result.current.refetch).toBe("function"); - }); -}); diff --git a/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx index 163b09ccc4f..49d22926827 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/CheckoutWidget.tsx @@ -198,17 +198,6 @@ type UIOptionsResult = * /> * ``` * - * ### Enable/Disable payment methods - * - * You can use `disableOnramps` to prevent the use of onramps in the widget. - * - * ```tsx - * - * ``` - * * ### Customize the UI * * You can customize the UI of the `CheckoutWidget` component by passing a custom theme object to the `theme` prop. diff --git a/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx b/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx index 6d32d79aadb..540b0c485d0 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/TransactionWidget.tsx @@ -201,23 +201,6 @@ type UIOptionsResult = * /> * ``` * - * ### Enable/Disable payment methods - * - * You can use `disableOnramps` to prevent the use of onramps in the widget. - * - * ```tsx - * - * ``` - * * ### Customize the UI * * You can customize the UI of the `TransactionWidget` component by passing a custom theme object to the `theme` prop.