diff --git a/.eslintrc b/.eslintrc index 106ef80950..80f5fc117e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -100,7 +100,7 @@ "newIsCapExceptions": ["mnemonic"] } ], - + "no-constant-condition": ["error", { "checkLoops": false }], "react/prop-types": "off", "no-plusplus": "off", "no-underscore-dangle": "off", diff --git a/libs/hwServer/index.js b/libs/hwServer/index.js index 0ad78dcb8b..494e9abed5 100644 --- a/libs/hwServer/index.js +++ b/libs/hwServer/index.js @@ -66,7 +66,7 @@ export class HwServer { async selectDevice({ id }) { this.currentDeviceId = id; - this.deviceUpdate(); + this.deviceUpdate(id); return this.currentDeviceId; } @@ -111,7 +111,10 @@ export class HwServer { const { sender } = this.pubSub; this.devices.push(device); this.syncDevices(); - publish(sender, { event: IPC_MESSAGES.HW_CONNECTED, payload: { model: device.model } }); + publish(sender, { + event: IPC_MESSAGES.HW_CONNECTED, + payload: { model: device.model, devices: this.devices }, + }); } /** diff --git a/setup/react/app/index.js b/setup/react/app/index.js index ab0a14d4e8..d6772b3f33 100644 --- a/setup/react/app/index.js +++ b/setup/react/app/index.js @@ -29,6 +29,7 @@ import { useCurrentApplication, } from 'src/modules/blockchainApplication/manage/hooks'; import './variables.css'; +import useHwListener from "src/modules/hardwareWallet/hooks/useHwListener"; import styles from './app.css'; if (MOCK_SERVICE_WORKER) { @@ -45,7 +46,7 @@ const App = ({ history }) => { const { data: chainMetaData, isLoading } = useBlockchainApplicationMeta(); const { setApplication } = useApplicationManagement(); const [, setCurrentApplication] = useCurrentApplication(); - + useHwListener(); useIpc(history); useEffect(() => { diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 0d6148386e..ce4869838c 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -113,6 +113,7 @@ "Claim reward": "Claim reward", "Claim rewards": "Claim rewards", "Clear all filters": "Clear all filters", + "Click here to select a hardware wallet": "Click here to select a hardware wallet", "Click to hide full and old addresses": "Click to hide full and old addresses", "Click to hide the full address": "Click to hide the full address", "Click to see full and old addresses": "Click to see full and old addresses", diff --git a/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.js b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.js new file mode 100644 index 0000000000..9749dd4af8 --- /dev/null +++ b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Dialog from '@theme/dialog/dialog'; +import BoxHeader from 'src/theme/box/header'; +import Box from '@theme/box'; + +function SelectHardwareDeviceModal() { + return ( + + + +

Switch account

+
+
+
+ ); +} + +export default SelectHardwareDeviceModal; diff --git a/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.test.js b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.test.js new file mode 100644 index 0000000000..c2e42124c2 --- /dev/null +++ b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal.test.js @@ -0,0 +1,10 @@ +import { screen } from '@testing-library/react'; +import {renderWithRouter} from "src/utils/testHelpers"; +import SelectHardwareDeviceModal from './SelectHardwareDeviceModal'; + +describe('SelectHardwareDeviceModal', () => { + it('Should show Switch account in the title', () => { + renderWithRouter(SelectHardwareDeviceModal); + expect(screen.getByText('Switch account')).toBeTruthy(); + }); +}); diff --git a/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/index.js b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/index.js new file mode 100644 index 0000000000..166bcdba6f --- /dev/null +++ b/src/modules/hardwareWallet/components/SelectHardwareDeviceModal/index.js @@ -0,0 +1,4 @@ +import SelectHardwareDeviceModal + from "src/modules/hardwareWallet/components/SelectHardwareDeviceModal/SelectHardwareDeviceModal"; + +export default SelectHardwareDeviceModal; diff --git a/src/modules/hardwareWallet/hooks/useHWAccounts.js b/src/modules/hardwareWallet/hooks/useHWAccounts.js index bcb9a58d25..094d17d700 100644 --- a/src/modules/hardwareWallet/hooks/useHWAccounts.js +++ b/src/modules/hardwareWallet/hooks/useHWAccounts.js @@ -1,5 +1,5 @@ import { useSelector } from 'react-redux'; -import { selectHWAccounts } from 'src/redux/selectors'; +import { selectHWAccounts } from '@hardwareWallet/store/selectors/hwSelectors'; const useHWAccounts = () => { const hwAccounts = useSelector(selectHWAccounts); diff --git a/src/modules/hardwareWallet/hooks/useHwListener.js b/src/modules/hardwareWallet/hooks/useHwListener.js new file mode 100644 index 0000000000..fd2ac6215f --- /dev/null +++ b/src/modules/hardwareWallet/hooks/useHwListener.js @@ -0,0 +1,28 @@ +import { useDispatch } from 'react-redux'; +import { useEffect } from 'react'; +import { IPC_MESSAGES } from '@libs/hwServer/constants'; +import { + setHardwareWalletDevices, + setCurrentDevice, +} from 'src/modules/hardwareWallet/store/actions'; + +const { DEVICE_LIST_CHANGED, DEVICE_UPDATE } = IPC_MESSAGES; + +function useHwListener() { + const dispatch = useDispatch(); + + const { ipc } = window; + + if (!ipc) return; + + useEffect(() => { + ipc.on(DEVICE_LIST_CHANGED, (action, data) => { + dispatch(setHardwareWalletDevices(data)); + }); + ipc.on(DEVICE_UPDATE, (action, data) => { + dispatch(setCurrentDevice(data)); + }); + }, []); +} + +export default useHwListener; diff --git a/src/modules/hardwareWallet/hooks/useHwListener.test.js b/src/modules/hardwareWallet/hooks/useHwListener.test.js new file mode 100644 index 0000000000..246e4e0c75 --- /dev/null +++ b/src/modules/hardwareWallet/hooks/useHwListener.test.js @@ -0,0 +1,58 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useDispatch } from 'react-redux'; +import { IPC_MESSAGES } from '@libs/hwServer/constants'; +import {setHardwareWalletDevices, setCurrentDevice} from "@hardwareWallet/store/actions"; +import useHwListener from './useHwListener'; + +const { DEVICE_LIST_CHANGED, DEVICE_UPDATE } = IPC_MESSAGES; + +const mockDispatch = jest.fn(); +useDispatch.mockReturnValue(mockDispatch); + +describe('useIpc', () => { + const callbacks = {}; + const ipc = { + on: jest.fn((event, callback) => { + callbacks[event] = callback; + }), + }; + + beforeEach(() => { + mockDispatch.mockClear(); + window.ipc = ipc; + }); + + afterEach(() => { + delete window.ipc; + }); + + it('Should return undefined if no ipc on window', () => { + const { result, rerender } = renderHook(() => useHwListener()); + act(() => { + delete window.ipc; + }); + rerender(); + + expect(result.current).toBe(undefined); + }); + + it('Should dispatch setDeviceUpdated when ipc receives DEVICE_LIST_CHANGED', () => { + expect(ipc.on).toHaveBeenCalled(); + const devices = [{ deviceId: '1' }]; + + callbacks[DEVICE_LIST_CHANGED]({}, devices); + expect(mockDispatch).toHaveBeenCalledWith( + setHardwareWalletDevices(devices) + ); + }); + + it('Should dispatch setDeviceUpdated when ipc receives DEVICE_UPDATE', () => { + expect(ipc.on).toHaveBeenCalled(); + const device = {deviceId: '1'}; + + callbacks[DEVICE_UPDATE]({}, device); + expect(mockDispatch).toHaveBeenCalledWith( + setCurrentDevice(device) + ); + }); +}); diff --git a/src/modules/hardwareWallet/hooks/useManageHWAccounts.js b/src/modules/hardwareWallet/hooks/useManageHWAccounts.js index 988ed166dd..4aec4d390c 100644 --- a/src/modules/hardwareWallet/hooks/useManageHWAccounts.js +++ b/src/modules/hardwareWallet/hooks/useManageHWAccounts.js @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { selectSettings, selectHW } from 'src/redux/selectors'; -import { storeHWAccounts, removeHWAccounts } from '@hardwareWallet/store/actions/actions'; +import { selectSettings } from 'src/redux/selectors'; +import { setHWAccounts, removeHWAccounts } from '@hardwareWallet/store/actions'; +import { selectActiveHardwareDevice } from '@hardwareWallet/store/selectors/hwSelectors'; import { getNameFromAccount } from '../utils/getNameFromAccount'; import { getHWAccounts } from '../utils/getHWAccounts'; @@ -9,7 +10,7 @@ const useManageHWAccounts = () => { const dispatch = useDispatch(); const settings = useSelector(selectSettings); const [prevDeviceId, setCurrentDeviceId] = useState(); - const { deviceId, status } = useSelector(selectHW); + const { deviceId, status } = useSelector(selectActiveHardwareDevice); const addAccounts = () => { if (prevDeviceId === deviceId || status !== 'connected') { @@ -18,7 +19,7 @@ const useManageHWAccounts = () => { setCurrentDeviceId(deviceId); getHWAccounts(getNameFromAccount, settings, deviceId) .then((accList) => { - dispatch(storeHWAccounts(accList)); + dispatch(setHWAccounts(accList)); }) .catch((err) => console.log({ err })); }; diff --git a/src/modules/hardwareWallet/hooks/useManageHWAccounts.test.js b/src/modules/hardwareWallet/hooks/useManageHWAccounts.test.js index 462148fa20..0c5bdfa21e 100644 --- a/src/modules/hardwareWallet/hooks/useManageHWAccounts.test.js +++ b/src/modules/hardwareWallet/hooks/useManageHWAccounts.test.js @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { useSelector } from 'react-redux'; -import { storeHWAccounts, removeHWAccounts } from '../store/actions/actions'; +import { setHWAccounts, removeHWAccounts } from '../store/actions'; import { hwAccounts } from '../__fixtures__/hwAccounts'; import { getHWAccounts } from '../utils/getHWAccounts'; import useManageHWAccounts from './useManageHWAccounts'; @@ -10,8 +10,10 @@ jest.useRealTimers(); const mockDispatch = jest.fn(); const mockAppState = { hardwareWallet: { - deviceId: 20231, - status: 'connected', + currentDevice: { + deviceId: 20231, + status: 'connected', + }, }, settings: { hardwareAccounts: {}, @@ -38,7 +40,7 @@ describe('useManageHWAccounts hook', () => { await waitFor(() => { expect(mockDispatch).toHaveBeenCalledTimes(1); - const hwWalletAccountsDetails = storeHWAccounts(hwAccounts); + const hwWalletAccountsDetails = setHWAccounts(hwAccounts); expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining(hwWalletAccountsDetails)); }); }); @@ -46,8 +48,10 @@ describe('useManageHWAccounts hook', () => { it('removes the list of accounts when the device is disconnected', async () => { const updatedMockAppState = { hardwareWallet: { - deviceId: 20231, - status: 'disconnected', + currentDevice: { + deviceId: 20231, + status: 'disconnected', + }, accounts: hwAccounts, }, settings: { diff --git a/src/modules/hardwareWallet/store/actions/actions.js b/src/modules/hardwareWallet/store/actions/accountsActions.js similarity index 62% rename from src/modules/hardwareWallet/store/actions/actions.js rename to src/modules/hardwareWallet/store/actions/accountsActions.js index ddabf8d7db..b14ae7990e 100644 --- a/src/modules/hardwareWallet/store/actions/actions.js +++ b/src/modules/hardwareWallet/store/actions/accountsActions.js @@ -1,7 +1,7 @@ import actionTypes from './actionTypes'; -export const storeHWAccounts = (accounts) => ({ - type: actionTypes.storeHWAccounts, +export const setHWAccounts = (accounts) => ({ + type: actionTypes.setHWAccounts, accounts, }); diff --git a/src/modules/hardwareWallet/store/actions/actions.test.js b/src/modules/hardwareWallet/store/actions/accountsActions.test.js similarity index 62% rename from src/modules/hardwareWallet/store/actions/actions.test.js rename to src/modules/hardwareWallet/store/actions/accountsActions.test.js index 258f3303c3..edd5cc4077 100644 --- a/src/modules/hardwareWallet/store/actions/actions.test.js +++ b/src/modules/hardwareWallet/store/actions/accountsActions.test.js @@ -1,14 +1,14 @@ +import { hwAccounts } from '@hardwareWallet/__fixtures__/hwAccounts'; +import { setHWAccounts, removeHWAccounts } from './accountsActions'; import actionTypes from './actionTypes'; -import { storeHWAccounts, removeHWAccounts } from './actions'; -import { hwAccounts } from '../../__fixtures__/hwAccounts'; describe('actions: hardware wallet', () => { it('stores the list of accounts', () => { const expectedAction = { - type: actionTypes.storeHWAccounts, + type: actionTypes.setHWAccounts, accounts: hwAccounts, }; - expect(storeHWAccounts(hwAccounts)).toEqual(expectedAction); + expect(setHWAccounts(hwAccounts)).toEqual(expectedAction); }); it('removes the list of accounts', () => { const expectedAction = { diff --git a/src/modules/hardwareWallet/store/actions/actionTypes.js b/src/modules/hardwareWallet/store/actions/actionTypes.js index 64009faa0a..6332ee07d7 100644 --- a/src/modules/hardwareWallet/store/actions/actionTypes.js +++ b/src/modules/hardwareWallet/store/actions/actionTypes.js @@ -1,6 +1,11 @@ +import { IPC_MESSAGES } from '@libs/hwServer/constants'; + +const {DEVICE_LIST_CHANGED, DEVICE_UPDATE} = IPC_MESSAGES const actionTypes = { - storeHWAccounts: 'STORE_HW_ACCOUNTS', - removeHWAccounts: 'REMOVE_HW_ACCOUNTS', + setHWAccounts: 'HW_ACCOUNTS_ADD', + removeHWAccounts: 'HW_ACCOUNTS_REMOVE', + setDevices: `HW_${DEVICE_LIST_CHANGED}`, + setCurrentDevice: `HW_${DEVICE_UPDATE}`, }; export default actionTypes; diff --git a/src/modules/hardwareWallet/store/actions/devicesActions.js b/src/modules/hardwareWallet/store/actions/devicesActions.js new file mode 100644 index 0000000000..1bb6aa15cb --- /dev/null +++ b/src/modules/hardwareWallet/store/actions/devicesActions.js @@ -0,0 +1,11 @@ +import actionTypes from '@hardwareWallet/store/actions/actionTypes'; + +export const setHardwareWalletDevices = (devices) => ({ + type: actionTypes.setDevices, + devices, +}); + +export const setCurrentDevice = ({ device }) => ({ + type: actionTypes.setCurrentDevice, + device, +}); diff --git a/src/modules/hardwareWallet/store/actions/devicesActions.test.js b/src/modules/hardwareWallet/store/actions/devicesActions.test.js new file mode 100644 index 0000000000..fffbd898ed --- /dev/null +++ b/src/modules/hardwareWallet/store/actions/devicesActions.test.js @@ -0,0 +1,28 @@ +import actionTypes from './actionTypes'; +import { setHardwareWalletDevices, setCurrentDevice } from './devicesActions'; + +describe('hardwareWalletActions', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should create an action to update devices', () => { + const devices = [{ deviceId: '1' }]; + const expectedAction = { + type: actionTypes.setDevices, + devices, + }; + + expect(setHardwareWalletDevices(devices)).toEqual(expectedAction); + }); + + it('should create an action to update device', () => { + const device = { deviceId: '1' }; + const expectedAction = { + type: actionTypes.setCurrentDevice, + device, + }; + + expect(setCurrentDevice({ device })).toEqual(expectedAction); + }); +}); diff --git a/src/modules/hardwareWallet/store/actions/index.js b/src/modules/hardwareWallet/store/actions/index.js new file mode 100644 index 0000000000..ebc04e0378 --- /dev/null +++ b/src/modules/hardwareWallet/store/actions/index.js @@ -0,0 +1,2 @@ +export * from './accountsActions' +export * from './devicesActions' diff --git a/src/modules/hardwareWallet/store/reducers/accountsReducers.js b/src/modules/hardwareWallet/store/reducers/accountsReducers.js new file mode 100644 index 0000000000..c705707b58 --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/accountsReducers.js @@ -0,0 +1,14 @@ +import actionTypes from '../actions/actionTypes'; + +const initAccounts = []; + +export const accounts = (state = initAccounts, {type, accounts: hwAccount= initAccounts}) => { + switch (type) { + case actionTypes.setHWAccounts: + return hwAccount; + case actionTypes.removeHWAccounts: + return initAccounts; + default: + return state; + } +}; diff --git a/src/modules/hardwareWallet/store/reducers/reducers.test.js b/src/modules/hardwareWallet/store/reducers/accountsReducers.test.js similarity index 65% rename from src/modules/hardwareWallet/store/reducers/reducers.test.js rename to src/modules/hardwareWallet/store/reducers/accountsReducers.test.js index c80aec6b60..602847257f 100644 --- a/src/modules/hardwareWallet/store/reducers/reducers.test.js +++ b/src/modules/hardwareWallet/store/reducers/accountsReducers.test.js @@ -1,9 +1,9 @@ import { hwAccounts } from '../../__fixtures__/hwAccounts'; import actionTypes from '../actions/actionTypes'; -import hardwareWallet from './reducers'; +import { accounts } from './accountsReducers'; -describe('reducer: hardware wallet', () => { - const testHWWallet = { +describe('reducer: hardware wallet accounts', () => { + const testHWWalletAccount = { hw: { deviceId: '20231', model: 'Nano S', @@ -20,15 +20,15 @@ describe('reducer: hardware wallet', () => { }, version: 1, }; - const state = { deviceId: 0, status: 'disconnected', accounts: [] }; + const state = []; it('stores the list of accounts', () => { const action = { - type: actionTypes.storeHWAccounts, - accounts: [...hwAccounts, testHWWallet], + type: actionTypes.setHWAccounts, + accounts: [...hwAccounts, testHWWalletAccount], }; - const expectedData = { ...state, accounts: [...state.accounts, ...action.accounts] }; - const updatedState = hardwareWallet(state, action); + const expectedData = [...state, ...action.accounts]; + const updatedState = accounts(state, action); expect(updatedState).toEqual(expectedData); }); @@ -36,7 +36,7 @@ describe('reducer: hardware wallet', () => { const action = { type: actionTypes.removeHWAccounts, }; - const updatedState = hardwareWallet(state, action); + const updatedState = accounts(state, action); expect(updatedState).toEqual(state); }); @@ -44,7 +44,7 @@ describe('reducer: hardware wallet', () => { const action = { type: 'INVALID_ACTION', }; - const updatedState = hardwareWallet(state, action); + const updatedState = accounts(state, action); expect(updatedState).toEqual(state); }); }); diff --git a/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.js b/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.js new file mode 100644 index 0000000000..7505bb40b7 --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.js @@ -0,0 +1,25 @@ +import actionTypes from '@hardwareWallet/store/actions/actionTypes'; + +export const initialState = { + deviceId: '', + model: '', + brand: '', + status: 'disconnected', +}; + +/** + * + * @param {Object} state + * @param {Object} action + */ +/* istanbul ignore next */ +export const currentDevice = (state = initialState, action) => { + const { type, device } = action; + switch (type) { + case actionTypes.setCurrentDevice: { + return device; + } + default: + return state; + } +}; diff --git a/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.test.js b/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.test.js new file mode 100644 index 0000000000..ebe0f221f7 --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/currentDeviceReducer.test.js @@ -0,0 +1,26 @@ +import actionTypes from '../actions/actionTypes'; +import { currentDevice, initialState } from './currentDeviceReducer'; + +describe('HardwareWallet current device reducer', () => { + + it('updates current device', () => { + const action = { + type: actionTypes.setCurrentDevice, + device: { + deviceId: '20231', + model: 'Nano S', + brand: 'Ledger', + } + }; + const updatedState = currentDevice(initialState, action); + expect(updatedState).toEqual(action.device); + }); + + it('returns default state', () => { + const action = { + type: 'OTHER_ACTION_TYPE', + }; + const updatedState = currentDevice(initialState, action); + expect(updatedState).toEqual(initialState); + }); +}); diff --git a/src/modules/hardwareWallet/store/reducers/devicesReducer.js b/src/modules/hardwareWallet/store/reducers/devicesReducer.js new file mode 100644 index 0000000000..11c215612b --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/devicesReducer.js @@ -0,0 +1,19 @@ +import actionTypes from '@hardwareWallet/store/actions/actionTypes'; + +export const initialState = []; + +/** + * + * @param {Object} state + * @param {Object} action + */ +export const devices = (state = initialState, action) => { + const { type, devices: newDevices } = action; + switch (type) { + case actionTypes.setDevices: { + return newDevices; + } + default: + return state; + } +}; diff --git a/src/modules/hardwareWallet/store/reducers/devicesReducer.test.js b/src/modules/hardwareWallet/store/reducers/devicesReducer.test.js new file mode 100644 index 0000000000..a1353a0d72 --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/devicesReducer.test.js @@ -0,0 +1,21 @@ +import actionTypes from '@hardwareWallet/store/actions/actionTypes'; +import { devices, initialState } from './devicesReducer'; + +describe('HardwareWallet reducer devices', () => { + it('Should update hardwareDevices when dispatching actionTypes.setDevices', async () => { + const updatedDevices = [ + { + deviceId: '3', + }, + ]; + const actionData = { + type: actionTypes.setDevices, + devices: updatedDevices, + }; + expect(devices(initialState, actionData)).toEqual(updatedDevices); + }); + + it('Should return default state', async () => { + expect(devices(initialState, {})).toEqual(initialState); + }); +}); diff --git a/src/modules/hardwareWallet/store/reducers/index.js b/src/modules/hardwareWallet/store/reducers/index.js new file mode 100644 index 0000000000..1ffbe32512 --- /dev/null +++ b/src/modules/hardwareWallet/store/reducers/index.js @@ -0,0 +1,10 @@ +import { combineReducers } from 'redux'; +import { accounts } from './accountsReducers'; +import { currentDevice } from './currentDeviceReducer'; +import { devices } from './devicesReducer'; + +export const hardwareWallet = combineReducers({ + devices, + currentDevice, + accounts, +}); diff --git a/src/modules/hardwareWallet/store/reducers/reducers.js b/src/modules/hardwareWallet/store/reducers/reducers.js deleted file mode 100644 index 5d9158b248..0000000000 --- a/src/modules/hardwareWallet/store/reducers/reducers.js +++ /dev/null @@ -1,28 +0,0 @@ -import actionTypes from '../actions/actionTypes'; - -const initAccounts = []; -const initState = { - deviceId: 0, - status: 'disconnected', - accounts: initAccounts, -}; - -// istanbul ignore next -const hardwareWallet = (state = initState, action) => { - switch (action.type) { - case actionTypes.storeHWAccounts: - return { - ...state, - accounts: action.accounts, - }; - case actionTypes.removeHWAccounts: - return { - ...state, - accounts: initAccounts, - }; - default: - return state; - } -}; - -export default hardwareWallet; diff --git a/src/modules/hardwareWallet/store/selectors/hwSelectors.js b/src/modules/hardwareWallet/store/selectors/hwSelectors.js new file mode 100644 index 0000000000..22f76e10be --- /dev/null +++ b/src/modules/hardwareWallet/store/selectors/hwSelectors.js @@ -0,0 +1,7 @@ +export const selectHardwareDevices = (state) => state.hardwareWallet.devices; + +export const selectActiveHardwareDevice = (state) => state.hardwareWallet.currentDevice; +export const selectActiveHardwareDeviceId = (state) => selectActiveHardwareDevice(state).deviceId; +export const selectHWStatus = (state) => selectActiveHardwareDevice(state).status; + +export const selectHWAccounts = (state) => state.hardwareWallet?.accounts || []; diff --git a/src/modules/hardwareWallet/store/selectors/hwSelectors.test.js b/src/modules/hardwareWallet/store/selectors/hwSelectors.test.js new file mode 100644 index 0000000000..eb2ec3bf2a --- /dev/null +++ b/src/modules/hardwareWallet/store/selectors/hwSelectors.test.js @@ -0,0 +1,32 @@ +import { + selectActiveHardwareDevice, + selectActiveHardwareDeviceId, + selectHardwareDevices, + selectHWStatus, +} from './hwSelectors'; + +describe('HardwareWallet selectors', () => { + const mockState = { + hardwareWallet: { + devices: [ + { deviceId: '1', status: 'connected' }, + { deviceId: '2', status: 'connected' }, + ], + currentDevice: { deviceId: '1', status: 'connected' }, + }, + }; + it('Should select ActiveHardwareDevice', async () => { + expect(selectActiveHardwareDevice(mockState)).toEqual(mockState.hardwareWallet.currentDevice); + }); + it('Should select ActiveHardware DeviceId', async () => { + expect(selectActiveHardwareDeviceId(mockState)).toEqual( + mockState.hardwareWallet.currentDevice.deviceId + ); + }); + it('Should select HardwareDevices', async () => { + expect(selectHardwareDevices(mockState)).toEqual(mockState.hardwareWallet.devices); + }); + it('Should select active hardware wallet status', async () => { + expect(selectHWStatus(mockState)).toEqual(mockState.hardwareWallet.currentDevice.status); + }); +}); diff --git a/src/modules/wallet/store/middlewares/hwManager.js b/src/modules/wallet/store/middlewares/hwManager.js index 577952f5af..5e7d087db3 100644 --- a/src/modules/wallet/store/middlewares/hwManager.js +++ b/src/modules/wallet/store/middlewares/hwManager.js @@ -1,8 +1,11 @@ +import React from 'react'; import { toast } from 'react-toastify'; import { subscribeToDeviceConnected, subscribeToDeviceDisconnected } from '@wallet/utils/hwManager'; import { addSearchParamsToUrl } from 'src/utils/searchParams'; import history from 'src/utils/history'; import actionTypes from 'src/modules/common/store/actionTypes'; +import i18n from 'i18next'; +import DialogLink from '@theme/dialog/link'; const hwWalletMiddleware = store => next => (action) => { const { ipc } = window; @@ -15,7 +18,16 @@ const hwWalletMiddleware = store => next => (action) => { * @param {fn} function - callback function to execute toast dispatch after receive the data */ subscribeToDeviceConnected((response) => { - toast.success(`${response.model} connected`); + const { devices, model } = response; + toast.success(`${model} connected`); + + if (devices?.length > 1) { + toast.info( + + {i18n.t('Click here to select a hardware wallet')} + + ); + } }); /** diff --git a/src/redux/rootReducer.js b/src/redux/rootReducer.js index c0d7e08a42..cbd2078c76 100644 --- a/src/redux/rootReducer.js +++ b/src/redux/rootReducer.js @@ -6,6 +6,7 @@ export { default as token } from '@token/fungible/store/reducer'; export { default as transactions } from '@transaction/store/reducer'; export { default as appUpdates } from '@update/store/reducers/appUpdates'; export { default as staking } from '@pos/validator/store/reducers/staking'; +export { hardwareWallet } from '@hardwareWallet/store/reducers'; export { default as watchList } from '@pos/validator/store/reducers/watchList'; export { account } from '@account/store/reducer'; export { blockChainApplications } from '@blockchainApplication/manage/store/reducer'; diff --git a/src/redux/selectors.js b/src/redux/selectors.js index f5c196a01e..53449ed6f2 100644 --- a/src/redux/selectors.js +++ b/src/redux/selectors.js @@ -27,9 +27,6 @@ const selectNetworkIdentifier = (state) => state.network.networks?.LSK?.networkI const selectNetworkName = (state) => state.network.name; const selectActiveTokenNetwork = (state) => state.network.networks[state.token.active]; const selectStaking = (state) => state.staking; -const selectHW = (state) => state.hardwareWallet; -const selectHWAccounts = (state) => state.hardwareWallet?.accounts || []; -const selectHWStatus = (state) => state.hardwareWallet.status; export { selectStaking, @@ -49,7 +46,4 @@ export { selectNetworkIdentifier, selectNetworkName, selectActiveTokenNetwork, - selectHW, - selectHWAccounts, - selectHWStatus, }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 8e9f7bdfb4..9d3db7d46b 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -195,6 +195,10 @@ export const modals = { isPrivate: true, forbiddenTokens: [], }, + selectHardwareDeviceModal: { + isPrivate: true, + forbiddenTokens: [], + }, stakingQueue: { isPrivate: true, forbiddenTokens: [], diff --git a/src/routes/routesMap.js b/src/routes/routesMap.js index 896ea1f8d8..65a5a097a0 100644 --- a/src/routes/routesMap.js +++ b/src/routes/routesMap.js @@ -56,6 +56,7 @@ import SessionManager from '@blockchainApplication/connection/components/Session import ConnectionSummary from 'src/modules/blockchainApplication/connection/components/ConnectionSummary'; import RequestView from '@blockchainApplication/connection/components/RequestView'; import ConnectionStatus from 'src/modules/blockchainApplication/connection/components/ConnectionStatus'; +import SelectHardwareDeviceModal from 'src/modules/hardwareWallet/components/SelectHardwareDeviceModal'; export default { wallet: AccountDetails, @@ -89,6 +90,7 @@ export default { lockedBalance: UnlockBalanceView, claimRewardsView: ClaimRewardsView, editStake: editStakeManager, + selectHardwareDeviceModal: SelectHardwareDeviceModal, stakingQueue: StakingQueue, deviceDisconnectDialog: DeviceDisconnect, reclaimBalance: ReclaimBalanceModal,