Skip to content

Commit 21cc45a

Browse files
Winston/tool 3161 cannot create backend wallets using latest version (#6025)
1 parent a67d342 commit 21cc45a

File tree

6 files changed

+132
-7
lines changed

6 files changed

+132
-7
lines changed

.changeset/two-trees-tease.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
fix (in app wallets): error when calling connect for backend strategy due to `document` reference

packages/thirdweb/src/wallets/in-app/core/authentication/backend.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export async function backendAuthenticate(args: {
3030
}),
3131
});
3232

33-
if (!res.ok) throw new Error("Failed to generate backend account");
33+
if (!res.ok) {
34+
throw new Error("Failed to generate backend account");
35+
}
3436

3537
return (await res.json()) satisfies AuthStoredTokenWithCookieReturnType;
3638
}

packages/thirdweb/src/wallets/in-app/web/lib/auth/iframe-auth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ export class Auth {
107107
authToken: AuthStoredTokenWithCookieReturnType,
108108
recoveryCode?: string,
109109
): Promise<AuthLoginReturnType> {
110-
await this.preLogin();
110+
// We don't call logout for backend auth because that is handled on the backend where the iframe isn't available to call. Moreover, logout clears the local storage which isn't applicable for backend auth.
111+
if (authToken.storedToken.authProvider !== "Backend") {
112+
await this.preLogin();
113+
}
111114

112115
const user = await getUserStatus({
113116
authToken: authToken.storedToken.cookieString,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { IframeCommunicator } from "./IframeCommunicator.js";
3+
4+
describe("IframeCommunicator", () => {
5+
// biome-ignore lint/suspicious/noExplicitAny: mock
6+
let mockLocalStorage: any;
7+
let mockContainer: HTMLElement;
8+
let mockIframe: HTMLIFrameElement;
9+
10+
beforeEach(() => {
11+
// Mock localStorage
12+
vi.restoreAllMocks();
13+
14+
mockLocalStorage = {
15+
getAuthCookie: vi.fn().mockResolvedValue("mockAuthCookie"),
16+
getDeviceShare: vi.fn().mockResolvedValue("mockDeviceShare"),
17+
getWalletUserId: vi.fn().mockResolvedValue("mockWalletUserId"),
18+
};
19+
// Mock DOM elements
20+
mockContainer = document.createElement("div");
21+
mockIframe = document.createElement("iframe");
22+
vi.spyOn(document, "createElement").mockReturnValue(mockIframe);
23+
vi.spyOn(document, "getElementById").mockReturnValue(null);
24+
});
25+
26+
it("should create an iframe with correct properties", () => {
27+
new IframeCommunicator({
28+
link: "https://example.com",
29+
baseUrl: "https://example.com",
30+
iframeId: "test-iframe",
31+
container: mockContainer,
32+
localStorage: mockLocalStorage,
33+
clientId: "test-client",
34+
});
35+
36+
expect(document.createElement).toHaveBeenCalledWith("iframe");
37+
expect(mockIframe.id).toBe("test-iframe");
38+
expect(mockIframe.src).toBe("https://example.com/");
39+
expect(mockIframe.style.display).toBe("none");
40+
});
41+
42+
it("should initialize with correct variables", async () => {
43+
const communicator = new IframeCommunicator({
44+
link: "https://example.com",
45+
baseUrl: "https://example.com",
46+
iframeId: "test-iframe",
47+
container: mockContainer,
48+
localStorage: mockLocalStorage,
49+
clientId: "test-client",
50+
});
51+
52+
// biome-ignore lint/complexity/useLiteralKeys: accessing protected method
53+
const vars = await communicator["onIframeLoadedInitVariables"]();
54+
55+
expect(vars).toEqual({
56+
authCookie: "mockAuthCookie",
57+
deviceShareStored: "mockDeviceShare",
58+
walletUserId: "mockWalletUserId",
59+
clientId: "test-client",
60+
partnerId: undefined,
61+
ecosystemId: undefined,
62+
});
63+
});
64+
65+
it("should throw error when calling methods without iframe", async () => {
66+
const temp = global.document;
67+
// @ts-expect-error - Testing undefined document scenario
68+
global.document = undefined;
69+
70+
const communicator = new IframeCommunicator({
71+
link: "https://example.com",
72+
baseUrl: "https://example.com",
73+
iframeId: "test-iframe",
74+
localStorage: mockLocalStorage,
75+
clientId: "test-client",
76+
});
77+
78+
await expect(
79+
communicator.call({
80+
procedureName: "test",
81+
params: {},
82+
}),
83+
).rejects.toThrow("Iframe not found");
84+
global.document = temp;
85+
});
86+
87+
it("should cleanup on destroy", () => {
88+
const communicator = new IframeCommunicator({
89+
link: "https://example.com",
90+
baseUrl: "https://example.com",
91+
iframeId: "test-iframe",
92+
container: mockContainer,
93+
localStorage: mockLocalStorage,
94+
clientId: "test-client",
95+
});
96+
97+
communicator.destroy();
98+
99+
// biome-ignore lint/complexity/useLiteralKeys: accessing protected field
100+
expect(communicator["iframe"]).toBeDefined();
101+
});
102+
});

packages/thirdweb/src/wallets/in-app/web/utils/iFrameCommunication/IframeCommunicator.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const isIframeLoaded = new Map<string, boolean>();
3535
*/
3636
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix later
3737
export class IframeCommunicator<T extends { [key: string]: any }> {
38-
private iframe: HTMLIFrameElement;
38+
private iframe?: HTMLIFrameElement;
3939
private POLLING_INTERVAL_SECONDS = 1.4;
4040
private iframeBaseUrl;
4141
protected localStorage: ClientScopedStorage;
@@ -49,7 +49,7 @@ export class IframeCommunicator<T extends { [key: string]: any }> {
4949
link,
5050
baseUrl,
5151
iframeId,
52-
container = document.body,
52+
container,
5353
onIframeInitialize,
5454
localStorage,
5555
clientId,
@@ -60,6 +60,10 @@ export class IframeCommunicator<T extends { [key: string]: any }> {
6060
this.ecosystem = ecosystem;
6161
this.iframeBaseUrl = baseUrl;
6262

63+
if (typeof document === "undefined") {
64+
return;
65+
}
66+
container = container ?? document.body;
6367
// Creating the IFrame element for communication
6468
let iframe = document.getElementById(iframeId) as HTMLIFrameElement | null;
6569
const hrefLink = new URL(link);
@@ -164,6 +168,11 @@ export class IframeCommunicator<T extends { [key: string]: any }> {
164168
params: T[keyof T];
165169
showIframe?: boolean;
166170
}) {
171+
if (!this.iframe) {
172+
throw new Error(
173+
"Iframe not found. You are likely calling this from the backend where the DOM is not available.",
174+
);
175+
}
167176
while (!isIframeLoaded.get(this.iframe.src)) {
168177
await sleep(this.POLLING_INTERVAL_SECONDS * 1000);
169178
}
@@ -182,7 +191,9 @@ export class IframeCommunicator<T extends { [key: string]: any }> {
182191
if (showIframe) {
183192
// magic number to let modal fade out before hiding it
184193
await sleep(0.1 * 1000);
185-
this.iframe.style.display = "none";
194+
if (this.iframe) {
195+
this.iframe.style.display = "none";
196+
}
186197
}
187198
if (!data.success) {
188199
rej(new Error(data.error));
@@ -213,6 +224,8 @@ export class IframeCommunicator<T extends { [key: string]: any }> {
213224
* @internal
214225
*/
215226
destroy() {
216-
isIframeLoaded.delete(this.iframe.src);
227+
if (this.iframe) {
228+
isIframeLoaded.delete(this.iframe.src);
229+
}
217230
}
218231
}

packages/thirdweb/src/wallets/in-app/web/utils/iFrameCommunication/InAppWalletIframeCommunicator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class InAppWalletIframeCommunicator<
3232
baseUrl,
3333
}).href,
3434
baseUrl,
35-
container: document.body,
35+
container: typeof document === "undefined" ? undefined : document.body,
3636
localStorage: new ClientScopedStorage({
3737
storage: webLocalStorage,
3838
clientId,

0 commit comments

Comments
 (0)