From eaad1955882fe005258f11f57de418edda8d0b23 Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Wed, 27 Feb 2019 16:55:07 +0200 Subject: [PATCH 1/7] Add: purser-metamask `accountChangeHook` index method --- .../@colony/purser-metamask/index.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/modules/node_modules/@colony/purser-metamask/index.js b/modules/node_modules/@colony/purser-metamask/index.js index 1996452c..ebfa9cea 100644 --- a/modules/node_modules/@colony/purser-metamask/index.js +++ b/modules/node_modules/@colony/purser-metamask/index.js @@ -9,11 +9,15 @@ import { methodCaller, getInpageProvider, detect as detectHelper, + setStateEventObserver, } from './helpers'; import { staticMethods as messages } from './messages'; -import type { MetamaskInpageProviderType } from './flowtypes'; +import type { + MetamaskInpageProviderType, + MetamaskStateEventsObserverType, +} from './flowtypes'; /** * Open the Metamask Wallet instance @@ -103,13 +107,28 @@ export const open = async (): Promise => { */ export const detect = async (): Promise => detectHelper(); +/** + * Hook into Metamask's state events observers array to be able to act on account + * changes from the UI + * + * It's a wrapper around the `setStateEventObserver()` helper method + * + * @method accountChangeHook + * + * @return {Promise} Does not return noting + */ +export const accountChangeHook = + async (callback: MetamaskStateEventsObserverType): Promise => + setStateEventObserver(callback); + /* - * @NOTE There's an argument here to expose the new version + * @NOTE There's an argument to be made here to expose the new version */ const metamaskWallet: Object = { open, detect, + accountChangeHook, }; export default metamaskWallet; From d226137778d0a9664b60432dc66748b00027a6a9 Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Fri, 1 Mar 2019 17:15:59 +0200 Subject: [PATCH 2/7] Fix: `purser-metamask` detect before accessing state --- modules/node_modules/@colony/purser-metamask/index.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/node_modules/@colony/purser-metamask/index.js b/modules/node_modules/@colony/purser-metamask/index.js index ebfa9cea..223b2534 100644 --- a/modules/node_modules/@colony/purser-metamask/index.js +++ b/modules/node_modules/@colony/purser-metamask/index.js @@ -118,8 +118,15 @@ export const detect = async (): Promise => detectHelper(); * @return {Promise} Does not return noting */ export const accountChangeHook = - async (callback: MetamaskStateEventsObserverType): Promise => - setStateEventObserver(callback); + async (callback: MetamaskStateEventsObserverType): Promise => { + /* + * If detect fails, it will throw, with a message explaining the problem + * (Most likely Metamask will be locked, so we won't be able to get to + * the state observer via the in-page provider) + */ + detectHelper(); + return setStateEventObserver(callback); + }; /* * @NOTE There's an argument to be made here to expose the new version From 41901de2c33ee994e764352b66d852f081d6cec0 Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Fri, 1 Mar 2019 17:18:24 +0200 Subject: [PATCH 3/7] Add: `purser-metamask` state observer better comments --- modules/node_modules/@colony/purser-metamask/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/node_modules/@colony/purser-metamask/index.js b/modules/node_modules/@colony/purser-metamask/index.js index 223b2534..ec2d2ee1 100644 --- a/modules/node_modules/@colony/purser-metamask/index.js +++ b/modules/node_modules/@colony/purser-metamask/index.js @@ -115,6 +115,9 @@ export const detect = async (): Promise => detectHelper(); * * @method accountChangeHook * + * @param {Function} callback Function to add the state events update array + * It receives the state object as an only argument + * * @return {Promise} Does not return noting */ export const accountChangeHook = From 1aacafc6daef0190db5fa8182f887d27c4c8052c Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Fri, 1 Mar 2019 17:42:13 +0200 Subject: [PATCH 4/7] Add: purser-metamask `accountChangeHook` unit tests --- .../purser-metamask/accountChangeHook.test.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 modules/tests/purser-metamask/accountChangeHook.test.js diff --git a/modules/tests/purser-metamask/accountChangeHook.test.js b/modules/tests/purser-metamask/accountChangeHook.test.js new file mode 100644 index 00000000..ebe6c51a --- /dev/null +++ b/modules/tests/purser-metamask/accountChangeHook.test.js @@ -0,0 +1,57 @@ +import metamaskWallet from '@colony/purser-metamask'; +import { + detect as detectHelper, + setStateEventObserver, +} from '@colony/purser-metamask/helpers'; + +jest.dontMock('@colony/purser-metamask'); + +/* + * @TODO Fix manual mocks + * This is needed since Jest won't see our manual mocks (because of our custom monorepo structure) + * and will replace them with automatic ones + */ +jest.mock('@colony/purser-metamask/helpers', () => + require('@mocks/purser-metamask/helpers'), +); + +/* + * Mock the global injected inpage provider + */ +global.web3 = { + currentProvider: { + publicConfigStore: { + _events: { + update: [], + }, + }, + }, +}; + +const mockedCallback = jest.fn(state => state); + +describe('Metamask` Wallet Module', () => { + describe('`accountChangeHook()` static method', () => { + test('Calls the correct helper method', async () => { + await metamaskWallet.accountChangeHook(); + /* + * Call the helper method + */ + expect(setStateEventObserver).toHaveBeenCalled(); + }); + test('Detects if Metamask is available', async () => { + await metamaskWallet.accountChangeHook(); + /* + * Calls the `detect()` helper + */ + expect(detectHelper).toHaveBeenCalled(); + }); + test('Adds a callback to the state observer', async () => { + await metamaskWallet.accountChangeHook(mockedCallback); + expect( + /* eslint-disable-next-line no-underscore-dangle */ + global.web3.currentProvider.publicConfigStore._events.update, + ).toContain(mockedCallback); + }); + }); +}); From b4b1a79512fbd14c0a1052ec949fc80ee139a21d Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Mon, 4 Mar 2019 16:30:43 +0200 Subject: [PATCH 5/7] Add: `purser-metamask` catch if we cannot add a hook --- .../node_modules/@colony/purser-metamask/index.js | 12 +++++++++++- .../node_modules/@colony/purser-metamask/messages.js | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/node_modules/@colony/purser-metamask/index.js b/modules/node_modules/@colony/purser-metamask/index.js index ec2d2ee1..7d52fe5c 100644 --- a/modules/node_modules/@colony/purser-metamask/index.js +++ b/modules/node_modules/@colony/purser-metamask/index.js @@ -128,7 +128,17 @@ export const accountChangeHook = * the state observer via the in-page provider) */ detectHelper(); - return setStateEventObserver(callback); + try { + return setStateEventObserver(callback); + } catch (error) { + /* + * If this throws/catches here it means something very weird is going on. + * `detect()` should catch anything that're directly related to Metamask's functionality, + * but if that passes and we have to catch it here, it means some underlying APIs + * might have changed, and this will be very hard to debug + */ + throw new Error(messages.cannotAddHook); + } }; /* diff --git a/modules/node_modules/@colony/purser-metamask/messages.js b/modules/node_modules/@colony/purser-metamask/messages.js index 5d4f8b5c..8564faf0 100644 --- a/modules/node_modules/@colony/purser-metamask/messages.js +++ b/modules/node_modules/@colony/purser-metamask/messages.js @@ -23,6 +23,7 @@ export const staticMethods: Object = { */ legacyMode: "Metamask is running in legacy mode. While this is still going to work, it will be disabled in the future, and it's recommended you upgrade the extension. See this for more details: https://bit.ly/2QQHXvF", + cannotAddHook: "Cannot add an Account Change Hook to the injected Metamask Instance. This should have been caught by the 'detect()' method. Since it didn't it means some API's might have changed." }; export const helpers: Object = { From 7cd4e8caf67308411c404bb22adcacfe9047540e Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Mon, 4 Mar 2019 16:36:45 +0200 Subject: [PATCH 6/7] Add: `purser-metamask` test account hook catching --- modules/tests/purser-metamask/accountChangeHook.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/tests/purser-metamask/accountChangeHook.test.js b/modules/tests/purser-metamask/accountChangeHook.test.js index ebe6c51a..c3f6cc6a 100644 --- a/modules/tests/purser-metamask/accountChangeHook.test.js +++ b/modules/tests/purser-metamask/accountChangeHook.test.js @@ -53,5 +53,13 @@ describe('Metamask` Wallet Module', () => { global.web3.currentProvider.publicConfigStore._events.update, ).toContain(mockedCallback); }); + test('Catches if something goes wrong', async () => { + /* + * We're re-mocking the helpers just for this test so we can simulate + * an error along the way + */ + setStateEventObserver.mockRejectedValueOnce(new Error()); + expect(metamaskWallet.accountChangeHook()).rejects.toThrow(); + }); }); }); From c45b34b36c0f2d6d3fce7fd6418486e2ffd74466 Mon Sep 17 00:00:00 2001 From: Raul Glogovetan Date: Mon, 4 Mar 2019 16:51:01 +0200 Subject: [PATCH 7/7] Add: purser-metamask `accountChangeHook` docs --- docs/_Modules_purser-metamask.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/_Modules_purser-metamask.md b/docs/_Modules_purser-metamask.md index b6482dbb..9f9dae3e 100644 --- a/docs/_Modules_purser-metamask.md +++ b/docs/_Modules_purser-metamask.md @@ -91,9 +91,36 @@ This method returns a `Promise` which, after resolving, it will `return` only re **Usage examples:** -Open the metamask wallet: +Detect if Metamask is available: ```js import { detect as isMetamaskAvailable } from '@colony/purser-metamask'; await isMetamaskAvailable(); // true ``` + +### `accountChangeHook` + +```js +await accountChangeHook(callback: Function); +``` + +This is a utility method to allow end users to hook into Metamask's State Event Observer, and execute a callback when that changes. _(Eg: When an account is changed in the Metamask UI)_ + +This method takes a callback as an argument, which will be added to the state events array. When the state changes, all the callbacks added to that array are called in order. + +When this is is called, it will receive a `state` Object as an only argument, Object which contains the new updated state. + +This utility method is useful to act on account changes from within a dApp. _(Eg: To logout a user)_ + +**Usage examples:** + +Hook into the state change events with a simple callback: +```js +import { accountChangeHook } from '@colony/purser-metamask'; + +const walletChangedCallback = ({ selectedAddress }) => { + console.log(`You changed your wallet. The new address is: ${selectedAddress}`); +}; + +await accountChangeHook(walletChangedCallback); +```