Skip to content
Merged
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
36 changes: 11 additions & 25 deletions packages/wallet-sdk/src/core/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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";
}

Expand Down Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions packages/wallet-sdk/test/src/Wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,47 +35,49 @@ 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(
"https://wallet.test/popup?origin=http%3A%2F%2Flocalhost%3A3000",
);

wallet.destroy();
await expect(request).resolves.toBeInstanceOf(Error);
});
});
Loading