Browse Source

Detect "Cant open device" errors and display link to help center

Fixes #1057
master
meriadec 7 years ago
parent
commit
75eff76704
No known key found for this signature in database GPG Key ID: 1D2FC2305E2CB399
  1. 5
      src/commands/getAddress.js
  2. 68
      src/components/DeviceInteraction/components.js
  3. 7
      src/components/EnsureDeviceApp.js
  4. 22
      src/components/GenuineCheck.js
  5. 2
      src/components/ManagerPage/ManagerGenuineCheck.js
  6. 3
      src/components/modals/Receive/steps/04-step-receive-funds.js
  7. 6
      src/config/errors.js
  8. 5
      src/config/urls.js
  9. 9
      src/helpers/deviceAccess.js
  10. 1
      static/i18n/en/app.json
  11. 4
      static/i18n/en/errors.json
  12. 4
      static/i18n/fr/errors.json

5
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,

68
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 }) => (
</ErrorContainerWrapper>
)
export const ErrorDescContainer = ({
error,
onRetry,
...p
}: {
error: Error,
onRetry: void => void,
}) => (
<Box
horizontal
fontSize={3}
color="alertRed"
align="center"
cursor="text"
ff="Open Sans|SemiBold"
style={{ maxWidth: 500 }}
{...p}
>
<IconExclamationCircle size={16} />
<Box ml={2} mr={1} shrink grow style={{ maxWidth: 300 }}>
<TranslatedError error={error} />
</Box>
<FakeLink ml="auto" underline color="alertRed" onClick={onRetry}>
{'Retry'}
</FakeLink>
</Box>
export const ErrorDescContainer = translate()(
({ error, onRetry, t, ...p }: { error: Error, onRetry: void => void, t: T }) => {
const errorHelpURL = urls.errors[error.name] || null
const errorDesc = <TranslatedError error={error} field="description" />
return (
<Box
horizontal
fontSize={3}
color="alertRed"
align="flex-start"
cursor="text"
ff="Open Sans|SemiBold"
style={{ maxWidth: 500 }}
{...p}
>
<IconExclamationCircle size={16} />
<Box ml={2} mr={1} shrink grow style={{ maxWidth: 300 }}>
<TranslatedError error={error} />
{!!errorDesc && (
<Box ff="Open Sans|Regular" mt={1}>
{errorDesc}
</Box>
)}
</Box>
<Box ml="auto" horizontal flow={2}>
{!!errorHelpURL && (
<FakeLink underline color="alertRed" onClick={() => openURL(errorHelpURL)}>
{t('app:common.help')}
</FakeLink>
)}
<FakeLink underline color="alertRed" onClick={onRetry}>
{t('app:common.retry')}
</FakeLink>
</Box>
</Box>
)
},
)

7
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 = <IconUsb size={16} />
const Bold = props => <Text ff="Open Sans|SemiBold" {...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
},
},
)

22
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<Props> {
})
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 ({

2
src/components/ManagerPage/ManagerGenuineCheck.js

@ -38,7 +38,7 @@ class ManagerGenuineCheck extends PureComponent<Props> {
</Text>
</Box>
<Space of={40} />
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} />
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} style={{ width: 400 }} />
</Box>
)
}

3
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 '..'

6
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')

5
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',
},
}

9
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) => <T>(job: (Transport<*>) => Promise<*>) => Promise<T>
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)

1
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?",

4
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."
}
}

4
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."
}
}
Loading…
Cancel
Save