Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-otp-getclientfetch-bundle-id.md
Original file line number Diff line number Diff line change
@@ -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.
82 changes: 82 additions & 0 deletions packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
23 changes: 3 additions & 20 deletions packages/thirdweb/src/wallets/in-app/web/lib/auth/otp.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,17 +21,8 @@ export const sendOtp = async (args: PreAuthArgsType): Promise<void> => {

const headers: Record<string, string> = {
"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":
Expand All @@ -44,7 +36,7 @@ export const sendOtp = async (args: PreAuthArgsType): Promise<void> => {
}
})();

const response = await fetch(url, {
const response = await getClientFetch(client, ecosystem)(url, {
body: stringify(body),
headers,
method: "POST",
Expand Down Expand Up @@ -85,17 +77,8 @@ export const verifyOtp = async (

const headers: Record<string, string> = {
"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":
Expand All @@ -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",
Expand Down
Loading