From a6e932980de234f6eb32028ccae7f30c20e0990d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 9 May 2026 21:11:05 +0000 Subject: [PATCH] Drop pre-emptive popup for unauth wallet_connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host now renders inline in the iframe for unauthenticated sessions (email OTP, SMS, passkey) — Privy's auth `allowed_domains` already covers the wallet host origin, so these flows don't need a popup. External OAuth providers (Google, Apple, …) still demand a top-level surface; the Privy SDK handles those by opening its own provider popup directly from the iframe. This removes the `TOP_LEVEL_AUTH_METHODS` set + `requiresTopLevelAuth` gate from `pickInitialMode`. The iframe is now the default surface for every request unless the caller forces popup mode or the iframe explicitly escalates via `__internal { type: 'switch', mode: 'popup' }` (IO-v2-unsupported parents not on the trusted-host list, Safari-only WebAuthn enrollment). Updates Wallet.test to reflect the new behaviour: `wallet_connect` under auto mode no longer pre-opens a popup; explicit `dialog: 'popup'` override still does. https://claude.ai/code/session_018kuvZqC6ReXAqjGbJt1yNz --- packages/wallet-sdk/src/core/Wallet.ts | 36 +++++++-------------- packages/wallet-sdk/test/src/Wallet.test.ts | 28 ++++++++-------- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/packages/wallet-sdk/src/core/Wallet.ts b/packages/wallet-sdk/src/core/Wallet.ts index fe5e89d..d67f696 100644 --- a/packages/wallet-sdk/src/core/Wallet.ts +++ b/packages/wallet-sdk/src/core/Wallet.ts @@ -4,20 +4,19 @@ * the appropriate dialog when the request requires user confirmation. * * Mode selection: - * 1. Start auth/account-creation requests in popup mode when the wallet - * session is missing or unknown because social OAuth providers - * (including Google) reject embedded iframe user agents. - * 2. If the prewarmed iframe reports an authenticated session, auth methods - * can stay in iframe mode. - * 3. Otherwise start with `iframe` unless the caller forced popup mode. - * 4. After the iframe's `ready` arrives, run `secure()`. If the parent is + * 1. Default to `iframe` for every request unless the caller explicitly + * forced popup mode. The wallet host renders inline auth surfaces + * (email OTP, passkey, SMS) inside the iframe and only requests popup + * escalation when the user picks an external OAuth provider via + * `__internal { type: 'switch', mode: 'popup' }`. + * 2. After the iframe's `ready` arrives, run `secure()`. If the parent is * on HTTP, or if IntersectionObserver v2 is unavailable AND the parent * origin isn't on the wallet host's trusted-host allowlist, transparently * switch to popup before sending pending requests. - * 5. The wallet host can request a runtime switch at any time by emitting + * 3. The wallet host can request a runtime switch at any time by emitting * `__internal { type: 'switch', mode: 'popup' }` over the messenger — - * used for cases the dApp side can't predict (e.g. WebAuthn credential - * creation on Safari, which Safari blocks in cross-origin iframes). + * used for cases the dApp side can't predict (external OAuth providers + * that reject embedded user agents, Safari-only WebAuthn enrollment). */ import * as Dialog from "./Dialog.js"; @@ -62,12 +61,6 @@ export type Wallet = { // ---------- Implementation ---------- -const TOP_LEVEL_AUTH_METHODS = new Set([ - "wallet_connect", - "eth_requestAccounts", - "wallet_requestPermissions", -]); - export function createWallet(config: WalletConfig): Wallet { const { host, @@ -169,15 +162,8 @@ export function createWallet(config: WalletConfig): Wallet { previous?.destroy(); } - function requiresTopLevelAuth(request: Messenger.RpcRequest): boolean { - return TOP_LEVEL_AUTH_METHODS.has(request.method); - } - - function pickInitialMode(request: Messenger.RpcRequest): Dialog.DialogMode { + function pickInitialMode(): Dialog.DialogMode { if (mode === "popup") return "popup"; - if (requiresTopLevelAuth(request) && latestReady?.authenticated !== true) { - return "popup"; - } return "iframe"; } @@ -230,7 +216,7 @@ export function createWallet(config: WalletConfig): Wallet { params, }; - const initialMode = pickInitialMode(rpc); + const initialMode = pickInitialMode(); const handle = ensureActive(initialMode); // After the wallet announces ready, validate that iframe mode is actually diff --git a/packages/wallet-sdk/test/src/Wallet.test.ts b/packages/wallet-sdk/test/src/Wallet.test.ts index b29d83b..ab2c881 100644 --- a/packages/wallet-sdk/test/src/Wallet.test.ts +++ b/packages/wallet-sdk/test/src/Wallet.test.ts @@ -35,40 +35,43 @@ describe("createWallet", () => { }); }); - it("opens wallet_connect in a top-level popup in auto mode", async () => { + it("opens wallet_connect in the iframe in auto mode (host handles inline auth)", () => { const openSpy = vi.spyOn(window, "open").mockReturnValue(mockPopupWindow()); const wallet = createWallet({ host: "https://wallet.test", chainId: 1, }); - const request = wallet.provider + void wallet.provider .request({ method: "wallet_connect", params: [{ chainId: 1 }], }) - .catch((error: unknown) => error); + .catch(() => { + // Pending; we destroy below. + }); - expect(openSpy).toHaveBeenCalledTimes(1); - expect(openSpy.mock.calls[0]?.[0]).toBe( - "https://wallet.test/popup?origin=http%3A%2F%2Flocalhost%3A3000", - ); + // No popup is opened pre-emptively; the iframe surface renders the + // inline LoginView and only escalates to popup when the user picks an + // OAuth provider that demands a top-level context. + expect(openSpy).not.toHaveBeenCalled(); wallet.destroy(); - await expect(request).resolves.toBeInstanceOf(Error); }); - it("opens eth_requestAccounts in a top-level popup even when iframe mode was requested", async () => { + it("respects an explicit popup mode override for eth_requestAccounts", () => { const openSpy = vi.spyOn(window, "open").mockReturnValue(mockPopupWindow()); const wallet = createWallet({ host: "https://wallet.test", chainId: 1, - dialog: "iframe", + dialog: "popup", }); - const request = wallet.provider + void wallet.provider .request({ method: "eth_requestAccounts" }) - .catch((error: unknown) => error); + .catch(() => { + // Pending; we destroy below. + }); expect(openSpy).toHaveBeenCalledTimes(1); expect(openSpy.mock.calls[0]?.[0]).toBe( @@ -76,6 +79,5 @@ describe("createWallet", () => { ); wallet.destroy(); - await expect(request).resolves.toBeInstanceOf(Error); }); });