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