diff --git a/README.md b/README.md index 985c08b..394da24 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Please consult [the MetaMask documentation](https://docs.metamask.io/guide/ether ### Node.js ```javascript -import detectEthereumProvider from '@metamask/detect-provider' +import { detectEthereumProvider } from '@metamask/detect-provider' const provider = await detectEthereumProvider() diff --git a/src/detect-ethereum-provider.ts b/src/detect-ethereum-provider.ts new file mode 100644 index 0000000..41d984f --- /dev/null +++ b/src/detect-ethereum-provider.ts @@ -0,0 +1,84 @@ +import { MetaMaskEthereumProvider } from './metamask-ethereum-provider'; +import { WindowWithEthereum } from './window-with-ethereum'; + +/** + * Returns a Promise that resolves to the value of window.ethereum if it is + * set within the given timeout, or null. + * The Promise will not reject, but an error will be thrown if invalid options + * are provided. + * + * @param options - Options bag. + * @param options.mustBeMetaMask - Whether to only look for MetaMask providers. + * Default: false + * @param options.silent - Whether to silence console errors. Does not affect + * thrown errors. Default: false + * @param options.timeout - Milliseconds to wait for 'ethereum#initialized' to + * be dispatched. Default: 3000 + * @returns A Promise that resolves with the Provider if it is detected within + * given timeout, otherwise null. + */ +export function detectEthereumProvider({ + mustBeMetaMask = false, + silent = false, + timeout = 3000, +} = {}): Promise { + + _validateInputs(); + + let handled = false; + + return new Promise((resolve) => { + if ((window as WindowWithEthereum).ethereum) { + + handleEthereum(); + + } else { + + window.addEventListener( + 'ethereum#initialized', + handleEthereum, + { once: true }, + ); + + setTimeout(() => { + handleEthereum(); + }, timeout); + } + + function handleEthereum() { + + if (handled) { + return; + } + handled = true; + + window.removeEventListener('ethereum#initialized', handleEthereum); + + const { ethereum } = window as WindowWithEthereum; + + if (ethereum && (!mustBeMetaMask || ethereum.isMetaMask)) { + resolve(ethereum as unknown as T); + } else { + + const message = mustBeMetaMask && ethereum + ? 'Non-MetaMask window.ethereum detected.' + : 'Unable to detect window.ethereum.'; + + !silent && console.error('@metamask/detect-provider:', message); + resolve(null); + } + } + }); + + function _validateInputs() { + if (typeof mustBeMetaMask !== 'boolean') { + throw new Error(`@metamask/detect-provider: Expected option 'mustBeMetaMask' to be a boolean.`); + } + if (typeof silent !== 'boolean') { + throw new Error(`@metamask/detect-provider: Expected option 'silent' to be a boolean.`); + } + if (typeof timeout !== 'number') { + throw new Error(`@metamask/detect-provider: Expected option 'timeout' to be a number.`); + } + } +} diff --git a/src/index.ts b/src/index.ts index 2141c3e..ee5a204 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,97 +1 @@ -interface MetaMaskEthereumProvider { - isMetaMask?: boolean; - once(eventName: string | symbol, listener: (...args: any[]) => void): this; - on(eventName: string | symbol, listener: (...args: any[]) => void): this; - off(eventName: string | symbol, listener: (...args: any[]) => void): this; - addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; - removeAllListeners(event?: string | symbol): this; -} - -interface Window { - ethereum?: MetaMaskEthereumProvider; -} - -export = detectEthereumProvider; - -/** - * Returns a Promise that resolves to the value of window.ethereum if it is - * set within the given timeout, or null. - * The Promise will not reject, but an error will be thrown if invalid options - * are provided. - * - * @param options - Options bag. - * @param options.mustBeMetaMask - Whether to only look for MetaMask providers. - * Default: false - * @param options.silent - Whether to silence console errors. Does not affect - * thrown errors. Default: false - * @param options.timeout - Milliseconds to wait for 'ethereum#initialized' to - * be dispatched. Default: 3000 - * @returns A Promise that resolves with the Provider if it is detected within - * given timeout, otherwise null. - */ -function detectEthereumProvider({ - mustBeMetaMask = false, - silent = false, - timeout = 3000, -} = {}): Promise { - - _validateInputs(); - - let handled = false; - - return new Promise((resolve) => { - if ((window as Window).ethereum) { - - handleEthereum(); - - } else { - - window.addEventListener( - 'ethereum#initialized', - handleEthereum, - { once: true }, - ); - - setTimeout(() => { - handleEthereum(); - }, timeout); - } - - function handleEthereum() { - - if (handled) { - return; - } - handled = true; - - window.removeEventListener('ethereum#initialized', handleEthereum); - - const { ethereum } = window as Window; - - if (ethereum && (!mustBeMetaMask || ethereum.isMetaMask)) { - resolve(ethereum as unknown as T); - } else { - - const message = mustBeMetaMask && ethereum - ? 'Non-MetaMask window.ethereum detected.' - : 'Unable to detect window.ethereum.'; - - !silent && console.error('@metamask/detect-provider:', message); - resolve(null); - } - } - }); - - function _validateInputs() { - if (typeof mustBeMetaMask !== 'boolean') { - throw new Error(`@metamask/detect-provider: Expected option 'mustBeMetaMask' to be a boolean.`); - } - if (typeof silent !== 'boolean') { - throw new Error(`@metamask/detect-provider: Expected option 'silent' to be a boolean.`); - } - if (typeof timeout !== 'number') { - throw new Error(`@metamask/detect-provider: Expected option 'timeout' to be a number.`); - } - } -} +export * from './detect-ethereum-provider'; diff --git a/src/metamask-ethereum-provider.ts b/src/metamask-ethereum-provider.ts new file mode 100644 index 0000000..f33bdec --- /dev/null +++ b/src/metamask-ethereum-provider.ts @@ -0,0 +1,9 @@ +export interface MetaMaskEthereumProvider { + isMetaMask?: boolean; + once(eventName: string | symbol, listener: (...args: any[]) => void): this; + on(eventName: string | symbol, listener: (...args: any[]) => void): this; + off(eventName: string | symbol, listener: (...args: any[]) => void): this; + addListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this; + removeAllListeners(event?: string | symbol): this; +} diff --git a/src/window-with-ethereum.ts b/src/window-with-ethereum.ts new file mode 100644 index 0000000..1f4f7b8 --- /dev/null +++ b/src/window-with-ethereum.ts @@ -0,0 +1,5 @@ +import { MetaMaskEthereumProvider } from './metamask-ethereum-provider'; + +export interface WindowWithEthereum { + ethereum?: MetaMaskEthereumProvider; +} diff --git a/test/spec.js b/test/spec.js index 7489cf9..d5ed28a 100644 --- a/test/spec.js +++ b/test/spec.js @@ -3,7 +3,7 @@ global.window = global // mock const test = require('tape') const sinon = require('sinon') -const detectProvider = require('../dist') +const { detectEthereumProvider } = require('../dist') // test mocking utility const mockGlobalProps = (ethereum) => { @@ -24,11 +24,11 @@ const providerWithMetaMask = { const providerNoMetaMask = {} const noProvider = null -test('detectProvider: defaults with ethereum already set', async function (t) { +test('detectEthereumProvider: defaults with ethereum already set', async function (t) { mockGlobalProps(providerNoMetaMask) - const provider = await detectProvider() + const provider = await detectEthereumProvider() t.deepEquals({}, provider, 'resolve with expected provider') t.ok(window.addEventListener.notCalled, 'addEventListener should not have been called') @@ -36,11 +36,11 @@ test('detectProvider: defaults with ethereum already set', async function (t) { t.end() }) -test('detectProvider: mustBeMetamask with ethereum already set', async function (t) { +test('detectEthereumProvider: mustBeMetamask with ethereum already set', async function (t) { mockGlobalProps(providerWithMetaMask) - const provider = await detectProvider() + const provider = await detectEthereumProvider() t.ok(provider.isMetaMask, 'should have resolved expected provider object') t.ok(window.addEventListener.notCalled, 'addEventListener should not have been called') @@ -48,23 +48,23 @@ test('detectProvider: mustBeMetamask with ethereum already set', async function t.end() }) -test('detectProvider: mustBeMetamask with non-MetaMask ethereum already set', async function (t) { +test('detectEthereumProvider: mustBeMetamask with non-MetaMask ethereum already set', async function (t) { mockGlobalProps(providerNoMetaMask) - const result = await detectProvider({ timeout: 1, mustBeMetaMask: true }) + const result = await detectEthereumProvider({ timeout: 1, mustBeMetaMask: true }) t.equal(result, null, 'promise should have resolved null') t.ok(window.addEventListener.notCalled, 'addEventListener should not have been called') t.ok(window.removeEventListener.calledOnce, 'removeEventListener called once') t.end() }) -test('detectProvider: ethereum set on ethereum#initialized', async function (t) { +test('detectEthereumProvider: ethereum set on ethereum#initialized', async function (t) { mockGlobalProps(noProvider) const clock = sinon.useFakeTimers() - const detectPromise = detectProvider({ timeout: 1 }) + const detectPromise = detectEthereumProvider({ timeout: 1 }) // set ethereum and call event handler as though event was dispatched window.ethereum = providerWithMetaMask @@ -84,12 +84,12 @@ test('detectProvider: ethereum set on ethereum#initialized', async function (t) t.end() }) -test('detectProvider: ethereum set at end of timeout', async function (t) { +test('detectEthereumProvider: ethereum set at end of timeout', async function (t) { mockGlobalProps(noProvider) const clock = sinon.useFakeTimers() - const detectPromise = detectProvider({ timeout: 1 }) + const detectPromise = detectEthereumProvider({ timeout: 1 }) // set ethereum window.ethereum = providerWithMetaMask @@ -107,11 +107,11 @@ test('detectProvider: ethereum set at end of timeout', async function (t) { t.end() }) -test('detectProvider: ethereum never set', async function (t) { +test('detectEthereumProvider: ethereum never set', async function (t) { mockGlobalProps(noProvider) - const result = await detectProvider({ timeout: 1 }) + const result = await detectEthereumProvider({ timeout: 1 }) t.equal(result, null, 'promise should have resolved null') t.ok(window.addEventListener.calledOnce, 'addEventListener should have been called once') t.ok(window.removeEventListener.calledOnce, 'removeEventListener should have been called once') @@ -119,11 +119,11 @@ test('detectProvider: ethereum never set', async function (t) { t.end() }) -test('detectProvider: ethereum never set (silent mode)', async function (t) { +test('detectEthereumProvider: ethereum never set (silent mode)', async function (t) { mockGlobalProps(noProvider) - const result = await detectProvider({ timeout: 1, silent: true }) + const result = await detectEthereumProvider({ timeout: 1, silent: true }) t.equal(result, null, 'promise should have resolved null') t.ok(window.addEventListener.calledOnce, 'addEventListener should have been called once') t.ok(window.removeEventListener.calledOnce, 'removeEventListener should have been called once')