diff --git a/src/commands/getAddress.js b/src/commands/getAddress.js index d4a73ae9..d2a24636 100644 --- a/src/commands/getAddress.js +++ b/src/commands/getAddress.js @@ -6,10 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise' import { withDevice } from 'helpers/deviceAccess' import getAddressForCurrency from 'helpers/getAddressForCurrency' -import { createCustomErrorClass } from 'helpers/errors' - -const DeviceAppVerifyNotSupported = createCustomErrorClass('DeviceAppVerifyNotSupported') -const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress') +import { DeviceAppVerifyNotSupported, UserRefusedAddress } from 'config/errors' type Input = { currencyId: string, diff --git a/src/components/DeviceInteraction/components.js b/src/components/DeviceInteraction/components.js index a81e425f..e133639a 100644 --- a/src/components/DeviceInteraction/components.js +++ b/src/components/DeviceInteraction/components.js @@ -2,8 +2,13 @@ import React from 'react' import styled from 'styled-components' +import { translate } from 'react-i18next' + +import type { T } from 'types/common' import { radii } from 'styles/theme' +import { openURL } from 'helpers/linking' +import { urls } from 'config/urls' import TranslatedError from 'components/TranslatedError' import Box from 'components/base/Box' @@ -112,30 +117,41 @@ export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => ( ) -export const ErrorDescContainer = ({ - error, - onRetry, - ...p -}: { - error: Error, - onRetry: void => void, -}) => ( - - - - - - - {'Retry'} - - +export const ErrorDescContainer = translate()( + ({ error, onRetry, t, ...p }: { error: Error, onRetry: void => void, t: T }) => { + const errorHelpURL = urls.errors[error.name] || null + const errorDesc = + return ( + + + + + {!!errorDesc && ( + + {errorDesc} + + )} + + + {!!errorHelpURL && ( + openURL(errorHelpURL)}> + {t('app:common.help')} + + )} + + {t('app:common.retry')} + + + + ) + }, ) diff --git a/src/components/EnsureDeviceApp.js b/src/components/EnsureDeviceApp.js index 6e1be5ac..26dc46a7 100644 --- a/src/components/EnsureDeviceApp.js +++ b/src/components/EnsureDeviceApp.js @@ -21,11 +21,9 @@ import IconUsb from 'icons/Usb' import type { Device } from 'types/common' -import { createCustomErrorClass } from 'helpers/errors' +import { WrongDeviceForAccount, CantOpenDevice } from 'config/errors' import { getCurrentDevice } from 'reducers/devices' -export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') - const usbIcon = const Bold = props => @@ -66,7 +64,8 @@ class EnsureDeviceApp extends Component<{ shouldThrow: (err: Error) => { const isWrongApp = err instanceof BtcUnmatchedApp const isWrongDevice = err instanceof WrongDeviceForAccount - return isWrongApp || isWrongDevice + const isCantOpenDevice = err instanceof CantOpenDevice + return isWrongApp || isWrongDevice || isCantOpenDevice }, }, ) diff --git a/src/components/GenuineCheck.js b/src/components/GenuineCheck.js index eb599bd9..b1012d4d 100644 --- a/src/components/GenuineCheck.js +++ b/src/components/GenuineCheck.js @@ -14,7 +14,7 @@ import type { DeviceInfo } from 'helpers/devices/getDeviceInfo' import { GENUINE_TIMEOUT, DEVICE_INFOS_TIMEOUT, GENUINE_CACHE_DELAY } from 'config/constants' import { getCurrentDevice } from 'reducers/devices' -import { createCustomErrorClass } from 'helpers/errors' +import { CantOpenDevice, DeviceNotGenuineError, DeviceGenuineSocketEarlyClose } from 'config/errors' import getDeviceInfo from 'commands/getDeviceInfo' import getIsGenuine from 'commands/getIsGenuine' @@ -26,9 +26,6 @@ import IconUsb from 'icons/Usb' import IconHome from 'icons/Home' import IconCheck from 'icons/Check' -const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine') -const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose') - type Props = { t: T, onFail?: Error => void, @@ -59,11 +56,18 @@ class GenuineCheck extends PureComponent { }) checkDashboardInteractionHandler = ({ device }: { device: Device }) => - createCancelablePolling(() => - getDeviceInfo - .send({ devicePath: device.path }) - .pipe(timeout(DEVICE_INFOS_TIMEOUT)) - .toPromise(), + createCancelablePolling( + () => + getDeviceInfo + .send({ devicePath: device.path }) + .pipe(timeout(DEVICE_INFOS_TIMEOUT)) + .toPromise(), + { + shouldThrow: (err: Error) => { + const isCantOpenDevice = err instanceof CantOpenDevice + return isCantOpenDevice + }, + }, ) checkGenuineInteractionHandler = async ({ diff --git a/src/components/ManagerPage/ManagerGenuineCheck.js b/src/components/ManagerPage/ManagerGenuineCheck.js index e83ab6cd..ef3fda6c 100644 --- a/src/components/ManagerPage/ManagerGenuineCheck.js +++ b/src/components/ManagerPage/ManagerGenuineCheck.js @@ -38,7 +38,7 @@ class ManagerGenuineCheck extends PureComponent { - + ) } diff --git a/src/components/modals/Receive/steps/04-step-receive-funds.js b/src/components/modals/Receive/steps/04-step-receive-funds.js index e506059c..1b090509 100644 --- a/src/components/modals/Receive/steps/04-step-receive-funds.js +++ b/src/components/modals/Receive/steps/04-step-receive-funds.js @@ -8,8 +8,7 @@ import getAddress from 'commands/getAddress' import { isSegwitAccount } from 'helpers/bip32' import Box from 'components/base/Box' import CurrentAddressForAccount from 'components/CurrentAddressForAccount' -import { WrongDeviceForAccount } from 'components/EnsureDeviceApp' -import { DisconnectedDevice } from 'config/errors' +import { DisconnectedDevice, WrongDeviceForAccount } from 'config/errors' import type { StepProps } from '..' diff --git a/src/config/errors.js b/src/config/errors.js index 4d1e5989..73c5bde5 100644 --- a/src/config/errors.js +++ b/src/config/errors.js @@ -6,3 +6,9 @@ import { createCustomErrorClass } from 'helpers/errors' export const DisconnectedDevice = createCustomErrorClass('DisconnectedDevice') export const UserRefusedOnDevice = createCustomErrorClass('UserRefusedOnDevice') // TODO rename because it's just for transaction refusal +export const CantOpenDevice = createCustomErrorClass('CantOpenDevice') +export const DeviceAppVerifyNotSupported = createCustomErrorClass('DeviceAppVerifyNotSupported') +export const UserRefusedAddress = createCustomErrorClass('UserRefusedAddress') +export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount') +export const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine') +export const DeviceGenuineSocketEarlyClose = createCustomErrorClass('DeviceGenuineSocketEarlyClose') diff --git a/src/config/urls.js b/src/config/urls.js index 636f6474..1e78b33f 100644 --- a/src/config/urls.js +++ b/src/config/urls.js @@ -27,4 +27,9 @@ export const urls = { coinmama: 'http://go.coinmama.com/visit/?bta=51801&nci=5343', simplex: 'https://partners.simplex.com/?partner=ledger', paybis: 'https://paybis.idevaffiliate.com/idevaffiliate.php?id=4064', + + // Errors + errors: { + CantOpenDevice: 'https://support.ledgerwallet.com/hc/en-us/articles/115005165269', + }, } diff --git a/src/helpers/deviceAccess.js b/src/helpers/deviceAccess.js index 2b0a15ff..05d9ced3 100644 --- a/src/helpers/deviceAccess.js +++ b/src/helpers/deviceAccess.js @@ -3,8 +3,7 @@ import logger from 'logger' import throttle from 'lodash/throttle' import type Transport from '@ledgerhq/hw-transport' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { DisconnectedDevice } from 'config/errors' -import { retry } from './promise' +import { DisconnectedDevice, CantOpenDevice } from 'config/errors' // all open to device must use openDevice so we can prevent race conditions // and guarantee we do one device access at a time. It also will handle the .close() @@ -13,6 +12,9 @@ import { retry } from './promise' type WithDevice = (devicePath: string) => (job: (Transport<*>) => Promise<*>) => Promise const mapError = e => { + if (e && e.message && e.message.indexOf('cannot open device with path') >= 0) { + throw new CantOpenDevice(e.message) + } if (e && e.message && e.message.indexOf('HID') >= 0) { throw new DisconnectedDevice(e.message) } @@ -37,8 +39,7 @@ export const withDevice: WithDevice = devicePath => job => { busy = true refreshBusyUIState() try { - // FIXME: remove this retry - const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) + const t = await TransportNodeHid.open(devicePath).catch(mapError) t.setDebugMode(logger.apdu) try { const res = await job(t).catch(mapError) diff --git a/static/i18n/en/app.json b/static/i18n/en/app.json index 836de549..a5164b1a 100644 --- a/static/i18n/en/app.json +++ b/static/i18n/en/app.json @@ -9,6 +9,7 @@ "launch": "Launch", "continue": "Continue", "learnMore": "Learn more", + "help": "Help", "skipThisStep": "Skip this step", "needHelp": "Need help?", "areYouSure": "Are you sure?", diff --git a/static/i18n/en/errors.json b/static/i18n/en/errors.json index 53f7a6c2..b504a56d 100644 --- a/static/i18n/en/errors.json +++ b/static/i18n/en/errors.json @@ -153,5 +153,9 @@ }, "InvalidAddress": { "title": "This is not a valid {{currencyName}} address" + }, + "CantOpenDevice": { + "title": "Oops, couldn’t connect to device", + "description": "Device detected but connection failed. Please retry and get help if the problem persists." } } \ No newline at end of file diff --git a/static/i18n/fr/errors.json b/static/i18n/fr/errors.json index 53f7a6c2..b504a56d 100644 --- a/static/i18n/fr/errors.json +++ b/static/i18n/fr/errors.json @@ -153,5 +153,9 @@ }, "InvalidAddress": { "title": "This is not a valid {{currencyName}} address" + }, + "CantOpenDevice": { + "title": "Oops, couldn’t connect to device", + "description": "Device detected but connection failed. Please retry and get help if the problem persists." } } \ No newline at end of file