diff --git a/.changeset/chatty-peaches-drop.md b/.changeset/chatty-peaches-drop.md new file mode 100644 index 00000000000..2ff9b8afc7d --- /dev/null +++ b/.changeset/chatty-peaches-drop.md @@ -0,0 +1,5 @@ +--- +"thirdweb": minor +--- + +[SDK] Add RampNow as a new onramp provider diff --git a/packages/thirdweb/src/bridge/Onramp.test.ts b/packages/thirdweb/src/bridge/Onramp.test.ts index 73870489cfd..37dcd34d906 100644 --- a/packages/thirdweb/src/bridge/Onramp.test.ts +++ b/packages/thirdweb/src/bridge/Onramp.test.ts @@ -115,4 +115,33 @@ describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Onramp.prepare", () => { // Steps array should be defined (it may be empty if the provider supports the destination token natively) expect(Array.isArray(prepared.steps)).toBe(true); }); + + // The Rampnow live-API test is gated on a separate env var because the + // server-side schema for `onramp: "rampnow"` lands in a separate deploy. + // Once `api.thirdweb-dev.com` accepts the new provider, drop the runIf and + // mirror the stripe/coinbase/transak assertions above. + it.runIf(process.env.TW_BRIDGE_RAMPNOW)( + "should prepare a Rampnow onramp successfully", + async () => { + const prepared = await Onramp.prepare({ + amount: toWei("0.01"), + chainId: 1, + client: TEST_CLIENT, + onramp: "rampnow", + receiver: RECEIVER_ADDRESS, + tokenAddress: NATIVE_TOKEN_ADDRESS, + }); + + expect(prepared).toBeDefined(); + expect(typeof prepared.destinationAmount).toBe("bigint"); + expect(prepared.destinationAmount > 0n).toBe(true); + expect(prepared.link).toBeDefined(); + expect(typeof prepared.link).toBe("string"); + expect(prepared.intent).toBeDefined(); + expect(prepared.intent.receiver.toLowerCase()).toBe( + RECEIVER_ADDRESS.toLowerCase(), + ); + expect(Array.isArray(prepared.steps)).toBe(true); + }, + ); }); diff --git a/packages/thirdweb/src/bridge/Onramp.ts b/packages/thirdweb/src/bridge/Onramp.ts index 266386d74b9..3627c874617 100644 --- a/packages/thirdweb/src/bridge/Onramp.ts +++ b/packages/thirdweb/src/bridge/Onramp.ts @@ -13,7 +13,7 @@ import type { TokenWithPrices } from "./types/Token.js"; export { status } from "./OnrampStatus.js"; type OnrampIntent = { - onramp: "stripe" | "coinbase" | "transak"; + onramp: "stripe" | "coinbase" | "transak" | "rampnow"; chainId: number; tokenAddress: ox__Address.Address; receiver: ox__Address.Address; @@ -42,7 +42,7 @@ type OnrampPrepareQuoteResponseData = { // Explicit type for the API request body interface OnrampApiRequestBody { - onramp: "stripe" | "coinbase" | "transak"; + onramp: "stripe" | "coinbase" | "transak" | "rampnow"; chainId: number; tokenAddress: ox__Address.Address; receiver: ox__Address.Address; @@ -128,7 +128,7 @@ interface OnrampApiRequestBody { * * @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.onramp - The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow"). * @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. @@ -272,8 +272,8 @@ export declare namespace prepare { export type Options = { /** Your thirdweb client */ client: ThirdwebClient; - /** The onramp provider to use (e.g., "stripe", "coinbase", "transak") */ - onramp: "stripe" | "coinbase" | "transak"; + /** The onramp provider to use (e.g., "stripe", "coinbase", "transak", "rampnow") */ + onramp: "stripe" | "coinbase" | "transak" | "rampnow"; /** The destination chain ID */ chainId: number; /** The destination token address */ diff --git a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts index a7ad3b11a0c..49b8d07365b 100644 --- a/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts +++ b/packages/thirdweb/src/pay/buyWithFiat/getQuote.ts @@ -290,12 +290,14 @@ export async function getBuyWithFiatQuote( // map preferred provider (FiatProvider) → onramp string expected by Onramp.prepare const mapProviderToOnramp = ( provider?: FiatProvider, - ): "stripe" | "coinbase" | "transak" => { + ): "stripe" | "coinbase" | "transak" | "rampnow" => { switch (provider) { case "stripe": return "stripe"; case "transak": return "transak"; + case "rampnow": + return "rampnow"; default: // default to coinbase when undefined or any other value return "coinbase"; } @@ -463,7 +465,7 @@ export async function getBuyWithFiatQuote( onRampLink: prepared.link, onRampToken: onRampTokenObject, processingFees: [], - provider: (params.preferredProvider ?? "COINBASE") as FiatProvider, + provider: params.preferredProvider ?? "coinbase", routingToken: routingTokenObject, toAddress: params.toAddress, toAmountMin: toAmountMin, diff --git a/packages/thirdweb/src/pay/utils/commonTypes.ts b/packages/thirdweb/src/pay/utils/commonTypes.ts index f4543d174a9..a8f6830db33 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]; -const FiatProviders = ["coinbase", "stripe", "transak"] as const; +const FiatProviders = ["coinbase", "stripe", "transak", "rampnow"] as const; diff --git a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.test.tsx b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.test.tsx new file mode 100644 index 00000000000..4e9e9ce727e --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.test.tsx @@ -0,0 +1,109 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import type React from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import { prepare as prepareOnramp } from "../../../../bridge/Onramp.js"; +import { getToken } from "../../../../pay/convert/get-token.js"; +import { useBuyWithFiatQuotesForProviders } from "./useBuyWithFiatQuotesForProviders.js"; + +vi.mock("../../../../bridge/Onramp.js"); +vi.mock("../../../../pay/convert/get-token.js"); + +const RECEIVER = "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709" as const; +const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as const; + +describe("useBuyWithFiatQuotesForProviders", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getToken).mockResolvedValue({ + address: USDC, + chainId: 1, + decimals: 6, + name: "USD Coin", + priceUsd: 1, + prices: { USD: 1 }, + symbol: "USDC", + }); + // Return a minimal valid prepared-onramp response per call. + vi.mocked(prepareOnramp).mockImplementation(async (opts) => ({ + currency: opts.currency ?? "USD", + currencyAmount: 1, + destinationAmount: opts.amount ?? 1n, + destinationToken: { + address: opts.tokenAddress, + chainId: opts.chainId, + decimals: 6, + name: "USD Coin", + priceUsd: 1, + prices: { USD: 1 }, + symbol: "USDC", + }, + id: `mock-${opts.onramp}`, + intent: { + amount: (opts.amount ?? 1n).toString(), + chainId: opts.chainId, + onramp: opts.onramp, + receiver: opts.receiver, + tokenAddress: opts.tokenAddress, + }, + link: `https://example.com/${opts.onramp}`, + steps: [], + timestamp: 0, + })); + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + return ( + {children} + ); + }; + + it("fans out to all four onramp providers (including rampnow)", async () => { + const { result } = renderHook( + () => + useBuyWithFiatQuotesForProviders({ + amount: "10", + chainId: 1, + client: TEST_CLIENT, + country: "US", + currency: "USD", + receiver: RECEIVER, + tokenAddress: USDC, + }), + { wrapper }, + ); + + await waitFor(() => { + expect(result.current.every((q) => q.isSuccess)).toBe(true); + }); + + expect(result.current).toHaveLength(4); + + // Each provider should be called exactly once with the expected `onramp` value. + const calls = vi.mocked(prepareOnramp).mock.calls.map((c) => c[0].onramp); + expect(calls.sort()).toEqual( + ["coinbase", "rampnow", "stripe", "transak"].sort(), + ); + + // The hook should surface the prepared link per provider. + const links = result.current.map((q) => q.data?.link); + expect(links).toContain("https://example.com/rampnow"); + expect(links).toContain("https://example.com/stripe"); + expect(links).toContain("https://example.com/coinbase"); + expect(links).toContain("https://example.com/transak"); + }); + + it("is disabled when no params are provided", () => { + const { result } = renderHook(() => useBuyWithFiatQuotesForProviders(), { + wrapper, + }); + + expect(result.current).toHaveLength(4); + expect(result.current.every((q) => !q.isSuccess && !q.isError)).toBe(true); + expect(vi.mocked(prepareOnramp)).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts index 62cec3c4558..93f08e15d17 100644 --- a/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts +++ b/packages/thirdweb/src/react/core/hooks/pay/useBuyWithFiatQuotesForProviders.ts @@ -60,13 +60,13 @@ type UseBuyWithFiatQuotesForProvidersResult = { /** * @internal - * Hook to get prepared onramp quotes from Coinbase, Stripe, and Transak providers. + * Hook to get prepared onramp quotes from Coinbase, Stripe, Transak, and Rampnow providers. */ export function useBuyWithFiatQuotesForProviders( params?: UseBuyWithFiatQuotesForProvidersParams, queryOptions?: OnrampQuoteQueryOptions, ): UseBuyWithFiatQuotesForProvidersResult { - const providers = ["coinbase", "stripe", "transak"] as const; + const providers = ["coinbase", "stripe", "transak", "rampnow"] as const; const queries = useQueries({ queries: providers.map((provider) => ({ diff --git a/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts index 0426d8f3ba8..2eb748f7a2d 100644 --- a/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts +++ b/packages/thirdweb/src/react/core/hooks/useBridgePrepare.test.ts @@ -81,6 +81,23 @@ describe("useBridgePrepare", () => { expect(onrampRequest.client).toBe(mockClient); }); + it("should accept all supported onramp providers including rampnow", () => { + const providers = ["stripe", "coinbase", "transak", "rampnow"] as const; + for (const onramp of providers) { + const onrampRequest: BridgePrepareRequest = { + amount: 1000000n, + chainId: 1, + client: mockClient, + onramp, + receiver: "0x1234567890123456789012345678901234567890", + tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + type: "onramp", + }; + expect(onrampRequest.type).toBe("onramp"); + expect(onrampRequest.onramp).toBe(onramp); + } + }); + it("should handle UseBridgePrepareParams with enabled option", () => { const params: UseBridgePrepareParams = { amount: 1000000n, diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx index 1cbe3040fb0..7620bb6eaff 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/FiatProviderSelection.tsx @@ -21,7 +21,9 @@ import { Text } from "../../components/text.js"; interface FiatProviderSelectionProps { client: ThirdwebClient; - onProviderSelected: (provider: "coinbase" | "stripe" | "transak") => void; + onProviderSelected: ( + provider: "coinbase" | "stripe" | "transak" | "rampnow", + ) => void; toChainId: number; toTokenAddress: string; toAddress: string; @@ -49,6 +51,12 @@ const PROVIDERS = [ id: "transak" as const, name: "Transak", }, + { + description: "Cards, bank transfers and more", + iconUri: "https://app.rampnow.io/favicon.ico", + id: "rampnow" as const, + name: "Rampnow", + }, ]; export function FiatProviderSelection({ diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx index 6f85d72654f..8b981c6e7d5 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/PaymentSelection.tsx @@ -184,7 +184,7 @@ export function PaymentSelection({ }; const handleOnrampProviderSelected = ( - provider: "coinbase" | "stripe" | "transak", + provider: "coinbase" | "stripe" | "transak" | "rampnow", ) => { const recipientAddress = receiverAddress || payerWallet?.getAccount()?.address; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/types.ts b/packages/thirdweb/src/react/web/ui/Bridge/types.ts index 442e0126b14..569a48f19a5 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/types.ts +++ b/packages/thirdweb/src/react/web/ui/Bridge/types.ts @@ -44,5 +44,5 @@ export type PaymentMethod = type: "fiat"; payerWallet?: Wallet; currency: SupportedFiatCurrency; - onramp: "stripe" | "coinbase" | "transak"; + onramp: "stripe" | "coinbase" | "transak" | "rampnow"; };