From df54ba29e0626b124b760cf4330933134770a4e2 Mon Sep 17 00:00:00 2001 From: blockgroot <170620375+blockgroot@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:39:05 +0530 Subject: [PATCH] [SDK] fix: route in-app wallet OTP login through getClientFetch sendOtp and verifyOtp built request headers by hand and called the global fetch directly, instead of going through getClientFetch like every other in-app wallet call. getClientFetch is the only place that attaches platform headers, including x-bundle-id on React Native, so OTP login never sent x-bundle-id even when it was available. Since the backend enforces the Bundle ID access restriction on this endpoint, any client ID with that restriction enabled rejected all email/phone OTP login attempts with a 401. Swap both functions to getClientFetch(client, ecosystem), matching the pattern already used in siwe.ts, and drop the now-redundant manual x-client-id/ecosystem headers since getClientFetch sets those itself. Fixes #8774 --- .../fix-otp-getclientfetch-bundle-id.md | 5 ++ .../wallets/in-app/web/lib/auth/otp.test.ts | 82 +++++++++++++++++++ .../src/wallets/in-app/web/lib/auth/otp.ts | 23 +----- 3 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 .changeset/fix-otp-getclientfetch-bundle-id.md create mode 100644 packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.test.ts 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",