Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modernize exports #100

Merged
merged 2 commits into from
Feb 13, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
84 changes: 84 additions & 0 deletions src/detect-ethereum-provider.ts
Original file line number Diff line number Diff line change
@@ -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<T = MetaMaskEthereumProvider>({
mustBeMetaMask = false,
silent = false,
timeout = 3000,
} = {}): Promise<T | null> {

_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.`);
}
}
}
98 changes: 1 addition & 97 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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<T = MetaMaskEthereumProvider>({
mustBeMetaMask = false,
silent = false,
timeout = 3000,
} = {}): Promise<T | null> {

_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';
legobeat marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions src/metamask-ethereum-provider.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions src/window-with-ethereum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { MetaMaskEthereumProvider } from './metamask-ethereum-provider';

export interface WindowWithEthereum {
ethereum?: MetaMaskEthereumProvider;
}
30 changes: 15 additions & 15 deletions test/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -24,47 +24,47 @@ 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')
t.ok(window.removeEventListener.calledOnce, 'removeEventListener called once')
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')
t.ok(window.removeEventListener.calledOnce, 'removeEventListener called once')
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
Expand All @@ -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
Expand All @@ -107,23 +107,23 @@ 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')
t.ok(console.error.calledOnce, 'console.error should have been called once')
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')
Expand Down
Loading