Connect Ledger via WebHID (#12411)
* Connect ledger via webhid if that option is available * Explicitly setting preference for webhid * Use ledgerTransportType enum instead of booleans for ledger live and webhid preferences * Use single setLEdgerTransport preference methods and property * Temp * Lint fix * Unit test fix * Remove async keyword from setLedgerTransportPreference function definition in preferences controller * Fix ledgelive setting toggle logic * Migrate useLedgerLive preference property to ledgerTransportType * Use shared constants for ledger transport type enums * Use constant for ledger usb vendor id * Use correct property to check if ledgerLive preference is set when deciding whether to ask for webhid connection * Update eth-ledger-bridge-keyring to v0.9.0 * Only show ledger live transaction helper messages if using ledger live * Only show ledger live part of tutorial if ledger live setting is on * Fix ledger related prop type errors * Explicitly use u2f enum instead of empty string as a transport type; default transport type to webhid if available; use constants for u2f and webhid * Cleanup * Wrap ledger webhid device request in try/catch * Clean up * Lint fix * Ensure user can easily connect their ledger wallet when they need to. * Fix locales * Fix/improve locales changes * Remove unused isFirefox property from confirm-transaction-base.container.js * Disable transaction and message signing confirmation if ledger webhid requires connection * Ensure translation keys for ledger connection options in settings dropdown can be properly detected by verify-locales * Drop .component from ledger-instruction-field file name * Move renderLedgerLiveStep to module scope * Remove ledgerLive from function and message names in ledger-instruction-field * Wrap ledger connection logic in ledger-instruction-field in try catch * Clean up signature-request.component.js * Check whether the signing address, and not the selected address, is a ledger account in singature-request.container * Ensure ledger instructions and webhid connection button are shown on signature-request-original signatures * Improve webhid selection handling in select-ledger-transport-type onChange handler * Move metamask redux focused ledger selectors to metamask duck * Lint fix * Use async await in checkWebHidStatusRef.current * Remove unnecessary use of ref in ledger-instruction-field.js * Lint fix * Remove unnecessary try/catch in ledger-instruction-field.js * Check if from address, not selected address, is from a ledger account in confirm-approve * Move findKeyringForAddress to metamask duck * Fix typo in function name * Ensure isEqualCaseInsensitive handles possible differences in address casing * Fix Learn More link size in advanced settings tab * Update app/scripts/migrations/066.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * Update ui/pages/settings/advanced-tab/advanced-tab.component.test.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * Add jsdoc comments for new selectors * Use jest.spyOn for mocking navigator in ledger webhid migration tests * Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType * Use LEDGER_TRANSPORT_TYPES values to set proptype of ledgerTransportType * Fix font size of link in ledger connection description in advanced settings * Fix return type in setLedgerTransportPreference comment * Clean up connectHardware code for webhid connection in actions.js * Update app/scripts/migrations/066.test.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * Update ui/ducks/metamask/metamask.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * Add migration test for when useLedgerLive is true in a browser that supports webhid * Lint fix * Fix inline-link size Co-authored-by: Mark Stacey <markjstacey@gmail.com>feature/default_network_editable
parent
1a1fa3aac5
commit
9d70c60c22
@ -0,0 +1,37 @@ |
||||
import { cloneDeep } from 'lodash'; |
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; |
||||
|
||||
const version = 66; |
||||
|
||||
/** |
||||
* Changes the useLedgerLive boolean property to the ledgerTransportType enum |
||||
*/ |
||||
export default { |
||||
version, |
||||
async migrate(originalVersionedData) { |
||||
const versionedData = cloneDeep(originalVersionedData); |
||||
versionedData.meta.version = version; |
||||
const state = versionedData.data; |
||||
const newState = transformState(state); |
||||
versionedData.data = newState; |
||||
return versionedData; |
||||
}, |
||||
}; |
||||
|
||||
function transformState(state) { |
||||
const defaultTransportType = window.navigator.hid |
||||
? LEDGER_TRANSPORT_TYPES.WEBHID |
||||
: LEDGER_TRANSPORT_TYPES.U2F; |
||||
const useLedgerLive = Boolean(state.PreferencesController?.useLedgerLive); |
||||
const newState = { |
||||
...state, |
||||
PreferencesController: { |
||||
...state?.PreferencesController, |
||||
ledgerTransportType: useLedgerLive |
||||
? LEDGER_TRANSPORT_TYPES.LIVE |
||||
: defaultTransportType, |
||||
}, |
||||
}; |
||||
delete newState.PreferencesController.useLedgerLive; |
||||
return newState; |
||||
} |
@ -0,0 +1,116 @@ |
||||
import { LEDGER_TRANSPORT_TYPES } from '../../../shared/constants/hardware-wallets'; |
||||
import migration66 from './066'; |
||||
|
||||
describe('migration #66', () => { |
||||
beforeEach(() => { |
||||
jest.resetAllMocks(); |
||||
}); |
||||
|
||||
it('should update the version metadata', async () => { |
||||
const oldStorage = { |
||||
meta: { |
||||
version: 65, |
||||
}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect(newStorage.meta).toStrictEqual({ |
||||
version: 66, |
||||
}); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if no preferences controller exists and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: {}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if no useLedgerLive property exists and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: {}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `u2f` if useLedgerLive is false and webhid is not available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: false, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.U2F); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `webhid` if useLedgerLive is false and webhid is available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: false, |
||||
}, |
||||
}, |
||||
}; |
||||
jest |
||||
.spyOn(window, 'navigator', 'get') |
||||
.mockImplementation(() => ({ hid: true })); |
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.WEBHID); |
||||
}); |
||||
|
||||
it('should set ledgerTransportType to `ledgerLive` if useLedgerLive is true', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: true, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual('ledgerLive'); |
||||
}); |
||||
|
||||
it('should not change ledgerTransportType if useLedgerLive is true and webhid is available', async () => { |
||||
const oldStorage = { |
||||
meta: {}, |
||||
data: { |
||||
PreferencesController: { |
||||
useLedgerLive: true, |
||||
}, |
||||
}, |
||||
}; |
||||
jest |
||||
.spyOn(window, 'navigator', 'get') |
||||
.mockImplementation(() => ({ hid: true })); |
||||
const newStorage = await migration66.migrate(oldStorage); |
||||
expect( |
||||
newStorage.data.PreferencesController.ledgerTransportType, |
||||
).toStrictEqual(LEDGER_TRANSPORT_TYPES.LIVE); |
||||
}); |
||||
}); |
@ -0,0 +1 @@ |
||||
export { default } from './ledger-instruction-field'; |
@ -0,0 +1,153 @@ |
||||
import React, { useEffect } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import { |
||||
LEDGER_TRANSPORT_TYPES, |
||||
LEDGER_USB_VENDOR_ID, |
||||
WEBHID_CONNECTED_STATUSES, |
||||
} from '../../../../shared/constants/hardware-wallets'; |
||||
import { |
||||
PLATFORM_FIREFOX, |
||||
ENVIRONMENT_TYPE_FULLSCREEN, |
||||
} from '../../../../shared/constants/app'; |
||||
|
||||
import { |
||||
setLedgerWebHidConnectedStatus, |
||||
getLedgerWebHidConnectedStatus, |
||||
} from '../../../ducks/app/app'; |
||||
|
||||
import Typography from '../../ui/typography/typography'; |
||||
import Button from '../../ui/button'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
import { |
||||
COLORS, |
||||
FONT_WEIGHT, |
||||
TYPOGRAPHY, |
||||
} from '../../../helpers/constants/design-system'; |
||||
import Dialog from '../../ui/dialog'; |
||||
import { |
||||
getPlatform, |
||||
getEnvironmentType, |
||||
} from '../../../../app/scripts/lib/util'; |
||||
import { getLedgerTransportType } from '../../../ducks/metamask/metamask'; |
||||
|
||||
const renderInstructionStep = (text, show = true, color = COLORS.PRIMARY3) => { |
||||
return ( |
||||
show && ( |
||||
<Typography |
||||
boxProps={{ margin: 0 }} |
||||
color={color} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
variant={TYPOGRAPHY.H7} |
||||
> |
||||
{text} |
||||
</Typography> |
||||
) |
||||
); |
||||
}; |
||||
|
||||
export default function LedgerInstructionField({ showDataInstruction }) { |
||||
const t = useI18nContext(); |
||||
const dispatch = useDispatch(); |
||||
|
||||
const webHidConnectedStatus = useSelector(getLedgerWebHidConnectedStatus); |
||||
const ledgerTransportType = useSelector(getLedgerTransportType); |
||||
const environmentType = getEnvironmentType(); |
||||
const environmentTypeIsFullScreen = |
||||
environmentType === ENVIRONMENT_TYPE_FULLSCREEN; |
||||
|
||||
useEffect(() => { |
||||
const initialConnectedDeviceCheck = async () => { |
||||
if ( |
||||
ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID && |
||||
webHidConnectedStatus !== WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
) { |
||||
const devices = await window.navigator.hid.getDevices(); |
||||
const webHidIsConnected = devices.some( |
||||
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID), |
||||
); |
||||
dispatch( |
||||
setLedgerWebHidConnectedStatus( |
||||
webHidIsConnected |
||||
? WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
), |
||||
); |
||||
} |
||||
}; |
||||
initialConnectedDeviceCheck(); |
||||
}, [dispatch, ledgerTransportType, webHidConnectedStatus]); |
||||
|
||||
const usingLedgerLive = ledgerTransportType === LEDGER_TRANSPORT_TYPES.LIVE; |
||||
const usingWebHID = ledgerTransportType === LEDGER_TRANSPORT_TYPES.WEBHID; |
||||
|
||||
const isFirefox = getPlatform() === PLATFORM_FIREFOX; |
||||
|
||||
return ( |
||||
<div> |
||||
<div className="confirm-detail-row"> |
||||
<Dialog type="message"> |
||||
<div className="ledger-live-dialog"> |
||||
{renderInstructionStep(t('ledgerConnectionInstructionHeader'))} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepOne')}`, |
||||
!isFirefox && usingLedgerLive, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepTwo')}`, |
||||
!isFirefox && usingLedgerLive, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepThree')}`, |
||||
)} |
||||
{renderInstructionStep( |
||||
`- ${t('ledgerConnectionInstructionStepFour')}`, |
||||
showDataInstruction, |
||||
)} |
||||
{renderInstructionStep( |
||||
<span> |
||||
<Button |
||||
type="link" |
||||
onClick={async () => { |
||||
if (environmentTypeIsFullScreen) { |
||||
const connectedDevices = await window.navigator.hid.requestDevice( |
||||
{ |
||||
filters: [{ vendorId: LEDGER_USB_VENDOR_ID }], |
||||
}, |
||||
); |
||||
const webHidIsConnected = connectedDevices.some( |
||||
(device) => |
||||
device.vendorId === Number(LEDGER_USB_VENDOR_ID), |
||||
); |
||||
dispatch( |
||||
setLedgerWebHidConnectedStatus({ |
||||
webHidConnectedStatus: webHidIsConnected |
||||
? WEBHID_CONNECTED_STATUSES.CONNECTED |
||||
: WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
}), |
||||
); |
||||
} else { |
||||
global.platform.openExtensionInBrowser(null, null, true); |
||||
} |
||||
}} |
||||
> |
||||
{environmentTypeIsFullScreen |
||||
? t('clickToConnectLedgerViaWebHID') |
||||
: t('openFullScreenForLedgerWebHid')} |
||||
</Button> |
||||
</span>, |
||||
usingWebHID && |
||||
webHidConnectedStatus === |
||||
WEBHID_CONNECTED_STATUSES.NOT_CONNECTED, |
||||
COLORS.SECONDARY1, |
||||
)} |
||||
</div> |
||||
</Dialog> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
LedgerInstructionField.propTypes = { |
||||
showDataInstruction: PropTypes.bool, |
||||
}; |
Loading…
Reference in new issue