diff --git a/.changeset/fix-otp-getclientfetch-bundle-id.md b/.changeset/fix-otp-getclientfetch-bundle-id.md new file mode 100644 index 00000000000..478f94c93b5 --- /dev/null +++ b/.changeset/fix-otp-getclientfetch-bundle-id.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Fix: in-app wallet email/phone OTP login (`sendOtp`/`verifyOtp`) now routes through `getClientFetch` instead of the global `fetch`. Previously these calls never attached the SDK's platform headers, so on React Native the `x-bundle-id` header was never sent — making it impossible to use email/phone OTP login with a Bundle ID access restriction configured on the client ID, since the backend rejects requests missing that header with a 401. diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.test.ts b/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.test.ts new file mode 100644 index 00000000000..b0478ddd4f5 --- /dev/null +++ b/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import { getClientFetch } from "../../../../../utils/fetch.js"; +import { sendOtp, verifyOtp } from "./otp.js"; + +vi.mock("../../../../../utils/fetch.js"); + +describe("sendOtp", () => { + it("should route the request through getClientFetch so platform headers (x-bundle-id) are attached", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({}), + ok: true, + }); + vi.mocked(getClientFetch).mockReturnValue(mockFetch); + + await sendOtp({ + client: TEST_CLIENT, + email: "user@example.com", + strategy: "email", + }); + + expect(getClientFetch).toHaveBeenCalledWith(TEST_CLIENT, undefined); + expect(mockFetch).toHaveBeenCalled(); + }); + + it("should forward the ecosystem to getClientFetch so ecosystem headers are attached", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({}), + ok: true, + }); + vi.mocked(getClientFetch).mockReturnValue(mockFetch); + + const ecosystem = { id: "ecosystem.test" as const, partnerId: "partner-1" }; + await sendOtp({ + client: TEST_CLIENT, + ecosystem, + email: "user@example.com", + strategy: "email", + }); + + expect(getClientFetch).toHaveBeenCalledWith(TEST_CLIENT, ecosystem); + }); +}); + +describe("verifyOtp", () => { + it("should route the request through getClientFetch so platform headers (x-bundle-id) are attached", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({}), + ok: true, + }); + vi.mocked(getClientFetch).mockReturnValue(mockFetch); + + await verifyOtp({ + client: TEST_CLIENT, + email: "user@example.com", + strategy: "email", + verificationCode: "123456", + }); + + expect(getClientFetch).toHaveBeenCalledWith(TEST_CLIENT, undefined); + expect(mockFetch).toHaveBeenCalled(); + }); + + it("should forward the ecosystem to getClientFetch so ecosystem headers are attached", async () => { + const mockFetch = vi.fn().mockResolvedValue({ + json: () => Promise.resolve({}), + ok: true, + }); + vi.mocked(getClientFetch).mockReturnValue(mockFetch); + + const ecosystem = { id: "ecosystem.test" as const, partnerId: "partner-1" }; + await verifyOtp({ + client: TEST_CLIENT, + ecosystem, + email: "user@example.com", + strategy: "email", + verificationCode: "123456", + }); + + expect(getClientFetch).toHaveBeenCalledWith(TEST_CLIENT, ecosystem); + }); +}); diff --git a/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.ts b/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.ts index 7a12a2fb49b..58f701526fe 100644 --- a/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.ts +++ b/packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.ts @@ -1,4 +1,5 @@ import type { ThirdwebClient } from "../../../../../client/client.js"; +import { getClientFetch } from "../../../../../utils/fetch.js"; import { stringify } from "../../../../../utils/json.js"; import { getLoginCallbackUrl, @@ -20,17 +21,8 @@ export const sendOtp = async (args: PreAuthArgsType): Promise => { const headers: Record = { "Content-Type": "application/json", - "x-client-id": client.clientId, }; - if (ecosystem?.id) { - headers["x-ecosystem-id"] = ecosystem.id; - } - - if (ecosystem?.partnerId) { - headers["x-ecosystem-partner-id"] = ecosystem.partnerId; - } - const body = (() => { switch (args.strategy) { case "email": @@ -44,7 +36,7 @@ export const sendOtp = async (args: PreAuthArgsType): Promise => { } })(); - const response = await fetch(url, { + const response = await getClientFetch(client, ecosystem)(url, { body: stringify(body), headers, method: "POST", @@ -85,17 +77,8 @@ export const verifyOtp = async ( const headers: Record = { "Content-Type": "application/json", - "x-client-id": client.clientId, }; - if (ecosystem?.id) { - headers["x-ecosystem-id"] = ecosystem.id; - } - - if (ecosystem?.partnerId) { - headers["x-ecosystem-partner-id"] = ecosystem.partnerId; - } - const body = (() => { switch (args.strategy) { case "email": @@ -111,7 +94,7 @@ export const verifyOtp = async ( } })(); - const response = await fetch(url, { + const response = await getClientFetch(client, ecosystem)(url, { body: stringify(body), headers, method: "POST",