From 20aefbf7a050b522944ef1c5f18e636d0d74f3d2 Mon Sep 17 00:00:00 2001 From: oluwadareab12 Date: Thu, 23 Jul 2026 16:22:20 +0100 Subject: [PATCH] fix(Provider): guard EIP-6963 announcement on DOM event APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provider.create() guarded the EIP-6963 announcement on window, CustomEvent and crypto.randomUUID, but mipd's announceProvider unconditionally calls window.dispatchEvent and window.addEventListener. In partial window-like runtimes (e.g. React Native, where window is aliased to globalThis and CustomEvent/crypto.randomUUID are polyfilled but no DOM event APIs exist) every check passed, announceProvider ran, and dispatchEvent threw "TypeError: undefined is not a function" — crashing Provider.create() entirely for any adapter, not just the announcement. Require window.dispatchEvent and window.addEventListener to be functions before announcing, so non-DOM runtimes skip the browser-only EIP-6963 announcement instead of crashing. Browser behavior is unchanged. Closes #730 --- .../announce-provider-dom-event-guard.md | 5 ++ src/core/Provider.test.ts | 61 +++++++++++++++++++ src/core/Provider.ts | 2 + 3 files changed, 68 insertions(+) create mode 100644 .changeset/announce-provider-dom-event-guard.md diff --git a/.changeset/announce-provider-dom-event-guard.md b/.changeset/announce-provider-dom-event-guard.md new file mode 100644 index 00000000..36991490 --- /dev/null +++ b/.changeset/announce-provider-dom-event-guard.md @@ -0,0 +1,5 @@ +--- +'accounts': patch +--- + +Skipped EIP-6963 provider announcement in partial `window`-like runtimes (e.g. React Native) that lack `window.dispatchEvent`/`window.addEventListener`, preventing `Provider.create` from throwing at startup. diff --git a/src/core/Provider.test.ts b/src/core/Provider.test.ts index c2919444..a4c9c16f 100644 --- a/src/core/Provider.test.ts +++ b/src/core/Provider.test.ts @@ -620,6 +620,67 @@ describe('wallet_connect', () => { }) }) +describe('EIP-6963 announcement', () => { + afterEach(() => vi.unstubAllGlobals()) + + // An explicit adapter sidesteps `Provider.create`'s default `dialog` adapter, which reads + // `window.location`/`window.isSecureContext` for unrelated reasons — this suite only exercises + // the EIP-6963 announcement guard. `rdns` must be unique per call: Provider.ts dedupes + // announcements through a module-level `announced` Set, so a fixed rdns would let one call (or + // vitest's automatic retry of a previous attempt) mask a later call's guard behavior. + function testAdapter(rdns: string) { + return Adapter.define({ name: 'Test Wallet', rdns }, () => ({ + actions: { + async createAccount() { + return { accounts: [{ address }] } + }, + async loadAccounts() { + return { accounts: [{ address }] } + }, + }, + })) + } + + function uniqueRdns() { + return `com.example.test.${Math.random().toString(36).slice(2)}` + } + + test('behavior: skips announcement in a partial window-like runtime (e.g. React Native)', () => { + // `window` aliased to an object without DOM event APIs, but with `CustomEvent`/ + // `crypto.randomUUID` polyfilled for unrelated reasons — mirrors the reported React Native + // (Expo) environment. + vi.stubGlobal('window', {}) + vi.stubGlobal('CustomEvent', class {}) + vi.stubGlobal('crypto', { randomUUID: () => 'test-uuid' }) + + expect(() => + Provider.create({ adapter: testAdapter(uniqueRdns()), storage: Storage.memory() }), + ).not.toThrow() + }) + + test('behavior: announces via mipd in a real browser-like window', () => { + const addEventListener = vi.fn() + const dispatchEvent = vi.fn() + vi.stubGlobal('window', { addEventListener, dispatchEvent }) + vi.stubGlobal( + 'CustomEvent', + class { + detail: unknown + type: string + constructor(type: string, options?: { detail?: unknown }) { + this.type = type + this.detail = options?.detail + } + }, + ) + vi.stubGlobal('crypto', { randomUUID: () => 'test-uuid' }) + + Provider.create({ adapter: testAdapter(uniqueRdns()), storage: Storage.memory() }) + + expect(dispatchEvent).toHaveBeenCalled() + }) +}) + describe('adapter actions', () => { test('behavior: explicit adapter action overrides provider default', async () => { const getAccount = vi.fn(() => { diff --git a/src/core/Provider.ts b/src/core/Provider.ts index 6d88a551..a877f520 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -1821,6 +1821,8 @@ export function create(options: create.Options = {}): create.ReturnType { if ( typeof window !== 'undefined' && + typeof window.dispatchEvent === 'function' && + typeof window.addEventListener === 'function' && typeof CustomEvent !== 'undefined' && typeof crypto.randomUUID === 'function' ) {