diff --git a/.releaserc.yml b/.releaserc.yml index 518f8dd..dad3bd9 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -1,3 +1,3 @@ { - "branches": ["main"] + "branches": ["main", "next"] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 04660c8..c354016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^3.10.1", - "eth-testing": "^1.0.0-alpha.1", + "eth-testing": "^1.1.0-alpha.4", "husky": "^4.3.7", "jest": "^26.6.3", "lint-staged": "^10.5.3", @@ -5179,9 +5179,9 @@ } }, "node_modules/eth-testing": { - "version": "1.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/eth-testing/-/eth-testing-1.0.0-alpha.1.tgz", - "integrity": "sha512-vUTQzyzWxAUn3R0RlMipC3QHdD9WYXVKFFpX9YG2frEl7nIgEGh1lnNqrTALB0ANXWA9OIzrXEjVBkGRBpXKhg==", + "version": "1.1.0-alpha.4", + "resolved": "https://registry.npmjs.org/eth-testing/-/eth-testing-1.1.0-alpha.4.tgz", + "integrity": "sha512-rOIVsizQb+T7RPn85IkCR60U+6sJGyixw14WBYZKeRFVtmV9s9hQDRgV0KnKFp0H529InKDGtlkPoL5bFui+WA==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.4.0", @@ -20841,9 +20841,9 @@ "dev": true }, "eth-testing": { - "version": "1.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/eth-testing/-/eth-testing-1.0.0-alpha.1.tgz", - "integrity": "sha512-vUTQzyzWxAUn3R0RlMipC3QHdD9WYXVKFFpX9YG2frEl7nIgEGh1lnNqrTALB0ANXWA9OIzrXEjVBkGRBpXKhg==", + "version": "1.1.0-alpha.4", + "resolved": "https://registry.npmjs.org/eth-testing/-/eth-testing-1.1.0-alpha.4.tgz", + "integrity": "sha512-rOIVsizQb+T7RPn85IkCR60U+6sJGyixw14WBYZKeRFVtmV9s9hQDRgV0KnKFp0H529InKDGtlkPoL5bFui+WA==", "dev": true, "requires": { "@ethersproject/abi": "^5.4.0", diff --git a/package.json b/package.json index 44c8206..8a55640 100644 --- a/package.json +++ b/package.json @@ -1,3 +1,4 @@ + { "name": "metamask-react", "version": "0.0.1-semantic-release", @@ -57,7 +58,7 @@ "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-testing-library": "^3.10.1", - "eth-testing": "^1.0.0-alpha.1", + "eth-testing": "^1.1.0-alpha.4", "husky": "^4.3.7", "jest": "^26.6.3", "lint-staged": "^10.5.3", diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index b11a2ca..ce04c35 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -111,6 +111,40 @@ describe("MetaMask provider", () => { expect(result.current.account).toEqual(address); }); + test("calling `connect` method while a pending Metamask request is pending should end in a successful connection", async () => { + const error = { + code: -32002, + }; + testingUtils.lowLevel.mockRequest("eth_requestAccounts", error, { + shouldThrow: true, + }); + + const { result, waitForNextUpdate } = renderHook(useMetaMask, { + wrapper: MetaMaskProvider, + }); + + expect(result.current.status).toEqual("initializing"); + + await waitForNextUpdate(); + + expect(result.current.status).toEqual("notConnected"); + + act(() => { + result.current.connect(); + }); + + expect(result.current.status).toEqual("connecting"); + + act(() => { + testingUtils.mockAccounts([address]); + }); + + await waitForNextUpdate(); + + expect(result.current.status).toEqual("connected"); + expect(result.current.account).toEqual(address); + }); + test("calling `connect` method should end in the `notConnected` status if the request fails", async () => { const error = new Error("Test Error"); testingUtils.lowLevel.mockRequest("eth_requestAccounts", error, { diff --git a/src/metamask-provider.tsx b/src/metamask-provider.tsx index 0060c5a..fb7ba24 100644 --- a/src/metamask-provider.tsx +++ b/src/metamask-provider.tsx @@ -7,6 +7,13 @@ import { import { Action, reducer } from "./reducer"; import { useSafeDispatch } from "./utils/useSafeDispatch"; +type ErrorWithCode = { + code: number; + [key: string]: any; +}; +// MetaMask - RPC Error: Request of type 'wallet_requestPermissions' already pending for origin [origin]. Please wait. +const ERROR_CODE_REQUEST_PENDING = -32002; + type WindowInstanceWithEthereum = Window & typeof globalThis & { ethereum?: any }; @@ -63,22 +70,50 @@ function subscribeToChainChanged(dispatch: (action: Action) => void) { }; } -async function requestAccounts( +function requestAccounts( dispatch: (action: Action) => void ): Promise { const ethereum = (window as WindowInstanceWithEthereum).ethereum; dispatch({ type: "metaMaskConnecting" }); - try { - const accounts: string[] = await ethereum.request({ - method: "eth_requestAccounts", - }); - dispatch({ type: "metaMaskConnected", payload: { accounts } }); - return accounts; - } catch (err) { - dispatch({ type: "metaMaskPermissionRejected" }); - throw err; - } + + /** + * Note about the pattern + * Instead of only relying on the RPC Request response, the resolve of the promise may happen based from a polling + * using the eth_accounts rpc endpoint. + * The reason for this change is in order to handle pending connection request on MetaMask side. + * See https://github.com/VGLoic/metamask-react/issues/13 for the full discussion. + * Any improvements on MetaMask side on this behaviour that could allow to go back to the previous, simple and safer, pattern + * should trigger the update of this code. + */ + + return new Promise((resolve, reject) => { + const intervalId = setInterval(async () => { + const accounts = await ethereum.request({ + method: "eth_accounts", + }); + if (accounts.length > 0) { + clearInterval(intervalId); + dispatch({ type: "metaMaskConnected", payload: { accounts } }); + resolve(accounts); + } + }, 500); + ethereum + .request({ + method: "eth_requestAccounts", + }) + .then((accounts: string[]) => { + clearInterval(intervalId); + dispatch({ type: "metaMaskConnected", payload: { accounts } }); + resolve(accounts); + }) + .catch((err: unknown) => { + if ((err as ErrorWithCode)?.code === ERROR_CODE_REQUEST_PENDING) return; + dispatch({ type: "metaMaskPermissionRejected" }); + clearInterval(intervalId); + reject(err); + }); + }); } const initialState: MetaMaskState = {