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); +``` diff --git a/modules/node_modules/@colony/purser-metamask/index.js b/modules/node_modules/@colony/purser-metamask/index.js index 1996452c..7d52fe5c 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,48 @@ 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 + * + * @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 = + 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(); + 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); + } + }; + /* - * @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; 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 = { diff --git a/modules/tests/purser-metamask/accountChangeHook.test.js b/modules/tests/purser-metamask/accountChangeHook.test.js new file mode 100644 index 00000000..c3f6cc6a --- /dev/null +++ b/modules/tests/purser-metamask/accountChangeHook.test.js @@ -0,0 +1,65 @@ +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); + }); + 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(); + }); + }); +});