Browse Source

Merge pull request #768 from LedgerHQ/develop

Prepare for beta.4
master
Gaëtan Renaudeau 7 years ago
committed by GitHub
parent
commit
c97907aca5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .circleci/config.yml
  2. 13
      PULL_REQUEST_TEMPLATE.md
  3. 12
      README.md
  4. BIN
      build/icon.png
  5. BIN
      build/windows/app.ico
  6. BIN
      build/windows/installer.ico
  7. BIN
      build/windows/installerSidebar.bmp
  8. BIN
      build/windows/uninstaller.ico
  9. BIN
      build/windows/uninstallerSidebar.bmp
  10. 2
      electron-builder.yml
  11. 2
      package.json
  12. 2
      scripts/compile.sh
  13. 2
      scripts/dist-dir.sh
  14. 2
      scripts/dist.sh
  15. 10
      scripts/download-analytics.sh
  16. 2
      scripts/hash-utils.sh
  17. 2
      scripts/install-ci-deps.sh
  18. 2
      scripts/postinstall.sh
  19. 2
      scripts/release.sh
  20. 38
      scripts/reset-files.sh
  21. 2
      scripts/start.sh
  22. 32
      src/analytics/Track.js
  23. 14
      src/analytics/TrackPage.js
  24. 18
      src/analytics/inject-in-window.js
  25. 91
      src/analytics/segment.js
  26. 45
      src/api/network.js
  27. 4
      src/commands/index.js
  28. 8
      src/commands/installFinalFirmware.js
  29. 8
      src/commands/installMcu.js
  30. 4
      src/commands/installOsuFirmware.js
  31. 15
      src/commands/listAppVersions.js
  32. 11
      src/commands/listApps.js
  33. 16
      src/commands/listCategories.js
  34. 125
      src/components/AccountPage/AccountBalanceSummaryHeader.js
  35. 100
      src/components/AccountPage/AccountHeaderActions.js
  36. 8
      src/components/AccountPage/EmptyStateAccount.js
  37. 164
      src/components/AccountPage/index.js
  38. 1
      src/components/BalanceSummary/index.js
  39. 10
      src/components/CalculateBalance.js
  40. 7
      src/components/ConfettiParty/Confetti.js
  41. 82
      src/components/ConfettiParty/index.js
  42. 53
      src/components/CurrentAddress/index.js
  43. 15
      src/components/DashboardPage/EmptyState.js
  44. 19
      src/components/DashboardPage/index.js
  45. 321
      src/components/DeviceConnect/index.js
  46. 64
      src/components/DeviceConnect/stories.js
  47. 203
      src/components/DeviceInteraction/DeviceInteractionStep.js
  48. 121
      src/components/DeviceInteraction/components.js
  49. 116
      src/components/DeviceInteraction/index.js
  50. 82
      src/components/DeviceInteraction/stories.js
  51. 0
      src/components/EnsureDevice.js
  52. 310
      src/components/EnsureDeviceApp.js
  53. 51
      src/components/ExchangePage/ExchangeCard.js
  54. 49
      src/components/ExchangePage/index.js
  55. 2
      src/components/ExportLogsBtn.js
  56. 156
      src/components/GenuineCheck.js
  57. 37
      src/components/GenuineCheckModal.js
  58. 90
      src/components/GenuineCheckModal/index.js
  59. 1
      src/components/IsUnlocked.js
  60. 114
      src/components/ManagerPage/AppsList.js
  61. 9
      src/components/ManagerPage/Dashboard.js
  62. 62
      src/components/ManagerPage/FirmwareFinalUpdate.js
  63. 122
      src/components/ManagerPage/FirmwareUpdate.js
  64. 50
      src/components/ManagerPage/FlashMcu.js
  65. 27
      src/components/ManagerPage/ManagerApp.js
  66. 48
      src/components/ManagerPage/ManagerGenuineCheck.js
  67. 15
      src/components/ManagerPage/UpdateFirmwareButton.js
  68. 68
      src/components/ManagerPage/index.js
  69. 30
      src/components/Onboarding/helperComponents.js
  70. 1
      src/components/Onboarding/index.js
  71. 28
      src/components/Onboarding/steps/Analytics.js
  72. 94
      src/components/Onboarding/steps/Finish.js
  73. 14
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js
  74. 46
      src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js
  75. 42
      src/components/Onboarding/steps/GenuineCheck/index.js
  76. 16
      src/components/Onboarding/steps/Init.js
  77. 32
      src/components/Onboarding/steps/NoDevice.js
  78. 3
      src/components/Onboarding/steps/SelectDevice.js
  79. 14
      src/components/Onboarding/steps/SelectPIN/SelectPINblue.js
  80. 14
      src/components/Onboarding/steps/SelectPIN/SelectPINnano.js
  81. 14
      src/components/Onboarding/steps/SelectPIN/SelectPINrestoreBlue.js
  82. 18
      src/components/Onboarding/steps/SelectPIN/SelectPINrestoreNano.js
  83. 36
      src/components/Onboarding/steps/SelectPIN/index.js
  84. 17
      src/components/Onboarding/steps/SetPassword.js
  85. 11
      src/components/Onboarding/steps/Start.js
  86. 19
      src/components/Onboarding/steps/WriteSeed/WriteSeedBlue.js
  87. 27
      src/components/Onboarding/steps/WriteSeed/WriteSeedNano.js
  88. 27
      src/components/Onboarding/steps/WriteSeed/WriteSeedRestore.js
  89. 30
      src/components/Onboarding/steps/WriteSeed/index.js
  90. 12
      src/components/OperationsList/index.js
  91. 1
      src/components/RenderError.js
  92. 2
      src/components/SettingsPage/SettingsSection.js
  93. 12
      src/components/SettingsPage/sections/About.js
  94. 8
      src/components/SettingsPage/sections/Currencies.js
  95. 74
      src/components/SettingsPage/sections/Display.js
  96. 39
      src/components/SettingsPage/sections/Profile.js
  97. 8
      src/components/TopBar/index.js
  98. 15
      src/components/TranslatedError.js
  99. 3
      src/components/UpdateNotifier/UpdateDownloaded.js
  100. 81
      src/components/Workflow/EnsureDashboard.js

4
.circleci/config.yml

@ -15,13 +15,13 @@ jobs:
- restore_cache:
name: Restore Yarn Package Cache
keys:
- v1-yarn-packages-{{ checksum "yarn.lock" }}
- v2-yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: bash scripts/install-ci-deps.sh
- save_cache:
name: Save Yarn Package Cache
key: v1-yarn-packages-{{ checksum "yarn.lock" }}
key: v2-yarn-packages-{{ checksum "yarn.lock" }}
paths:
- node_modules/
- run:

13
PULL_REQUEST_TEMPLATE.md

@ -0,0 +1,13 @@
<!-- Description of what the PR does go here... screenshot might be good if appropriate -->
### Type
<!-- e.g. Bug Fix, Feature, Code Quality Improvement, UI Polish... -->
### Context
<!-- e.g. GitHub issue #45 / contextual discussion -->
### Parts of the app affected / Test plan
<!-- Which part of the app is affected? What to do to test it, any special thing to do? -->

12
README.md

@ -14,7 +14,7 @@
From one side Ledger Desktop app connected to the Blockchain via the in-house written C++ library - LibCore and from the other it communicates to the Ledger Hardware Device to securely sign all transactions.
<p align="center">
<img src="/static/docs/architecture.jpg" width="550"/>
<img src="/static/docs/architecture.png" width="550"/>
</p>
## Setup
@ -120,12 +120,14 @@ yarn flow # launch flow
yarn test # launch unit tests
```
### Programmaically reset hard the app
Stop the app and to clean accounts, settings, etc, run
### Programmatically reset app files
```bash
rm -rf ~/Library/Application\ Support/Electron/
# clear the dev electron user data directory
# it remove sqlite db, accounts, settings
# useful to start from a fresh state
yarn reset-files
```
## File structure

BIN
build/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 138 KiB

BIN
build/windows/app.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
build/windows/installer.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 216 KiB

BIN
build/windows/installerSidebar.bmp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 151 KiB

BIN
build/windows/uninstaller.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

BIN
build/windows/uninstallerSidebar.bmp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 151 KiB

2
electron-builder.yml

@ -27,6 +27,7 @@ linux:
win:
artifactName: ${name}-${version}-${os}-${arch}.${ext}
icon: build/windows/app.ico
certificateSubjectName: Ledger SAS
certificateSha1: 7dd9acb2ef0402883c65901ebbafd06e5293d391
signingHashAlgorithms:
@ -45,6 +46,7 @@ nsis:
allowToChangeInstallationDirectory: true
installerIcon: build/windows/installer.ico
installerSidebar: build/windows/installerSidebar.bmp
uninstallerIcon: build/windows/uninstaller.ico
uninstallerSidebar: build/windows/uninstallerSidebar.bmp
files:

2
package.json

@ -20,7 +20,7 @@
"release": "bash ./scripts/release.sh",
"start": "bash ./scripts/start.sh",
"storybook": "NODE_ENV=development STORYBOOK_ENV=1 start-storybook -s ./static -p 4444",
"trans": "node scripts/trans"
"reset-files": "bash ./scripts/reset-files.sh"
},
"electronWebpack": {
"title": true,

2
scripts/compile.sh

@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
set -e

2
scripts/dist-dir.sh

@ -1,3 +1,3 @@
#/bin/bash
#!/bin/bash
yarn compile && DEBUG=electron-builder electron-builder --dir -c.compression=store -c.mac.identity=null

2
scripts/dist.sh

@ -1,3 +1,3 @@
#/bin/bash
#!/bin/bash
yarn compile && DEBUG=electron-builder electron-builder

10
scripts/download-analytics.sh

@ -0,0 +1,10 @@
#!/bin/bash
if [ -z $ANALYTICS_KEY ]; then
echo 'ANALYTICS_KEY must be set'
exit 1
fi
cd `dirname $0`/..
wget https://cdn.segment.com/analytics.js/v1/$ANALYTICS_KEY/analytics.min.js -O static/analytics.min.js

2
scripts/hash-utils.sh

@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
function GET_HASH_PATH {
HASH_NAME=$1

2
scripts/install-ci-deps.sh

@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
source scripts/hash-utils.sh

2
scripts/postinstall.sh

@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
source scripts/hash-utils.sh

2
scripts/release.sh

@ -21,4 +21,4 @@ fi
# TODO check if local git HEAD is EXACTLY our remote master HEAD
yarn compile
build
DEBUG=electron-builder yarn run electron-builder build --publish always

38
scripts/reset-files.sh

@ -0,0 +1,38 @@
#!/bin/bash
set -e
echo "> Getting user data folder..."
TMP_FILE=`mktemp`
cat <<EOF > $TMP_FILE
const { app } = require('electron')
console.log(app.getPath('userData'))
EOF
USER_DATA_FOLDER=`timeout 0.5 electron $TMP_FILE || echo` # echo used to ensure status 0
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "You probably are on a slow computer. Be patient..."
USER_DATA_FOLDER=`timeout 3 electron $TMP_FILE || echo` # echo used to ensure status 0
fi
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "Apparently, very very slow computer..."
USER_DATA_FOLDER=`timeout 6 electron $TMP_FILE || echo` # echo used to ensure status 0
fi
rm $TMP_FILE
if [ "$USER_DATA_FOLDER" == "" ]; then
echo "Could not find the data folder. Bye"
exit 0
fi
read -p "> Remove folder \"$USER_DATA_FOLDER\"? (y/n) " -n 1 -r
echo
if [[ $REPLY == "y" ]]
then
rm -rf "$USER_DATA_FOLDER"
else
echo "> Nothing done. Bye"
fi

2
scripts/start.sh

@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
concurrently --raw --kill-others \
"cross-env NODE_ENV=development webpack-cli --mode development --watch --config webpack/internals.config.js" \

32
src/analytics/Track.js

@ -0,0 +1,32 @@
import logger from 'logger'
import { PureComponent } from 'react'
import { track } from './segment'
class Track extends PureComponent<{
onMount?: boolean,
onUnmount?: boolean,
onUpdate?: boolean,
event: string,
}> {
componentDidMount() {
if (typeof this.props.event !== 'string') {
logger.warn('analytics Track: invalid event=', this.props.event)
}
if (this.props.onMount) this.track()
}
componentDidUpdate() {
if (this.props.onUpdate) this.track()
}
componentWillUnmount() {
if (this.props.onUnmount) this.track()
}
track = () => {
const { event, onMount, onUnmount, onUpdate, ...properties } = this.props
track(event, properties)
}
render() {
return null
}
}
export default Track

14
src/analytics/TrackPage.js

@ -0,0 +1,14 @@
import { PureComponent } from 'react'
import { page } from './segment'
class TrackPage extends PureComponent<{ category: string, name?: string }> {
componentDidMount() {
const { category, name, ...properties } = this.props
page(category, name, properties)
}
render() {
return null
}
}
export default TrackPage

18
src/analytics/inject-in-window.js

@ -0,0 +1,18 @@
/* eslint-disable */
import { getPath } from 'helpers/staticPath'
// prettier-ignore
!function(){var analytics=window.analytics=window.analytics||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error("Segment snippet included twice.");else{analytics.invoked=!0;analytics.methods=["trackSubmit","trackClick","trackLink","trackForm","pageview","identify","reset","group","track","ready","alias","debug","page","once","off","on"];analytics.factory=function(t){return function(){var e=Array.prototype.slice.call(arguments);e.unshift(t);analytics.push(e);return analytics}};for(var t=0;t<analytics.methods.length;t++){var e=analytics.methods[t];analytics[e]=analytics.factory(e)};analytics.SNIPPET_VERSION="4.1.0";
}}();
let loaded = false
export const load = () => {
if (loaded) return
loaded = true
var n = document.createElement('script')
n.type = 'text/javascript'
n.async = !0
n.src = getPath('analytics.min.js')
var a = document.getElementsByTagName('script')[0]
a.parentNode.insertBefore(n, a)
}

91
src/analytics/segment.js

@ -0,0 +1,91 @@
// @flow
import uuid from 'uuid/v4'
import logger from 'logger'
import invariant from 'invariant'
import user from 'helpers/user'
import { langAndRegionSelector } from 'reducers/settings'
import { getSystemLocale } from 'helpers/systemLocale'
import { load } from './inject-in-window'
invariant(typeof window !== 'undefined', 'analytics/segment must be called on renderer thread')
const sessionId = uuid()
const getContext = store => {
const state = store.getState()
const { language, region } = langAndRegionSelector(state)
const systemLocale = getSystemLocale()
return {
ip: '0.0.0.0',
appVersion: __APP_VERSION__,
language,
region,
environment: __DEV__ ? 'development' : 'production',
systemLanguage: systemLocale.language,
systemRegion: systemLocale.region,
sessionId,
}
}
let storeInstance // is the redux store. it's also used as a flag to know if analytics is on or off.
export const start = (store: *) => {
const { id } = user()
logger.analyticsStart(id)
storeInstance = store
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
load()
analytics.identify(
id,
{},
{
context: getContext(store),
},
)
}
export const stop = () => {
logger.analyticsStop()
storeInstance = null
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.reset()
}
export const track = (event: string, properties: ?Object) => {
logger.analyticsTrack(event, properties)
if (!storeInstance) {
return
}
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.track(event, properties, {
context: getContext(storeInstance),
})
}
export const page = (category: string, name: ?string, properties: ?Object) => {
logger.analyticsPage(category, name, properties)
if (!storeInstance) {
return
}
const { analytics } = window
if (typeof analytics === 'undefined') {
logger.error('analytics is not available')
return
}
analytics.page(category, name, properties, {
context: getContext(storeInstance),
})
}

45
src/api/network.js

@ -9,12 +9,13 @@ export const LedgerAPIErrorWithMessage = createCustomErrorClass('LedgerAPIErrorW
export const LedgerAPIError = createCustomErrorClass('LedgerAPIError')
export const NetworkDown = createCustomErrorClass('NetworkDown')
const userFriendlyError = <A>(p: Promise<A>): Promise<A> =>
const userFriendlyError = <A>(p: Promise<A>, { url, method, startTime }): Promise<A> =>
p.catch(error => {
let errorToThrow
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const { data } = error.response
const { data, status } = error.response
if (data && typeof data.error === 'string') {
let msg = data.error || data.message
if (typeof msg === 'string') {
@ -32,19 +33,28 @@ const userFriendlyError = <A>(p: Promise<A>): Promise<A> =>
} catch (e) {
logger.warn("can't parse server result", e)
}
if (msg && msg[0] !== '<') {
throw new LedgerAPIErrorWithMessage(msg)
errorToThrow = new LedgerAPIErrorWithMessage(msg)
}
}
}
const { status } = error.response
logger.log('Ledger API: HTTP status', status, 'data: ', error.response.data)
throw new LedgerAPIError(`LedgerAPIError ${status}`, { status })
if (!errorToThrow) {
errorToThrow = new LedgerAPIError(`LedgerAPIError ${status}`, { status })
}
logger.networkError({
status,
url,
method,
error: errorToThrow.message,
responseTime: Date.now() - startTime,
})
throw errorToThrow
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
logger.networkDown({
url,
method,
responseTime: Date.now() - startTime,
})
throw new NetworkDown()
}
throw error
@ -62,7 +72,20 @@ let implementation = (arg: Object) => {
} else {
promise = axios(arg)
}
return userFriendlyError(promise)
const meta = {
url: arg.url,
method: arg.method,
startTime: Date.now(),
}
logger.network(meta)
promise.then(response => {
logger.networkSucceed({
...meta,
status: response.status,
responseTime: Date.now() - meta.startTime,
})
})
return userFriendlyError(promise, meta)
}
export const setImplementation = (impl: *) => {

4
src/commands/index.js

@ -22,6 +22,8 @@ import libcoreSignAndBroadcast from 'commands/libcoreSignAndBroadcast'
import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import listCategories from 'commands/listCategories'
import listenDevices from 'commands/listenDevices'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
@ -49,6 +51,8 @@ const all: Array<Command<any, any>> = [
libcoreSyncAccount,
libcoreValidAddress,
listApps,
listAppVersions,
listCategories,
listenDevices,
signTransaction,
testApdu,

8
src/commands/installFinalFirmware.js

@ -3,23 +3,19 @@
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import installFinalFirmware from 'helpers/firmware/installFinalFirmware'
type Input = {
devicePath: string,
deviceInfo: DeviceInfo,
}
type Result = {
success: boolean,
}
const cmd: Command<Input, Result> = createCommand(
'installFinalFirmware',
({ devicePath, deviceInfo }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport, deviceInfo))),
const cmd: Command<Input, Result> = createCommand('installFinalFirmware', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installFinalFirmware(transport))),
)
export default cmd

8
src/commands/installMcu.js

@ -8,16 +8,12 @@ import installMcu from 'helpers/firmware/installMcu'
type Input = {
devicePath: string,
targetId: string | number,
version: string,
}
type Result = *
const cmd: Command<Input, Result> = createCommand(
'installMcu',
({ devicePath, targetId, version }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport, { targetId, version }))),
const cmd: Command<Input, Result> = createCommand('installMcu', ({ devicePath }) =>
fromPromise(withDevice(devicePath)(transport => installMcu(transport))),
)
export default cmd

4
src/commands/installOsuFirmware.js

@ -6,12 +6,12 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import { withDevice } from 'helpers/deviceAccess'
import installOsuFirmware from 'helpers/firmware/installOsuFirmware'
import type { LedgerScriptParams } from 'helpers/common'
import type { Firmware } from 'components/modals/UpdateFirmware'
type Input = {
devicePath: string,
targetId: string | number,
firmware: LedgerScriptParams,
firmware: Firmware,
}
type Result = *

15
src/commands/listAppVersions.js

@ -0,0 +1,15 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import listAppVersions from 'helpers/apps/listAppVersions'
type Result = *
const cmd: Command<DeviceInfo, Result> = createCommand('listAppVersions', deviceInfo =>
fromPromise(listAppVersions(deviceInfo)),
)
export default cmd

11
src/commands/listApps.js

@ -5,17 +5,10 @@ import { fromPromise } from 'rxjs/observable/fromPromise'
import listApps from 'helpers/apps/listApps'
type Input = {
targetId: string | number,
fullVersion: string,
provider: number,
}
type Input = {}
type Result = *
const cmd: Command<Input, Result> = createCommand(
'listApps',
({ targetId, fullVersion, provider }) => fromPromise(listApps(targetId, fullVersion, provider)),
)
const cmd: Command<Input, Result> = createCommand('listApps', () => fromPromise(listApps()))
export default cmd

16
src/commands/listCategories.js

@ -0,0 +1,16 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { fromPromise } from 'rxjs/observable/fromPromise'
import listCategories from 'helpers/apps/listCategories'
type Input = {}
type Result = *
const cmd: Command<Input, Result> = createCommand('listCategories', () =>
fromPromise(listCategories()),
)
export default cmd

125
src/components/AccountPage/AccountBalanceSummaryHeader.js

@ -0,0 +1,125 @@
// @flow
import React, { PureComponent } from 'react'
import { createStructuredSelector } from 'reselect'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import { saveSettings } from 'actions/settings'
import { accountSelector } from 'reducers/accounts'
import { counterValueCurrencySelector, selectedTimeRangeSelector } from 'reducers/settings'
import type { TimeRange } from 'reducers/settings'
import {
BalanceTotal,
BalanceSinceDiff,
BalanceSincePercent,
} from 'components/BalanceSummary/BalanceInfos'
import Box from 'components/base/Box'
import FormattedVal from 'components/base/FormattedVal'
import PillsDaysCount from 'components/PillsDaysCount'
type OwnProps = {
isAvailable: boolean,
totalBalance: number,
sinceBalance: number,
refBalance: number,
accountId: string, // eslint-disable-line
}
type Props = OwnProps & {
counterValue: Currency,
t: T,
account: Account,
saveSettings: ({ selectedTimeRange: TimeRange }) => *,
selectedTimeRange: TimeRange,
}
const mapStateToProps = createStructuredSelector({
account: accountSelector,
counterValue: counterValueCurrencySelector,
selectedTimeRange: selectedTimeRangeSelector,
})
const mapDispatchToProps = {
saveSettings,
}
class AccountBalanceSummaryHeader extends PureComponent<Props> {
handleChangeSelectedTime = item => {
this.props.saveSettings({ selectedTimeRange: item.key })
}
render() {
const {
account,
t,
counterValue,
selectedTimeRange,
isAvailable,
totalBalance,
sinceBalance,
refBalance,
} = this.props
return (
<Box flow={4} mb={2}>
<Box horizontal>
<BalanceTotal
showCryptoEvenIfNotAvailable
isAvailable={isAvailable}
totalBalance={account.balance}
unit={account.unit}
>
<FormattedVal
animateTicker
disableRounding
alwaysShowSign={false}
color="warmGrey"
unit={counterValue.units[0]}
fontSize={6}
showCode
val={totalBalance}
/>
</BalanceTotal>
<Box>
<PillsDaysCount selected={selectedTimeRange} onChange={this.handleChangeSelectedTime} />
</Box>
</Box>
<Box horizontal justifyContent="center" flow={7}>
<BalanceSincePercent
isAvailable={isAvailable}
t={t}
alignItems="center"
totalBalance={totalBalance}
sinceBalance={sinceBalance}
refBalance={refBalance}
since={selectedTimeRange}
/>
<BalanceSinceDiff
isAvailable={isAvailable}
t={t}
counterValue={counterValue}
alignItems="center"
totalBalance={totalBalance}
sinceBalance={sinceBalance}
refBalance={refBalance}
since={selectedTimeRange}
/>
</Box>
</Box>
)
}
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(), // FIXME t() is not even needed directly here. should be underlying component responsability to inject it
)(AccountBalanceSummaryHeader)

100
src/components/AccountPage/AccountHeaderActions.js

@ -0,0 +1,100 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import styled from 'styled-components'
import type { Account } from '@ledgerhq/live-common/lib/types'
import Tooltip from 'components/base/Tooltip'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants'
import type { T } from 'types/common'
import { rgba } from 'styles/helpers'
import { openModal } from 'reducers/modals'
import IconAccountSettings from 'icons/AccountSettings'
import IconReceive from 'icons/Receive'
import IconSend from 'icons/Send'
import Box, { Tabbable } from 'components/base/Box'
import Button from 'components/base/Button'
const ButtonSettings = styled(Tabbable).attrs({
cursor: 'pointer',
align: 'center',
justify: 'center',
borderRadius: 1,
})`
width: 40px;
height: 40px;
&:hover {
color: ${p => (p.disabled ? '' : p.theme.colors.dark)};
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.2))};
}
&:active {
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.3))};
}
`
const mapStateToProps = null
const mapDispatchToProps = {
openModal,
}
type OwnProps = {
account: Account,
}
type Props = OwnProps & {
t: T,
openModal: Function,
}
class AccountHeaderActions extends PureComponent<Props> {
render() {
const { account, openModal, t } = this.props
return (
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}>
{account.operations.length > 0 && (
<Fragment>
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconSend size={12} />
<Box>{t('app:send.title')}</Box>
</Box>
</Button>
<Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:receive.title')}</Box>
</Box>
</Button>
</Fragment>
)}
<Tooltip render={() => t('app:account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Box justifyContent="center">
<IconAccountSettings size={16} />
</Box>
</ButtonSettings>
</Tooltip>
</Box>
)
}
}
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(AccountHeaderActions)

8
src/components/AccountPage/EmptyStateAccount.js

@ -41,16 +41,16 @@ class EmptyStateAccount extends PureComponent<Props, *> {
/>
<Box mt={5} alignItems="center">
<Title>{t('app:account.emptyState.title')}</Title>
<Description>
<Description mt={3} style={{ display: 'block' }}>
<Trans i18nKey="app:account.emptyState.desc">
Make sure the
{'Make sure the'}
<Text ff="Open Sans|SemiBold" color="dark">
{account.currency.name}
</Text>
app is installed to receive funds.
{'app is installed to receive funds.'}
</Trans>
</Description>
<Button mt={3} padded primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Button mt={5} padded primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:account.emptyState.buttons.receiveFunds')}</Box>

164
src/components/AccountPage/index.js

@ -5,18 +5,8 @@ import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import { Redirect } from 'react-router'
import styled from 'styled-components'
import type { Currency, Account } from '@ledgerhq/live-common/lib/types'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
import Tooltip from 'components/base/Tooltip'
import { MODAL_SEND, MODAL_RECEIVE, MODAL_SETTINGS_ACCOUNT } from 'config/constants'
import type { T } from 'types/common'
import { rgba } from 'styles/helpers'
import { saveSettings } from 'actions/settings'
import { accountSelector } from 'reducers/accounts'
import {
counterValueCurrencySelector,
@ -25,47 +15,19 @@ import {
timeRangeDaysByKey,
} from 'reducers/settings'
import type { TimeRange } from 'reducers/settings'
import { openModal } from 'reducers/modals'
import IconAccountSettings from 'icons/AccountSettings'
import IconReceive from 'icons/Receive'
import IconSend from 'icons/Send'
import TrackPage from 'analytics/TrackPage'
import SyncOneAccountOnMount from 'components/SyncOneAccountOnMount'
import BalanceSummary from 'components/BalanceSummary'
import {
BalanceTotal,
BalanceSinceDiff,
BalanceSincePercent,
} from 'components/BalanceSummary/BalanceInfos'
import Box, { Tabbable } from 'components/base/Box'
import Button from 'components/base/Button'
import FormattedVal from 'components/base/FormattedVal'
import PillsDaysCount from 'components/PillsDaysCount'
import Box from 'components/base/Box'
import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop'
import AccountHeader from './AccountHeader'
import AccountHeaderActions from './AccountHeaderActions'
import AccountBalanceSummaryHeader from './AccountBalanceSummaryHeader'
import EmptyStateAccount from './EmptyStateAccount'
const ButtonSettings = styled(Tabbable).attrs({
cursor: 'pointer',
align: 'center',
justify: 'center',
borderRadius: 1,
})`
width: 40px;
height: 40px;
&:hover {
color: ${p => (p.disabled ? '' : p.theme.colors.dark)};
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.2))};
}
&:active {
background: ${p => (p.disabled ? '' : rgba(p.theme.colors.fog, 0.3))};
}
`
const mapStateToProps = (state, props) => ({
account: accountSelector(state, { accountId: props.match.params.id }),
counterValue: counterValueCurrencySelector(state),
@ -73,69 +35,54 @@ const mapStateToProps = (state, props) => ({
selectedTimeRange: selectedTimeRangeSelector(state),
})
const mapDispatchToProps = {
openModal,
saveSettings,
}
const mapDispatchToProps = null
type Props = {
counterValue: Currency,
t: T,
account?: Account,
openModal: Function,
saveSettings: ({ selectedTimeRange: TimeRange }) => *,
selectedTimeRange: TimeRange,
}
class AccountPage extends PureComponent<Props> {
handleChangeSelectedTime = item => {
this.props.saveSettings({ selectedTimeRange: item.key })
renderBalanceSummaryHeader = ({ isAvailable, totalBalance, sinceBalance, refBalance }) => {
const { account } = this.props
if (!account) return null
return (
<AccountBalanceSummaryHeader
accountId={account.id}
isAvailable={isAvailable}
totalBalance={totalBalance}
sinceBalance={sinceBalance}
refBalance={refBalance}
/>
)
}
_cacheBalance = null
render() {
const { account, openModal, t, counterValue, selectedTimeRange } = this.props
const { account, t, counterValue, selectedTimeRange } = this.props
const daysCount = timeRangeDaysByKey[selectedTimeRange]
// Don't even throw if we jumped in wrong account route
if (!account) {
return <Redirect to="/" />
}
return (
// Force re-render account page, for avoid animation
// `key` forces re-render account page when going an another account (skip animations)
<Box key={account.id}>
<TrackPage
category="Account"
currency={account.currency.id}
operationsLength={account.operations.length}
/>
<SyncOneAccountOnMount priority={10} accountId={account.id} />
<Box horizontal mb={5} flow={4}>
<AccountHeader account={account} />
<Box horizontal alignItems="center" justifyContent="flex-end" flow={2}>
{account.operations.length > 0 && (
<Fragment>
<Button small primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconSend size={12} />
<Box>{t('app:send.title')}</Box>
</Box>
</Button>
<Button small primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={1} alignItems="center">
<IconReceive size={12} />
<Box>{t('app:receive.title')}</Box>
</Box>
</Button>
</Fragment>
)}
<Tooltip render={() => t('app:account.settings.title')}>
<ButtonSettings onClick={() => openModal(MODAL_SETTINGS_ACCOUNT, { account })}>
<Box justifyContent="center">
<IconAccountSettings size={16} />
</Box>
</ButtonSettings>
</Tooltip>
</Box>
<AccountHeaderActions account={account} />
</Box>
{account.operations.length > 0 ? (
<Fragment>
<Box mb={7}>
@ -146,59 +93,12 @@ class AccountPage extends PureComponent<Props> {
counterValue={counterValue}
daysCount={daysCount}
selectedTimeRange={selectedTimeRange}
renderHeader={({ isAvailable, totalBalance, sinceBalance, refBalance }) => (
<Box flow={4} mb={2}>
<Box horizontal>
<BalanceTotal
showCryptoEvenIfNotAvailable
isAvailable={isAvailable}
totalBalance={account.balance}
unit={account.unit}
>
<FormattedVal
animateTicker
disableRounding
alwaysShowSign={false}
color="warmGrey"
unit={counterValue.units[0]}
fontSize={6}
showCode
val={totalBalance}
/>
</BalanceTotal>
<Box>
<PillsDaysCount
selected={selectedTimeRange}
onChange={this.handleChangeSelectedTime}
/>
</Box>
</Box>
<Box horizontal justifyContent="center" flow={7}>
<BalanceSincePercent
isAvailable={isAvailable}
t={t}
alignItems="center"
totalBalance={totalBalance}
sinceBalance={sinceBalance}
refBalance={refBalance}
since={selectedTimeRange}
/>
<BalanceSinceDiff
isAvailable={isAvailable}
t={t}
counterValue={counterValue}
alignItems="center"
totalBalance={totalBalance}
sinceBalance={sinceBalance}
refBalance={refBalance}
since={selectedTimeRange}
/>
</Box>
</Box>
)}
renderHeader={this.renderBalanceSummaryHeader}
/>
</Box>
<OperationsList account={account} title={t('app:account.lastOperations')} />
<StickyBackToTop />
</Fragment>
) : (

1
src/components/BalanceSummary/index.js

@ -35,6 +35,7 @@ const BalanceSummary = ({
selectedTimeRange,
}: Props) => {
const account = accounts.length === 1 ? accounts[0] : undefined
// FIXME This nesting 😱
return (
<Card p={0} py={5}>
<CalculateBalance accounts={accounts} daysCount={daysCount}>

10
src/components/CalculateBalance.js

@ -32,6 +32,7 @@ type Props = OwnProps & {
balanceStart: number,
balanceEnd: number,
isAvailable: boolean,
hash: string,
}
const mapStateToProps = (state: State, props: OwnProps) => {
@ -71,19 +72,20 @@ const mapStateToProps = (state: State, props: OwnProps) => {
({ ...item, originalValue: originalValues[i] || 0 }),
)
const balanceEnd = balanceHistory[balanceHistory.length - 1].value
return {
isAvailable,
balanceHistory,
balanceStart: balanceHistory[0].value,
balanceEnd: balanceHistory[balanceHistory.length - 1].value,
balanceEnd,
hash: `${balanceHistory.length}_${balanceEnd}`,
}
}
const hash = ({ balanceHistory, balanceEnd }) => `${balanceHistory.length}_${balanceEnd}`
class CalculateBalance extends Component<Props> {
shouldComponentUpdate(nextProps) {
return hash(nextProps) !== hash(this.props)
return nextProps.hash !== this.props.hash
}
render() {
const { children } = this.props

7
src/components/ConfettiParty/Confetti.js

@ -1,5 +1,9 @@
// @flow
import React, { PureComponent } from 'react'
import Animated from 'animated/lib/targets/react-dom'
import Easing from 'animated/lib/Easing'
const easing = Easing.bezier(0.0, 0.3, 1, 1)
class Confetti extends PureComponent<
{
@ -13,7 +17,7 @@ class Confetti extends PureComponent<
delta: [number, number],
},
{
value: *,
progress: Animated.Value,
},
> {
state = {
@ -24,6 +28,7 @@ class Confetti extends PureComponent<
Animated.timing(this.state.progress, {
toValue: 1,
duration,
easing,
}).start()
}
render() {

82
src/components/ConfettiParty/index.js

@ -1,3 +1,5 @@
// @flow
import React, { PureComponent } from 'react'
import { i } from 'helpers/staticPath'
import Confetti from './Confetti'
@ -9,22 +11,80 @@ const shapes = [
i('confetti-shapes/4.svg'),
]
class ConfettiParty extends PureComponent<{}> {
state = {
confettis: Array(64)
.fill(null)
.map((_, i) => ({
id: i,
let id = 1
const nextConfetti = (mode: ?string) =>
mode === 'emit'
? {
id: id++,
shape: shapes[Math.floor(shapes.length * Math.random())],
initialRotation: 360 * Math.random(),
initialYPercent: -0.2 + 0.1 * Math.random(),
initialYPercent: -0.05,
initialXPercent:
0.5 + 0.5 * Math.cos(Date.now() / 1000) * (0.5 + 0.5 * Math.sin(Date.now() / 6000)),
initialScale: 1,
rotations: 4 * Math.random() - 2,
delta: [(Math.random() - 0.5) * 200, 600 + 200 * Math.random()],
duration: 10000,
}
: {
id: id++,
shape: shapes[Math.floor(shapes.length * Math.random())],
initialRotation: 360 * Math.random(),
initialYPercent: -0.04 + -0.25 * Math.random(),
initialXPercent: 0.2 + 0.6 * Math.random(),
initialScale: 1,
rotations: 4 + 4 * Math.random(),
delta: [(Math.random() - 0.5) * 600, 300 + 300 * Math.random()],
duration: 6000 + 5000 * Math.random(),
})),
rotations: 8 * Math.random() - 4,
delta: [(Math.random() - 0.5) * 1500, 500 + 500 * Math.random()],
duration: 12000 + 8000 * Math.random(),
}
class ConfettiParty extends PureComponent<{ emit: boolean }, { confettis: Array<Object> }> {
state = {
// $FlowFixMe
confettis: Array(100)
.fill(null)
.map(nextConfetti),
}
componentDidMount() {
this.setEmit(this.props.emit)
this.initialTimeout = setTimeout(() => {
clearInterval(this.initialInterval)
}, 10000)
this.initialInterval = setInterval(() => {
this.setState(({ confettis }) => ({
confettis: confettis.slice(confettis.length > 200 ? 1 : 0).concat(nextConfetti()),
}))
}, 100)
}
componentDidUpdate(prevProps: *) {
if (this.props.emit !== prevProps.emit) {
this.setEmit(this.props.emit)
}
}
componentWillUnmount() {
this.setEmit(false)
clearInterval(this.initialInterval)
clearTimeout(this.initialTimeout)
}
setEmit(on: boolean) {
if (on) {
this.interval = setInterval(() => {
this.setState(({ confettis }) => ({
confettis: confettis.slice(confettis.length > 200 ? 1 : 0).concat(nextConfetti('emit')),
}))
}, 40)
} else {
clearInterval(this.interval)
}
}
interval: *
initialInterval: *
initialTimeout: *
render() {
const { confettis } = this.state

53
src/components/CurrentAddress/index.js

@ -16,7 +16,7 @@ import Box from 'components/base/Box'
import CopyToClipboard from 'components/base/CopyToClipboard'
import QRCode from 'components/base/QRCode'
import IconCheck from 'icons/Check'
import IconRecheck from 'icons/Recover'
import IconCopy from 'icons/Copy'
import IconInfoCircle from 'icons/InfoCircle'
import IconShield from 'icons/Shield'
@ -27,26 +27,34 @@ const Container = styled(Box).attrs({
bg: p =>
p.withQRCode ? (p.notValid ? rgba(p.theme.colors.alertRed, 0.02) : 'lightGrey') : 'transparent',
py: 4,
px: 7,
px: 5,
})`
border: ${p => (p.notValid ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.5)}` : 'none')};
`
const Address = styled(Box).attrs({
bg: p => (p.notValid ? 'transparent' : p.withQRCode ? 'white' : 'lightGrey'),
bg: 'white',
borderRadius: 1,
color: 'dark',
ff: 'Open Sans|SemiBold',
fontSize: 4,
mt: 2,
px: p => (p.notValid ? 0 : 4),
py: p => (p.notValid ? 0 : 3),
px: 4,
py: 3,
relative: true,
})`
border: ${p => (p.notValid ? 'none' : `1px dashed ${p.theme.colors.fog}`)};
border: ${p => `1px dashed ${p.theme.colors.fog}`};
cursor: text;
user-select: text;
`
const CopyFeedback = styled(Box).attrs({
sticky: true,
bg: 'white',
align: 'center',
justify: 'center',
})``
const Label = styled(Box).attrs({
alignItems: 'center',
color: 'graphite',
@ -77,10 +85,11 @@ const FooterButtonWrapper = styled(Box).attrs({
alignItems: 'center',
justifyContent: 'center',
borderRadius: 1,
px: 2,
})`
line-height: 1;
cursor: pointer;
height: 55px;
width: 55px;
&:hover {
background-color: ${p => rgba(p.theme.colors.wallet, 0.1)};
@ -131,7 +140,7 @@ type Props = {
withVerify: boolean,
}
class CurrentAddress extends PureComponent<Props> {
class CurrentAddress extends PureComponent<Props, { copyFeedback: boolean }> {
static defaultProps = {
addressVerified: null,
amount: null,
@ -145,6 +154,12 @@ class CurrentAddress extends PureComponent<Props> {
withVerify: false,
}
state = {
copyFeedback: false,
}
_isUnmounted = false
render() {
const {
account: { name: accountName, currency },
@ -163,6 +178,8 @@ class CurrentAddress extends PureComponent<Props> {
...props
} = this.props
const { copyFeedback } = this.state
const notValid = addressVerified === false
return (
@ -193,6 +210,7 @@ class CurrentAddress extends PureComponent<Props> {
<IconInfoCircle size={12} />
</Label>
<Address withQRCode={withQRCode} notValid={notValid}>
{copyFeedback && <CopyFeedback>{t('app:common.addressCopied')}</CopyFeedback>}
{address}
</Address>
{withBadge && (
@ -207,11 +225,26 @@ class CurrentAddress extends PureComponent<Props> {
)}
{withFooter && (
<Footer>
<FooterButton icon={<IconCheck size={16} />} label="Verify" onClick={onVerify} />
<FooterButton
icon={<IconRecheck size={16} />}
label={notValid ? t('app:common.verify') : t('app:common.reverify')}
onClick={onVerify}
/>
<CopyToClipboard
data={address}
render={copy => (
<FooterButton icon={<IconCopy size={16} />} label="Copy" onClick={copy} />
<FooterButton
icon={<IconCopy size={16} />}
label={t('app:common.copy')}
onClick={() => {
this.setState({ copyFeedback: true })
setTimeout(() => {
if (this._isUnmounted) return
this.setState({ copyFeedback: false })
}, 1e3)
copy()
}}
/>
)}
/>
</Footer>

15
src/components/DashboardPage/EmptyState.js

@ -46,14 +46,16 @@ class EmptyState extends PureComponent<Props, *> {
/>
<Box mt={5} alignItems="center">
<Title>{t('app:emptyState.dashboard.title')}</Title>
<Description>{t('app:emptyState.dashboard.desc')}</Description>
<Box mt={3} horizontal justifyContent="space-around" style={{ width: 300 }}>
<Description mt={3} style={{ maxWidth: 600 }}>
{t('app:emptyState.dashboard.desc')}
</Description>
<Box mt={5} horizontal style={{ width: 300 }} flow={3} justify="center">
<Button padded primary style={{ minWidth: 120 }} onClick={this.handleInstallApp}>
{t('app:emptyState.dashboard.buttons.installApp')}
</Button>
<Button
padded
primary
outline
style={{ minWidth: 120 }}
onClick={() => openModal(MODAL_ADD_ACCOUNTS)}
>
@ -76,10 +78,9 @@ export const Description = styled(Box).attrs({
ff: 'Open Sans|Regular',
fontSize: 4,
color: p => p.theme.colors.graphite,
})`
margin: 10px auto 25px;
display: block;
`
textAlign: 'center',
})``
export default compose(
connect(
null,

19
src/components/DashboardPage/index.js

@ -1,6 +1,7 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import uniq from 'lodash/uniq'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
@ -26,6 +27,7 @@ import type { TimeRange } from 'reducers/settings'
import { reorderAccounts } from 'actions/accounts'
import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage'
import UpdateNotifier from 'components/UpdateNotifier'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
@ -92,12 +94,20 @@ class DashboardPage extends PureComponent<Props> {
const timeFrame = this.handleGreeting()
const imagePath = i('empty-account-tile.svg')
const totalAccounts = accounts.length
const totalCurrencies = uniq(accounts.map(a => a.currency.id)).length
const totalOperations = accounts.reduce((sum, a) => sum + a.operations.length, 0)
const displayOperationsHelper = (account: Account) => account.operations.length > 0
const displayOperations = accounts.some(displayOperationsHelper)
return (
<Fragment>
<UpdateNotifier />
<TrackPage
category="Portfolio"
totalAccounts={totalAccounts}
totalOperations={totalOperations}
totalCurrencies={totalCurrencies}
/>
<Box flow={7}>
{totalAccounts > 0 ? (
<Fragment>
@ -182,13 +192,17 @@ class DashboardPage extends PureComponent<Props> {
/>
) : (
<Wrapper>
<img alt="" src={imagePath} />
<Box mt={2}>
<img alt="" src={imagePath} />
</Box>
<Box
ff="Open Sans"
fontSize={3}
color="graphite"
color="grey"
pb={2}
mt={3}
textAlign="center"
style={{ maxWidth: 150 }}
>
{t('app:dashboard.emptyAccountTile.desc')}
</Box>
@ -237,4 +251,5 @@ const Wrapper = styled(Box).attrs({
})`
border: 1px dashed ${p => p.theme.colors.fog};
border-radius: 4px;
height: 215px;
`

321
src/components/DeviceConnect/index.js

@ -1,321 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { Trans, translate } from 'react-i18next'
import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { T, Device } from 'types/common'
import noop from 'lodash/noop'
import Box from 'components/base/Box'
import Spinner from 'components/base/Spinner'
import CryptoCurrencyIcon from 'components/CryptoCurrencyIcon'
import TranslatedError from 'components/TranslatedError'
import IconCheck from 'icons/Check'
import IconExclamationCircle from 'icons/ExclamationCircle'
import IconInfoCircle from 'icons/InfoCircle'
import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import * as IconDevice from 'icons/device'
// TODO: CHECK IF COMPONENT CAN BE REMOVED
const Step = styled(Box).attrs({
borderRadius: 1,
justifyContent: 'center',
fontSize: 4,
})`
border: 1px solid
${p =>
p.validated
? p.theme.colors.wallet
: p.hasErrors
? p.theme.colors.alertRed
: p.theme.colors.fog};
`
const StepIcon = styled(Box).attrs({
alignItems: 'center',
justifyContent: 'center',
})`
width: 64px;
`
const StepContent = styled(Box).attrs({
color: 'dark',
horizontal: true,
alignItems: 'center',
})`
height: 60px;
line-height: 1.2;
strong {
font-weight: 600;
}
`
const ListDevices = styled(Box).attrs({
p: 3,
pt: 1,
flow: 2,
})``
const DeviceItem = styled(Box).attrs({
bg: 'lightGrey',
borderRadius: 1,
alignItems: 'center',
color: 'dark',
ff: 'Open Sans|SemiBold',
fontSize: 4,
horizontal: true,
pr: 3,
pl: 0,
})`
cursor: pointer;
height: 54px;
`
const DeviceIcon = styled(Box).attrs({
alignItems: 'center',
justifyContent: 'center',
color: 'graphite',
})`
width: 55px;
`
const DeviceSelected = styled(Box).attrs({
alignItems: 'center',
bg: p => (p.selected ? 'wallet' : 'white'),
color: 'white',
justifyContent: 'center',
})`
border-radius: 50%;
border: 1px solid ${p => (p.selected ? p.theme.colors.wallet : p.theme.colors.fog)};
height: 18px;
width: 18px;
`
const WrapperIconCurrency = styled(Box).attrs({
alignItems: 'center',
justifyContent: 'center',
})`
border: 1px solid ${p => p.theme.colors[p.color]};
border-radius: 8px;
height: 24px;
width: 24px;
`
const Info = styled(Box).attrs({
alignItems: 'center',
color: p => (p.hasErrors ? 'alertRed' : 'grey'),
flow: 2,
fontSize: 3,
horizontal: true,
ml: 1,
})`
strong {
font-weight: 600;
}
`
const StepCheck = ({ checked, hasErrors }: { checked: boolean, hasErrors?: boolean }) => (
<Box pr={5}>
{checked ? (
<Box color="wallet">
<IconCheck size={16} />
</Box>
) : hasErrors ? (
<Box color="alertRed">
<IconExclamationCircle size={16} />
</Box>
) : (
<Spinner color="grey" size={16} />
)}
</Box>
)
StepCheck.defaultProps = {
hasErrors: false,
}
type Props = {
appOpened: null | 'success' | 'fail',
genuineCheckStatus: null | 'success' | 'fail',
withGenuineCheck: boolean,
currency: CryptoCurrency,
devices: Device[],
deviceSelected: ?Device,
onChangeDevice: Device => void,
t: T,
error: ?Error,
}
const emitChangeDevice = props => {
const { onChangeDevice, deviceSelected, devices } = props
if (deviceSelected === null && devices.length > 0) {
onChangeDevice(devices[0])
}
}
class DeviceConnect extends PureComponent<Props> {
static defaultProps = {
accountName: null,
appOpened: null,
devices: [],
deviceSelected: null,
onChangeDevice: noop,
withGenuineCheck: false,
}
componentDidMount() {
emitChangeDevice(this.props)
}
componentWillReceiveProps(nextProps) {
emitChangeDevice(nextProps)
}
getStepState = stepStatus => ({
success: stepStatus === 'success',
fail: stepStatus === 'fail',
})
render() {
const {
deviceSelected,
genuineCheckStatus,
withGenuineCheck,
appOpened,
error,
currency,
t,
onChangeDevice,
devices,
} = this.props
const appState = this.getStepState(appOpened)
const genuineCheckState = this.getStepState(genuineCheckStatus)
const hasDevice = devices.length > 0
const hasMultipleDevices = devices.length > 1
// TODO: place custom wording in trans tags into yml file
/* eslint-disable react/jsx-no-literals */
return (
<Box flow={4} ff="Open Sans">
<Step validated={hasDevice}>
<StepContent>
<StepIcon>
<IconUsb size={36} />
</StepIcon>
<Box grow shrink>
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
Connect and unlock your <strong>Ledger device</strong>
</Trans>
</Box>
<StepCheck checked={hasDevice} />
</StepContent>
{hasMultipleDevices && (
<ListDevices>
<Box color="graphite" fontSize={3}>
{t('app:deviceConnect.step1.choose', { count: devices.length })}
</Box>
<Box flow={2}>
{devices.map(d => {
const Icon = IconDevice[d.product.replace(/\s/g, '')]
return (
<DeviceItem key={d.path} onClick={() => onChangeDevice(d)}>
<DeviceIcon>
<Icon size={28} />
</DeviceIcon>
<Box grow noShrink>
{`${d.manufacturer} ${d.product}`}
</Box>
<Box>
<DeviceSelected selected={d === deviceSelected}>
<IconCheck size={10} />
</DeviceSelected>
</Box>
</DeviceItem>
)
})}
</Box>
</ListDevices>
)}
</Step>
<Step validated={appState.success} hasErrors={appState.fail}>
{currency ? (
<StepContent>
<StepIcon>
<WrapperIconCurrency>
<CryptoCurrencyIcon currency={currency} size={12} />
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:step2.open" parent="div">
{'Open the '}
<strong>{currency.name}</strong>
{' app on your device'}
</Trans>
</Box>
<StepCheck checked={appState.success} hasErrors={appState.fail} />
</StepContent>
) : (
<StepContent>
<StepIcon>
<WrapperIconCurrency>
<IconHome size={12} />
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="app:dashboard.open" parent="div">
{'Navigate to the '}
<strong>{'dashboard'}</strong>
{' on your device'}
</Trans>
</Box>
<StepCheck checked={appState.success} hasErrors={appState.fail} />
</StepContent>
)}
</Step>
{/* GENUINE CHECK */}
{/* ------------- */}
{withGenuineCheck && (
<Step validated={genuineCheckState.success} hasErrors={genuineCheckState.fail}>
<StepContent>
<StepIcon>
<WrapperIconCurrency>
<IconCheck size={12} />
</WrapperIconCurrency>
</StepIcon>
<Box grow shrink>
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Allow the '}
<strong>{'Ledger Manager'}</strong>
{' on your device'}
</Trans>
</Box>
<StepCheck checked={genuineCheckState.success} hasErrors={genuineCheckState.fail} />
</StepContent>
</Step>
)}
{error ? (
<Info hasErrors>
<IconInfoCircle size={12} />
<Box shrink selectable>
<TranslatedError error={error} />
</Box>
</Info>
) : null}
</Box>
)
}
}
export default translate()(DeviceConnect)

64
src/components/DeviceConnect/stories.js

@ -1,64 +0,0 @@
// @flow
import React from 'react'
import { storiesOf } from '@storybook/react'
import { select } from '@storybook/addon-knobs'
import { action } from '@storybook/addon-actions'
import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies'
import { listCryptoCurrencies } from 'config/cryptocurrencies'
import type { Currency } from '@ledgerhq/live-common/lib/types'
import DeviceConnect from 'components/DeviceConnect'
const currencies = listCryptoCurrencies().map(c => c.id)
const stories = storiesOf('Components', module)
const devices = [
{
manufacturer: 'Ledger',
product: 'Nano S',
vendorId: '1',
productId: '11',
path: '111',
},
{
manufacturer: 'Ledger',
product: 'Blue',
vendorId: '2',
productId: '22',
path: '222',
},
{
manufacturer: 'Ledger',
product: 'Nano S',
vendorId: '3',
productId: '33',
path: '333',
},
]
stories.add('DeviceConnect', () => (
<Wrapper currencyId={select('currencyId', currencies, 'bitcoin_testnet')}>
{({ currency }) => (
<DeviceConnect
currency={currency}
appOpened={select('appOpened', ['', 'success', 'fail'], '')}
devices={devices.slice(0, Number(select('devices', [0, 1, 2, 3], '0')))}
deviceSelected={devices[select('deviceSelected', ['', 0, 1, 2], '')] || null}
onChangeDevice={action('onChangeDevice')}
/>
)}
</Wrapper>
))
function Wrapper({
currencyId,
children,
}: {
currencyId: string,
children: (props: { currency: Currency }) => any,
}) {
const currency = getCryptoCurrencyById(currencyId)
return children({ currency })
}

203
src/components/DeviceInteraction/DeviceInteractionStep.js

@ -0,0 +1,203 @@
// @flow
import React, { PureComponent } from 'react'
import Box from 'components/base/Box'
import { delay } from 'helpers/promise'
import {
DeviceInteractionStepContainer,
SpinnerContainer,
IconContainer,
SuccessContainer,
ErrorContainer,
} from './components'
export type Step = {
id: string,
title?: React$Node | (Object => React$Node),
desc?: React$Node,
icon: React$Node,
run?: Object => Promise<any> | { promise: Promise<any>, unsubscribe: void => any },
render?: ({ onSuccess: Object => any, onFail: Error => void }, any) => React$Node,
minMs?: number,
}
type Status = 'idle' | 'running'
type Props = {
isFirst: boolean,
isLast: boolean,
isActive: boolean,
isFinished: boolean,
isPrecedentActive: boolean,
isError: boolean,
isSuccess: boolean,
isPassed: boolean,
step: Step,
onSuccess: (any, Step) => any,
onFail: (Error, Step) => any,
data: any,
}
class DeviceInteractionStep extends PureComponent<
Props,
{
status: Status,
},
> {
static defaultProps = {
data: {},
}
state = {
status: this.props.isFirst ? 'running' : 'idle',
}
componentDidMount() {
if (this.props.isFirst) {
this.run()
}
}
componentDidUpdate(prevProps: Props) {
const { isActive, isError } = this.props
const { status } = this.state
const didActivated = isActive && !prevProps.isActive
const didDeactivated = !isActive && prevProps.isActive
const stillActivated = isActive && prevProps.isActive
const didResetError = !isError && !!prevProps.isError
if (didActivated && status !== 'running') {
this.run()
}
if (didResetError && stillActivated) {
this.run()
}
if (didDeactivated && status === 'running') {
this.cancel()
}
}
componentWillUnmount() {
if (this._unsubscribe) {
this._unsubscribe()
}
this._unmounted = true
}
_unsubscribe = null
_unmounted = false
handleSuccess = (res: any) => {
const { onSuccess, step, isError } = this.props
if (isError) return
this.setState({ status: 'idle' })
onSuccess(res, step)
}
handleFail = (e: Error) => {
const { onFail, step } = this.props
this.setState({ status: 'idle' })
onFail(e, step)
}
run = async () => {
const { step, data } = this.props
const { status } = this.state
if (status !== 'running') {
this.setState({ status: 'running' })
}
if (!step.run) {
return
}
try {
const d1 = Date.now()
// $FlowFixMe JUST TESTED THE `run` 6 LINES BEFORE!!!
const res = (await step.run(data)) || {}
if (this._unmounted) return
if (step.minMs) {
const d2 = Date.now()
// $FlowFixMe SAME THING, JUST TESTED THE MINMS KEY, BUT EH
if (d2 - d1 < step.minMs) {
// $FlowFixMe nice type checking
await delay(step.minMs - (d2 - d1))
if (this._unmounted) return
}
}
if (res.promise) {
this._unsubscribe = res.unsubscribe
const realRes = await res.promise
if (this._unmounted) return
this.handleSuccess(realRes)
} else {
this.handleSuccess(res)
}
} catch (e) {
this.handleFail(e)
}
}
cancel = () => this.setState({ status: 'idle' })
render() {
const {
isFirst,
isLast,
isActive,
isFinished,
isPrecedentActive,
isSuccess,
isError,
isPassed,
step,
data,
} = this.props
const { status } = this.state
const title = typeof step.title === 'function' ? step.title(data) : step.title
const { render: CustomRender } = step
const isRunning = status === 'running'
return (
<DeviceInteractionStepContainer
isFirst={isFirst}
isLast={isLast}
isFinished={isFinished}
isSuccess={isSuccess}
isActive={isActive}
isPrecedentActive={isPrecedentActive}
isError={isError}
>
<IconContainer>{step.icon}</IconContainer>
<Box py={4} justify="center" grow shrink>
{title && (
<Box color={isActive && !isSuccess ? 'dark' : ''} ff="Open Sans|SemiBold">
{title}
</Box>
)}
{step.desc && step.desc}
{CustomRender && (
<CustomRender onSuccess={this.handleSuccess} onFail={this.handleFail} data={data} />
)}
</Box>
<div style={{ width: 70, position: 'relative', overflow: 'hidden', pointerEvents: 'none' }}>
<SpinnerContainer isVisible={isRunning} isPassed={isPassed} isError={isError} />
<ErrorContainer isVisible={isError} />
<SuccessContainer isVisible={isSuccess} />
</div>
</DeviceInteractionStepContainer>
)
}
}
export default DeviceInteractionStep

121
src/components/DeviceInteraction/components.js

@ -0,0 +1,121 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { radii } from 'styles/theme'
import { rgba } from 'styles/helpers'
import TranslatedError from 'components/TranslatedError'
import Box from 'components/base/Box'
import FakeLink from 'components/base/FakeLink'
import Spinner from 'components/base/Spinner'
import IconCheck from 'icons/Check'
import IconCross from 'icons/Cross'
import IconExclamationCircle from 'icons/ExclamationCircle'
export const DeviceInteractionStepContainer = styled(Box).attrs({
horizontal: true,
ff: 'Open Sans',
fontSize: 3,
bg: 'white',
color: 'graphite',
})`
position: relative;
z-index: ${p => (p.isActive ? 1 : '')};
max-width: 500px;
min-height: 80px;
border: 1px solid ${p => p.theme.colors.fog};
border-color: ${p =>
p.isError ? p.theme.colors.alertRed : p.isActive && !p.isFinished ? p.theme.colors.wallet : ''};
border-top-color: ${p => (p.isFirst || p.isActive ? '' : 'transparent')};
border-bottom-color: ${p => (p.isPrecedentActive ? 'transparent' : '')};
border-bottom-left-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)};
border-bottom-right-radius: ${p => (p.isLast ? `${radii[1]}px` : 0)};
border-top-left-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)};
border-top-right-radius: ${p => (p.isFirst ? `${radii[1]}px` : 0)};
box-shadow: ${p =>
p.isActive && !p.isSuccess
? `
${rgba(p.isError ? p.theme.colors.alertRed : p.theme.colors.wallet, 0.2)} 0 0 3px 2px
`
: 'none'};
`
export const IconContainer = ({ children }: { children: any }) => (
<Box align="center" justify="center" style={{ width: 70 }}>
{children}
</Box>
)
const SpinnerContainerWrapper = styled.div`
color: ${p => p.theme.colors.grey};
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
transition: 350ms cubic-bezier(0.62, 0.28, 0.39, 0.94);
transition-property: transform opacity;
opacity: ${p => (p.isVisible ? 1 : 0)};
transform: translate3d(0, ${p => (!p.isVisible ? -40 : 0)}px, 0);
`
export const SpinnerContainer = ({ isVisible }: { isVisible: boolean }) => (
<SpinnerContainerWrapper isVisible={isVisible}>
<Spinner size={16} />
</SpinnerContainerWrapper>
)
const SuccessContainerWrapper = styled(SpinnerContainerWrapper)`
color: ${p => p.theme.colors.wallet};
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0);
`
export const SuccessContainer = ({ isVisible }: { isVisible: boolean }) => (
<SuccessContainerWrapper isVisible={isVisible}>
<IconCheck size={16} />
</SuccessContainerWrapper>
)
const ErrorContainerWrapper = styled(SpinnerContainerWrapper)`
color: ${p => p.theme.colors.alertRed};
transform: translate3d(0, ${p => (!p.isVisible ? 40 : 0)}px, 0);
`
export const ErrorContainer = ({ isVisible }: { isVisible: boolean }) => (
<ErrorContainerWrapper isVisible={isVisible}>
<IconCross size={16} />
</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>
)

116
src/components/DeviceInteraction/index.js

@ -0,0 +1,116 @@
// @flow
import React, { PureComponent } from 'react'
import { delay } from 'helpers/promise'
import Box from 'components/base/Box'
import DeviceInteractionStep from './DeviceInteractionStep'
import { ErrorDescContainer } from './components'
import type { Step } from './DeviceInteractionStep'
type Props = {
steps: Step[],
onSuccess?: any => void,
onFail?: any => void,
waitBeforeSuccess?: number,
// when true and there is an error, display the error + retry button
shouldRenderRetry?: boolean,
}
type State = {
stepIndex: number,
isSuccess: boolean,
error: ?Error,
data: Object,
}
const INITIAL_STATE = {
stepIndex: 0,
isSuccess: false,
error: null,
data: {},
}
class DeviceInteraction extends PureComponent<Props, State> {
state = INITIAL_STATE
componentWillUnmount() {
this._unmounted = true
}
_unmounted = false
reset = () => this.setState(INITIAL_STATE)
handleSuccess = async (res: any, step: Step) => {
const { onSuccess, steps, waitBeforeSuccess } = this.props
const { stepIndex, data: prevData } = this.state
const isCurrentStep = step.id === steps[stepIndex].id
if (!isCurrentStep) {
return
}
const data = { ...prevData, [step.id]: res || true }
const isLast = stepIndex === steps.length - 1
if (isLast) {
if (!waitBeforeSuccess) {
onSuccess && onSuccess(data)
}
this.setState({ isSuccess: true, data })
if (waitBeforeSuccess) {
await delay(waitBeforeSuccess)
if (this._unmounted) return
onSuccess && onSuccess(data)
}
} else {
this.setState({ stepIndex: stepIndex + 1, data })
}
}
handleFail = (error: Error, step: Step) => {
const { steps, onFail } = this.props
const { stepIndex } = this.state
const isCurrentStep = step === steps[stepIndex]
if (!isCurrentStep) {
return
}
this.setState({ error })
onFail && onFail(error)
}
render() {
const { steps, shouldRenderRetry, ...props } = this.props
const { stepIndex, error, isSuccess, data } = this.state
return (
<Box {...props}>
{steps.map((step, i) => {
const isError = !!error && i === stepIndex
return (
<DeviceInteractionStep
key={step.id}
step={step}
isError={isError}
isFirst={i === 0}
isLast={i === steps.length - 1}
isPrecedentActive={i === stepIndex - 1}
isActive={i === stepIndex}
isPassed={i < stepIndex}
isSuccess={i < stepIndex || (i === stepIndex && isSuccess)}
isFinished={isSuccess}
onSuccess={this.handleSuccess}
onFail={this.handleFail}
data={data}
/>
)
})}
{error &&
shouldRenderRetry && <ErrorDescContainer error={error} onRetry={this.reset} mt={4} />}
</Box>
)
}
}
export default DeviceInteraction

82
src/components/DeviceInteraction/stories.js

@ -0,0 +1,82 @@
// @flow
import React, { Fragment } from 'react'
import styled from 'styled-components'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'
import DeviceInteraction from 'components/DeviceInteraction'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import IconUsb from 'icons/Usb'
const stories = storiesOf('Components', module)
stories.add('DeviceInteraction', () => <Wrapper />)
const MockIcon = styled.div`
width: ${p => p.size}px;
height: ${p => p.size}px;
background: ${p => p.theme.colors.lightFog};
border-radius: 50%;
`
const mockIcon = <MockIcon size={36} />
class Wrapper extends React.Component<any> {
_ref = null
handleReset = () => this._ref && this._ref.reset()
render() {
return (
<Fragment>
<button style={{ marginBottom: 40 }} onClick={this.handleReset}>
{'reset'}
</button>
<DeviceInteraction
shouldRenderRetry
ref={n => (this._ref = n)}
steps={[
{
id: 'deviceConnect',
title: 'Connect your device',
icon: <IconUsb size={36} />,
desc: 'If you dont connect your device, we wont be able to read on it',
render: ({ onSuccess, onFail }) => (
<Box p={2} bg="lightGrey" mt={2} borderRadius={1}>
<Box horizontal flow={2}>
<Button small primary onClick={() => onSuccess({ name: 'Nano S' })}>
{'Nano S'}
</Button>
<Button small primary onClick={() => onSuccess({ name: 'Blue' })}>
{'Blue'}
</Button>
<Button small danger onClick={onFail}>
{'make it fail'}
</Button>
</Box>
</Box>
),
},
{
id: 'deviceOpen',
title: ({ deviceConnect: device }) =>
`Open the Bitcoin application on your ${device ? `${device.name} ` : ''}device`,
desc: 'To be able to retriev your Bitcoins',
icon: mockIcon,
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)),
},
{
id: 'check',
title: 'Checking if all is alright...',
desc: 'This should take only 1 second...',
icon: mockIcon,
run: () => new Promise(resolve => setTimeout(resolve, 1 * 1000)),
},
]}
onSuccess={action('onSuccess')}
/>
</Fragment>
)
}
}

0
src/components/Workflow/EnsureDevice.js → src/components/EnsureDevice.js

310
src/components/EnsureDeviceApp.js

@ -1,153 +1,57 @@
// @flow
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import logger from 'logger'
import invariant from 'invariant'
import { isSegwitAccount } from 'helpers/bip32'
import React, { Component } from 'react'
import invariant from 'invariant'
import { connect } from 'react-redux'
import { Trans } from 'react-i18next'
import type { Account, CryptoCurrency } from '@ledgerhq/live-common/lib/types'
import type { Device } from 'types/common'
import { getCryptoCurrencyIcon } from '@ledgerhq/live-common/lib/react'
import { getDevices } from 'reducers/devices'
import type { State as StoreState } from 'reducers/index'
import logger from 'logger'
import getAddress from 'commands/getAddress'
import { createCancelablePolling } from 'helpers/promise'
import { standardDerivation } from 'helpers/derivations'
import isDashboardOpen from 'commands/isDashboardOpen'
import { createCustomErrorClass } from 'helpers/errors'
import { CHECK_APP_INTERVAL_WHEN_VALID, CHECK_APP_INTERVAL_WHEN_INVALID } from 'config/constants'
export const WrongAppOpened = createCustomErrorClass('WrongAppOpened')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
import { isSegwitAccount } from 'helpers/bip32'
import { BtcUnmatchedApp } from 'helpers/getAddressForCurrency/btc'
type OwnProps = {
currency?: ?CryptoCurrency,
deviceSelected: ?Device,
withGenuineCheck?: boolean,
account?: ?Account,
onStatusChange?: (DeviceStatus, AppStatus, ?string) => void,
onGenuineCheck?: (isGenuine: boolean) => void,
// TODO prefer children function
render?: ({
appStatus: AppStatus,
genuineCheckStatus: GenuineCheckStatus,
currency: ?CryptoCurrency,
devices: Device[],
deviceSelected: ?Device,
deviceStatus: DeviceStatus,
error: ?Error,
}) => React$Node,
}
import DeviceInteraction from 'components/DeviceInteraction'
import Text from 'components/base/Text'
type Props = OwnProps & {
devices: Device[],
}
import IconUsb from 'icons/Usb'
type DeviceStatus = 'unconnected' | 'connected'
import type { Device } from 'types/common'
type AppStatus = 'success' | 'fail' | 'progress'
import { createCustomErrorClass } from 'helpers/errors'
import { getCurrentDevice } from 'reducers/devices'
type GenuineCheckStatus = 'success' | 'fail' | 'progress'
export const WrongAppOpened = createCustomErrorClass('WrongAppOpened')
export const WrongDeviceForAccount = createCustomErrorClass('WrongDeviceForAccount')
type State = {
deviceStatus: DeviceStatus,
appStatus: AppStatus,
error: ?Error,
genuineCheckStatus: GenuineCheckStatus,
}
const usbIcon = <IconUsb size={36} />
const Bold = props => <Text ff="Open Sans|Bold" {...props} />
const mapStateToProps = (state: StoreState) => ({
devices: getDevices(state),
const mapStateToProps = state => ({
device: getCurrentDevice(state),
})
// TODO we want to split into <EnsureDeviceCurrency/> and <EnsureDeviceAccount/>
// and minimize the current codebase AF
class EnsureDeviceApp extends PureComponent<Props, State> {
state = {
appStatus: 'progress',
deviceStatus: this.props.deviceSelected ? 'connected' : 'unconnected',
error: null,
genuineCheckStatus: 'progress',
}
componentDidMount() {
if (this.props.deviceSelected !== null) {
this.checkAppOpened()
}
}
componentWillReceiveProps(nextProps) {
const { deviceStatus } = this.state
const { deviceSelected, devices } = this.props
const { devices: nextDevices, deviceSelected: nextDeviceSelected } = nextProps
if (deviceStatus === 'unconnected' && !deviceSelected && nextDeviceSelected) {
this.handleStatusChange('connected', 'progress')
}
if (deviceStatus !== 'unconnected' && devices !== nextDevices) {
const isConnected = nextDevices.find(d => d === nextDeviceSelected)
if (!isConnected) {
this.handleStatusChange('unconnected', 'progress')
}
}
}
componentDidUpdate(prevProps) {
const { deviceSelected } = this.props
const { deviceSelected: prevDeviceSelected } = prevProps
if (prevDeviceSelected !== deviceSelected) {
this.handleStatusChange('connected', 'progress')
// TODO: refacto to more generic/global way
clearTimeout(this._timeout)
this._timeout = setTimeout(this.checkAppOpened, 250)
}
}
componentWillUnmount() {
clearTimeout(this._timeout)
this._unmounted = true
}
checkAppOpened = async () => {
const { deviceSelected, account, currency, withGenuineCheck } = this.props
const { appStatus } = this.state
if (!deviceSelected) {
return
}
let isSuccess = true
try {
if (account || currency) {
const cur = account ? account.currency : currency
invariant(cur, 'currency is available')
const { address } = await getAddress
.send({
devicePath: deviceSelected.path,
currencyId: cur.id,
path: account
? account.freshAddressPath
: standardDerivation({ currency: cur, segwit: false, x: 0 }),
segwit: account ? isSegwitAccount(account) : false,
})
.toPromise()
.catch(e => {
if (
e &&
(e.name === 'TransportStatusError' ||
// we don't want these error to appear (caused by usb disconnect..)
e.message === 'could not read from HID device' ||
e.message === 'Cannot write to HID device')
) {
logger.log(e)
throw new WrongAppOpened(`WrongAppOpened ${cur.id}`, { currencyName: cur.name })
}
throw e
})
class EnsureDeviceApp extends Component<{
device: ?Device,
account?: ?Account,
currency?: ?CryptoCurrency,
}> {
connectInteractionHandler = () =>
createCancelablePolling(() => {
if (!this.props.device) return Promise.reject()
return Promise.resolve(this.props.device)
})
openAppInteractionHandler = ({ device }) =>
createCancelablePolling(
async () => {
const { account, currency: _currency } = this.props
const currency = account ? account.currency : _currency
invariant(currency, 'No currency given')
const address = await getAddressFromAccountOrCurrency(device, account, currency)
if (account) {
const { freshAddress } = account
if (account && freshAddress !== address) {
@ -157,82 +61,74 @@ class EnsureDeviceApp extends PureComponent<Props, State> {
})
}
}
} else {
logger.warn('EnsureDeviceApp for using dashboard is DEPRECATED !!!')
// TODO: FIXME REMOVE THIS ! should use EnsureDashboard dedicated component.
const isDashboard = isDashboardOpen.send({ devicePath: deviceSelected.path }).toPromise()
if (!isDashboard) {
throw new Error(`dashboard is not opened`)
}
}
this.handleStatusChange(this.state.deviceStatus, 'success')
if (withGenuineCheck && appStatus !== 'success') {
this.handleGenuineCheck()
}
} catch (e) {
this.handleStatusChange(this.state.deviceStatus, 'fail', e)
isSuccess = false
}
// TODO: refacto to more generic/global way
if (!this._unmounted) {
this._timeout = setTimeout(
this.checkAppOpened,
isSuccess ? CHECK_APP_INTERVAL_WHEN_VALID : CHECK_APP_INTERVAL_WHEN_INVALID,
)
}
}
_timeout: *
_unmounted = false
handleStatusChange = (deviceStatus, appStatus, error = null) => {
const { onStatusChange } = this.props
clearTimeout(this._timeout)
if (!this._unmounted) {
this.setState({ deviceStatus, appStatus, error })
onStatusChange && onStatusChange(deviceStatus, appStatus, error)
}
}
handleGenuineCheck = async () => {
// TODO: do a *real* genuine check
await sleep(1)
if (!this._unmounted) {
this.setState({ genuineCheckStatus: 'success' })
this.props.onGenuineCheck && this.props.onGenuineCheck(true)
}
return address
},
{
shouldThrow: (err: Error) => {
const isWrongApp = err instanceof BtcUnmatchedApp
const isWrongDevice = err instanceof WrongDeviceForAccount
return isWrongApp || isWrongDevice
},
},
)
renderOpenAppTitle = () => {
const { account, currency } = this.props
const cur = account ? account.currency : currency
invariant(cur, 'No currency given')
return (
<Trans i18nKey="deviceConnect:step2.open" parent="div">
{'Open the '}
<strong>{cur.name}</strong>
{' app on your device'}
</Trans>
)
}
render() {
const { currency, account, devices, deviceSelected, render } = this.props
const { appStatus, deviceStatus, genuineCheckStatus, error } = this.state
if (render) {
// if cur is not provided, we assume we want to check if user is on
// the dashboard
const cur = account ? account.currency : currency
return render({
appStatus,
currency: cur,
devices,
deviceSelected: deviceStatus === 'connected' ? deviceSelected : null,
deviceStatus,
genuineCheckStatus,
error,
})
}
return null
const { account, currency, ...props } = this.props
const cur = account ? account.currency : currency
const Icon = cur ? getCryptoCurrencyIcon(cur) : null
return (
<DeviceInteraction
shouldRenderRetry
steps={[
{
id: 'device',
title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
{'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold>
</Trans>
),
icon: usbIcon,
run: this.connectInteractionHandler,
},
{
id: 'address',
title: this.renderOpenAppTitle,
icon: Icon ? <Icon size={24} /> : null,
run: this.openAppInteractionHandler,
},
]}
{...props}
/>
)
}
}
export default connect(mapStateToProps)(EnsureDeviceApp)
async function sleep(s) {
return new Promise(resolve => setTimeout(resolve, s * 1e3))
async function getAddressFromAccountOrCurrency(device, account, currency) {
const { address } = await getAddress
.send({
devicePath: device.path,
currencyId: currency.id,
path: account
? account.freshAddressPath
: standardDerivation({ currency, segwit: false, x: 0 }),
segwit: account ? isSegwitAccount(account) : false,
})
.toPromise()
return address
}
export default connect(mapStateToProps)(EnsureDeviceApp)

51
src/components/ExchangePage/ExchangeCard.js

@ -1,7 +1,8 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { track } from 'analytics/segment'
import type { T } from 'types/common'
@ -10,31 +11,35 @@ import Box, { Card } from 'components/base/Box'
import { FakeLink } from 'components/base/Link'
type CardType = {
id: string,
logo: any,
desc: string,
url: string,
}
export default function ExchangeCard({ t, card }: { t: T, card: CardType }) {
const { logo, desc } = card
return (
<Card
horizontal
py={5}
px={6}
style={{ cursor: 'pointer' }}
onClick={() => shell.openExternal(card.url)}
>
<Box justify="center" style={{ width: 200 }}>
{logo}
</Box>
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{desc}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink>{t('app:exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
export default class ExchangeCard extends PureComponent<{ t: T, card: CardType }> {
onClick = () => {
const { card } = this.props
shell.openExternal(card.url)
track('VisitExchange', { id: card.id, url: card.url })
}
render() {
const {
card: { logo, id },
t,
} = this.props
return (
<Card horizontal py={5} px={6} style={{ cursor: 'pointer' }} onClick={this.onClick}>
<Box justify="center" style={{ width: 200 }}>
{logo}
</Box>
</Box>
</Card>
)
<Box shrink ff="Open Sans|Regular" fontSize={4} flow={3}>
<Box>{t(`app:exchange.${id}`)}</Box>
<Box horizontal align="center" color="wallet" flow={1}>
<FakeLink>{t('app:exchange.visitWebsite')}</FakeLink>
<ExternalLinkIcon size={14} />
</Box>
</Box>
</Card>
)
}
}

49
src/components/ExchangePage/index.js

@ -5,6 +5,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import TrackPage from 'analytics/TrackPage'
import Box from 'components/base/Box'
import ExchangeCard from './ExchangeCard'
@ -16,35 +17,39 @@ type Props = {
t: T,
}
const cards = [
{
key: 'coinhouse',
id: 'coinhouse',
url: 'https://www.coinhouse.com/r/157530',
logo: <CoinhouseLogo width={150} />,
},
{
key: 'changelly',
id: 'changelly',
url: 'https://changelly.com/?ref_id=aac789605a01',
logo: <ChangellyLogo width={150} />,
},
{
key: 'coinmama',
id: 'coinmama',
url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
logo: <CoinmamaLogo width={150} />,
},
]
class ExchangePage extends PureComponent<Props> {
render() {
const { t } = this.props
const cards = [
{
key: 'coinhouse',
url: 'https://www.coinhouse.com/r/157530',
logo: <CoinhouseLogo width={150} />,
desc: t('app:exchange.coinhouse'),
},
{
key: 'changelly',
url: 'https://changelly.com/?ref_id=aac789605a01',
logo: <ChangellyLogo width={150} />,
desc: t('app:exchange.changelly'),
},
{
key: 'coinmama',
url: 'http://go.coinmama.com/visit/?bta=51801&nci=5343',
logo: <CoinmamaLogo width={150} />,
desc: t('app:exchange.coinmama'),
},
]
return (
<Box pb={6}>
<Box ff="Museo Sans|Regular" color="dark" fontSize={7} mb={5}>
<TrackPage category="Exchange" />
<Box ff="Museo Sans|Regular" color="dark" fontSize={7} mb={3}>
{t('app:exchange.title')}
</Box>
<Box ff="Museo Sans|Light" color="grey" fontSize={5} mb={5}>
{t('app:exchange.desc')}
</Box>
<Box flow={5}>{cards.map(card => <ExchangeCard key={card.key} t={t} card={card} />)}</Box>
</Box>
)

2
src/components/ExportLogsBtn.js

@ -71,7 +71,7 @@ class ExportLogsBtn extends Component<{
return hookToShortcut ? (
<KeyHandler keyValue="e" onKeyHandle={this.onKeyHandle} />
) : (
<Button primary onClick={this.handleExportLogs}>
<Button primary event="ExportLogs" onClick={this.handleExportLogs}>
{t('app:settings.exportLogs.btn')}
</Button>
)

156
src/components/GenuineCheck.js

@ -0,0 +1,156 @@
// @flow
import React, { PureComponent } from 'react'
import { timeout } from 'rxjs/operators/timeout'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate, Trans } from 'react-i18next'
import { delay, createCancelablePolling } from 'helpers/promise'
import logger from 'logger'
import type { T, Device } from 'types/common'
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 getDeviceInfo from 'commands/getDeviceInfo'
import getIsGenuine from 'commands/getIsGenuine'
import DeviceInteraction from 'components/DeviceInteraction'
import Text from 'components/base/Text'
import IconUsb from 'icons/Usb'
import IconHome from 'icons/Home'
import IconEye from 'icons/Eye'
const DeviceNotGenuineError = createCustomErrorClass('DeviceNotGenuine')
type Props = {
t: T,
onSuccess: void => void,
onFail: Error => void,
onUnavailable: Error => void,
device: ?Device,
}
const usbIcon = <IconUsb size={36} />
const homeIcon = <IconHome size={24} />
const eyeIcon = <IconEye size={24} />
const mapStateToProps = state => ({
device: getCurrentDevice(state),
})
const Bold = props => <Text ff="Open Sans|Bold" {...props} />
// to speed up genuine check, cache result by device id
const genuineDevices = new WeakSet()
class GenuineCheck extends PureComponent<Props> {
connectInteractionHandler = () =>
createCancelablePolling(() => {
const { device } = this.props
if (!device) return Promise.reject()
return Promise.resolve(device)
})
checkDashboardInteractionHandler = ({ device }: { device: Device }) =>
createCancelablePolling(() =>
getDeviceInfo
.send({ devicePath: device.path })
.pipe(timeout(DEVICE_INFOS_TIMEOUT))
.toPromise(),
)
checkGenuineInteractionHandler = async ({
device,
deviceInfo,
}: {
device: Device,
deviceInfo: DeviceInfo,
}) => {
if (genuineDevices.has(device)) {
logger.log("genuine was already checked. don't check again")
await delay(GENUINE_CACHE_DELAY)
return true
}
const res = await getIsGenuine
.send({ devicePath: device.path, deviceInfo })
.pipe(timeout(GENUINE_TIMEOUT))
.toPromise()
const isGenuine = res === '0000'
if (!isGenuine) {
throw new DeviceNotGenuineError()
}
genuineDevices.add(device)
return true
}
handleFail = (err: Error) => {
const { onFail, onUnavailable } = this.props
if (err instanceof DeviceNotGenuineError) {
onFail(err)
} else {
onUnavailable(err)
}
}
render() {
const { onSuccess, ...props } = this.props
const steps = [
{
id: 'device',
title: (
<Trans i18nKey="app:deviceConnect.step1.connect" parent="div">
{'Connect and unlock your '}
<Bold>{'Ledger device'}</Bold>
</Trans>
),
icon: usbIcon,
run: this.connectInteractionHandler,
},
{
id: 'deviceInfo',
title: (
<Trans i18nKey="deviceConnect:dashboard.open" parent="div">
{'Navigate to the '}
<Bold>{'dashboard'}</Bold>
{' on your device'}
</Trans>
),
icon: homeIcon,
run: this.checkDashboardInteractionHandler,
},
{
id: 'isGenuine',
title: (
<Trans i18nKey="deviceConnect:stepGenuine.open" parent="div">
{'Allow '}
<Bold>{'Ledger Manager'}</Bold>
{' on your device'}
</Trans>
),
icon: eyeIcon,
run: this.checkGenuineInteractionHandler,
},
]
return (
<DeviceInteraction
{...props}
waitBeforeSuccess={500}
steps={steps}
onSuccess={onSuccess}
onFail={this.handleFail}
/>
)
}
}
export default compose(
translate(),
connect(mapStateToProps),
)(GenuineCheck)

37
src/components/GenuineCheckModal.js

@ -0,0 +1,37 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
import GenuineCheck from 'components/GenuineCheck'
type Props = {
t: T,
onSuccess: void => void,
onFail: void => void,
onUnavailable: Error => void,
}
class GenuineCheckModal extends PureComponent<Props> {
renderBody = ({ onClose }) => {
const { t, onSuccess, onFail, onUnavailable } = this.props
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle>
<ModalContent>
<GenuineCheck onSuccess={onSuccess} onFail={onFail} onUnavailable={onUnavailable} />
</ModalContent>
</ModalBody>
)
}
render() {
const { t, ...props } = this.props
return <Modal {...props} render={this.renderBody} />
}
}
export default translate()(GenuineCheckModal)

90
src/components/GenuineCheckModal/index.js

@ -1,90 +0,0 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import Modal, { ModalBody, ModalTitle, ModalContent } from 'components/base/Modal'
import Workflow from 'components/Workflow'
import WorkflowDefault from 'components/Workflow/WorkflowDefault'
type Props = {
t: T,
onGenuineCheckPass: () => void,
onGenuineCheckFailed: () => void,
onGenuineCheckUnavailable: Error => void,
}
type State = {}
class GenuineCheckStatus extends PureComponent<*> {
componentDidUpdate() {
this.sideEffect()
}
sideEffect() {
const {
isGenuine,
error,
onGenuineCheckPass,
onGenuineCheckFailed,
onGenuineCheckUnavailable,
} = this.props
if (isGenuine !== null) {
if (isGenuine) {
onGenuineCheckPass()
} else {
onGenuineCheckFailed()
}
} else if (error) {
onGenuineCheckUnavailable(error)
}
}
render() {
return null
}
}
/* eslint-disable react/no-multi-comp */
class GenuineCheck extends PureComponent<Props, State> {
renderBody = ({ onClose }) => {
const { t, onGenuineCheckPass, onGenuineCheckFailed, onGenuineCheckUnavailable } = this.props
// TODO: use the real devices list. for now we force choosing only
// the current device because we don't handle multi device in MVP
return (
<ModalBody onClose={onClose}>
<ModalTitle>{t('app:genuinecheck.modal.title')}</ModalTitle>
<ModalContent>
<Workflow
renderDefault={(device, deviceInfo, isGenuine, errors) => (
<Fragment>
<GenuineCheckStatus
isGenuine={isGenuine}
error={errors.genuineError}
onGenuineCheckPass={onGenuineCheckPass}
onGenuineCheckFailed={onGenuineCheckFailed}
onGenuineCheckUnavailable={onGenuineCheckUnavailable}
/>
<WorkflowDefault
device={device}
deviceInfo={deviceInfo}
isGenuine={isGenuine}
errors={errors} // TODO: FIX ERRORS
/>
</Fragment>
)}
/>
</ModalContent>
</ModalBody>
)
}
render() {
const { ...props } = this.props
return <Modal {...props} render={({ onClose }) => this.renderBody({ onClose })} />
}
}
export default translate()(GenuineCheck)

1
src/components/IsUnlocked.js

@ -192,6 +192,7 @@ class IsUnlocked extends Component<Props, State> {
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}

114
src/components/ManagerPage/AppsList.js

@ -4,15 +4,21 @@
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { compose } from 'redux'
import type { Device, T } from 'types/common'
import type { LedgerScriptParams } from 'helpers/common'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import { developerModeSelector } from 'reducers/settings'
import listApps from 'commands/listApps'
import listAppVersions from 'commands/listAppVersions'
import installApp from 'commands/installApp'
import uninstallApp from 'commands/uninstallApp'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Tooltip from 'components/base/Tooltip'
import Text from 'components/base/Text'
@ -20,6 +26,7 @@ import Progress from 'components/base/Progress'
import Spinner from 'components/base/Spinner'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import TrackPage from 'analytics/TrackPage'
import IconInfoCircle from 'icons/InfoCircle'
import ExclamationCircleThin from 'icons/ExclamationCircleThin'
@ -27,9 +34,13 @@ import Update from 'icons/Update'
import Trash from 'icons/Trash'
import CheckCircle from 'icons/CheckCircle'
import ManagerApp from './ManagerApp'
import ManagerApp, { Container as FakeManagerAppContainer } from './ManagerApp'
import AppSearchBar from './AppSearchBar'
const mapStateToProps = state => ({
isDevMode: developerModeSelector(state),
})
const List = styled(Box).attrs({
horizontal: true,
m: -3,
@ -46,26 +57,33 @@ type Mode = 'home' | 'installing' | 'uninstalling'
type Props = {
device: Device,
targetId: string | number,
deviceInfo: DeviceInfo,
t: T,
fullVersion: string,
provider: number,
isDevMode: boolean,
}
type State = {
status: Status,
error: ?Error,
appsList: LedgerScriptParams[],
filteredAppVersionsList: LedgerScriptParams[],
appsLoaded: boolean,
app: string,
mode: Mode,
}
const LoadingApp = () => (
<FakeManagerAppContainer noShadow align="center" justify="center" style={{ height: 90 }}>
<Spinner size={16} color="rgba(0, 0, 0, 0.3)" />
</FakeManagerAppContainer>
)
const loadingApp = <LoadingApp />
class AppsList extends PureComponent<Props, State> {
state = {
status: 'loading',
error: null,
appsList: [],
filteredAppVersionsList: [],
appsLoaded: false,
app: '',
mode: 'home',
@ -81,12 +99,31 @@ class AppsList extends PureComponent<Props, State> {
_unmounted = false
filterAppVersions = (applicationsList, compatibleAppVersionsList) => {
if (!this.props.isDevMode) {
return compatibleAppVersionsList.filter(
version => applicationsList.find(e => e.id === version.app).category !== 2,
)
}
return compatibleAppVersionsList
}
async fetchAppList() {
try {
const { targetId, fullVersion, provider } = this.props
const appsList = await listApps.send({ targetId, fullVersion, provider }).toPromise()
const { deviceInfo } = this.props
const applicationsList = await listApps.send({}).toPromise()
const compatibleAppVersionsList = await listAppVersions.send(deviceInfo).toPromise()
const filteredAppVersionsList = this.filterAppVersions(
applicationsList,
compatibleAppVersionsList,
)
if (!this._unmounted) {
this.setState({ appsList, status: 'idle', appsLoaded: true })
this.setState({
status: 'idle',
filteredAppVersionsList,
appsLoaded: true,
})
}
} catch (err) {
this.setState({ status: 'error', error: err })
@ -98,9 +135,9 @@ class AppsList extends PureComponent<Props, State> {
try {
const {
device: { path: devicePath },
targetId,
deviceInfo,
} = this.props
const data = { app, devicePath, targetId }
const data = { app, devicePath, targetId: deviceInfo.targetId }
await installApp.send(data).toPromise()
this.setState({ status: 'success' })
} catch (err) {
@ -113,9 +150,9 @@ class AppsList extends PureComponent<Props, State> {
try {
const {
device: { path: devicePath },
targetId,
deviceInfo,
} = this.props
const data = { app, devicePath, targetId }
const data = { app, devicePath, targetId: deviceInfo.targetId }
await uninstallApp.send(data).toPromise()
this.setState({ status: 'success' })
} catch (err) {
@ -157,6 +194,12 @@ class AppsList extends PureComponent<Props, State> {
</Fragment>
) : status === 'error' ? (
<Fragment>
<TrackPage
category="Manager"
name="Error Modal"
error={error && error.name}
app={app}
/>
<ModalContent grow align="center" justify="center" mt={3}>
<Box color="alertRed">
<ExclamationCircleThin size={44} />
@ -169,7 +212,17 @@ class AppsList extends PureComponent<Props, State> {
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} />
<TranslatedError error={error} field="title" />
</Box>
<Box
color="graphite"
mt={4}
fontSize={6}
ff="Open Sans"
textAlign="center"
style={{ maxWidth: 350 }}
>
<TranslatedError error={error} field="description" />
</Box>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
@ -214,10 +267,10 @@ class AppsList extends PureComponent<Props, State> {
}
renderList() {
const { appsList, appsLoaded } = this.state
return appsLoaded ? (
const { filteredAppVersionsList, appsLoaded } = this.state
return (
<Box>
<AppSearchBar list={appsList}>
<AppSearchBar list={filteredAppVersionsList}>
{items => (
<List>
{items.map(c => (
@ -234,10 +287,22 @@ class AppsList extends PureComponent<Props, State> {
)}
</AppSearchBar>
{this.renderModal()}
</Box>
) : (
<Box align="center" justify="center">
<Spinner size={50} />
{!appsLoaded && (
<Fragment>
<Space of={30} />
<List>
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
{loadingApp}
</List>
</Fragment>
)}
</Box>
)
}
@ -268,4 +333,7 @@ class AppsList extends PureComponent<Props, State> {
}
}
export default translate()(AppsList)
export default compose(
translate(),
connect(mapStateToProps),
)(AppsList)

9
src/components/ManagerPage/Dashboard.js

@ -7,6 +7,7 @@ import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import TrackPage from 'analytics/TrackPage'
import AppsList from './AppsList'
import FirmwareUpdate from './FirmwareUpdate'
@ -19,6 +20,7 @@ type Props = {
const Dashboard = ({ device, deviceInfo, t }: Props) => (
<Box flow={4} pb={8}>
<TrackPage category="Manager" name="Dashboard" />
<Box>
<Text ff="Museo Sans|Regular" fontSize={7} color="black">
{t('app:manager.title')}
@ -31,12 +33,7 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => (
<FirmwareUpdate deviceInfo={deviceInfo} device={device} />
</Box>
<Box mt={5}>
<AppsList
device={device}
targetId={deviceInfo.targetId}
provider={deviceInfo.providerId}
fullVersion={deviceInfo.fullVersion}
/>
<AppsList device={device} deviceInfo={deviceInfo} />
</Box>
</Box>
)

62
src/components/ManagerPage/FirmwareFinalUpdate.js

@ -1,62 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import logger from 'logger'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import type { Device, T } from 'types/common'
import installFinalFirmware from 'commands/installFinalFirmware'
import Box, { Card } from 'components/base/Box'
// import Button from 'components/base/Button'
type Props = {
t: T,
device: Device,
deviceInfo: DeviceInfo,
}
type State = {}
class FirmwareFinalUpdate extends PureComponent<Props, State> {
componentDidMount() {}
componentWillUnmount() {
this._unmounting = true
}
_unmounting = false
installFinalFirmware = async () => {
try {
const { device, deviceInfo } = this.props
const { success } = await installFinalFirmware
.send({ devicePath: device.path, deviceInfo })
.toPromise()
if (success) {
this.setState()
}
} catch (err) {
logger.log(err)
}
}
render() {
const { t, ...props } = this.props
return (
<Box flow={4} {...props}>
<Box color="dark" ff="Museo Sans" fontSize={6}>
{t('app:manager.firmware.update')}
</Box>
<Card flow={2} {...props}>
<Box horizontal align="center" flow={2} />
</Card>
</Box>
)
}
}
export default translate()(FirmwareFinalUpdate)

122
src/components/ManagerPage/FirmwareUpdate.js

@ -2,7 +2,7 @@
/* eslint-disable react/jsx-no-literals */ // FIXME
import React, { PureComponent, Fragment } from 'react'
import { translate, Trans } from 'react-i18next'
import { translate } from 'react-i18next'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
import invariant from 'invariant'
@ -14,35 +14,34 @@ import type { LedgerScriptParams } from 'helpers/common'
import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice'
import installOsuFirmware from 'commands/installOsuFirmware'
import installFinalFirmware from 'commands/installFinalFirmware'
import installMcu from 'commands/installMcu'
import DisclaimerModal from 'components/modals/UpdateFirmware/Disclaimer'
import UpdateModal from 'components/modals/UpdateFirmware'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import Tooltip from 'components/base/Tooltip'
import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Modal, { ModalBody, ModalFooter, ModalTitle, ModalContent } from 'components/base/Modal'
import Button from 'components/base/Button'
// import Progress from 'components/base/Progress'
import NanoS from 'icons/device/NanoS'
import CheckFull from 'icons/CheckFull'
import { PreventDeviceChangeRecheck } from '../Workflow/EnsureDevice'
import { PreventDeviceChangeRecheck } from 'components/EnsureDevice'
import UpdateFirmwareButton from './UpdateFirmwareButton'
let CACHED_LATEST_FIRMWARE = null
export const getCleanVersion = (input: string): string =>
input.endsWith('-osu') ? input.replace('-osu', '') : input
type ModalStatus = 'closed' | 'disclaimer' | 'installing' | 'error' | 'success'
export type ModalStatus = 'closed' | 'disclaimer' | 'install' | 'error' | 'success'
type Props = {
t: T,
device: Device,
deviceInfo: DeviceInfo,
}
type State = {
latestFirmware: ?LedgerScriptParams,
latestFirmware: ?LedgerScriptParams & ?{ shouldUpdateMcu: boolean },
modal: ModalStatus,
}
@ -57,7 +56,7 @@ class FirmwareUpdate extends PureComponent<Props, State> {
}
componentDidUpdate() {
if (!CACHED_LATEST_FIRMWARE || isEmpty(this.state.latestFirmware)) {
if (isEmpty(this.state.latestFirmware)) {
this.fetchLatestFirmware()
}
}
@ -70,82 +69,55 @@ class FirmwareUpdate extends PureComponent<Props, State> {
fetchLatestFirmware = async () => {
const { deviceInfo } = this.props
const latestFirmware =
CACHED_LATEST_FIRMWARE || (await getLatestFirmwareForDevice.send(deviceInfo).toPromise())
const latestFirmware = await getLatestFirmwareForDevice.send(deviceInfo).toPromise()
if (
!isEmpty(latestFirmware) &&
!isEqual(this.state.latestFirmware, latestFirmware) &&
!this._unmounting
) {
CACHED_LATEST_FIRMWARE = latestFirmware
this.setState({ latestFirmware })
}
}
installFirmware = async () => {
installOsuFirmware = async (device: Device) => {
try {
const { latestFirmware } = this.state
const { deviceInfo } = this.props
invariant(latestFirmware, 'did not find a new firmware or firmware is not set')
const {
device: { path: devicePath },
} = this.props
this.setState({ modal: 'installing' })
this.setState({ modal: 'install' })
const { success } = await installOsuFirmware
.send({ devicePath, firmware: latestFirmware, targetId: deviceInfo.targetId })
.send({ devicePath: device.path, firmware: latestFirmware, targetId: deviceInfo.targetId })
.toPromise()
if (success) {
this.fetchLatestFirmware()
}
return success
} catch (err) {
logger.log(err)
throw err
}
}
handleCloseModal = () => this.setState({ modal: 'closed' })
handleInstallModal = () => this.setState({ modal: 'disclaimer' })
installFinalFirmware = async (device: Device) => {
try {
const { success } = await installFinalFirmware.send({ devicePath: device.path }).toPromise()
return success
} catch (err) {
logger.log(err)
throw err
}
}
renderModal = () => {
const { t } = this.props
const { modal, latestFirmware } = this.state
return (
<Modal
isOpened={modal !== 'closed'}
render={() => (
<ModalBody grow align="center" justify="center" mt={3}>
<Fragment>
<ModalTitle>{t('app:manager.firmware.update')}</ModalTitle>
<ModalContent>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
<Trans i18nKey="app:manager.firmware.disclaimerTitle">
You are about to install the latest
<Text ff="Open Sans|SemiBold" color="dark">
{`firmware ${latestFirmware ? getCleanVersion(latestFirmware.name) : ''}`}
</Text>
</Trans>
</Text>
<Text ff="Open Sans|Regular" fontSize={4} color="graphite" align="center">
{t('app:manager.firmware.disclaimerAppDelete')}
{t('app:manager.firmware.disclaimerAppReinstall')}
</Text>
</ModalContent>
<ModalFooter horizontal justifyContent="flex-end" style={{ width: '100%' }}>
<Button primary padded onClick={this.installFirmware}>
{t('app:manager.firmware.continue')}
</Button>
</ModalFooter>
</Fragment>
</ModalBody>
)}
/>
)
flashMCU = async (device: Device) => {
await installMcu.send({ devicePath: device.path }).toPromise()
}
handleCloseModal = () => this.setState({ modal: 'closed' })
handleDisclaimerModal = () => this.setState({ modal: 'disclaimer' })
handleInstallModal = () => this.setState({ modal: 'install' })
render() {
const { deviceInfo, t } = this.props
const { latestFirmware, modal } = this.state
return (
<Card p={4}>
<Box horizontal align="center" flow={2}>
@ -158,7 +130,9 @@ class FirmwareUpdate extends PureComponent<Props, State> {
Ledger Nano S
</Text>
<Box color="wallet" style={{ marginLeft: 10 }}>
<CheckFull size={13} color="wallet" />
<Tooltip render={() => t('app:manager.yourDeviceIsGenuine')}>
<CheckFull size={13} color="wallet" />
</Tooltip>
</Box>
</Box>
<Text ff="Open Sans|SemiBold" fontSize={2}>
@ -167,13 +141,27 @@ class FirmwareUpdate extends PureComponent<Props, State> {
})}
</Text>
</Box>
<UpdateFirmwareButton
firmware={latestFirmware}
installFirmware={this.handleInstallModal}
/>
<UpdateFirmwareButton firmware={latestFirmware} onClick={this.handleDisclaimerModal} />
</Box>
{modal !== 'closed' ? <PreventDeviceChangeRecheck /> : null}
{this.renderModal()}
{latestFirmware && (
<Fragment>
<DisclaimerModal
firmware={latestFirmware}
status={modal}
goToNextStep={this.handleInstallModal}
onClose={this.handleCloseModal}
/>
<UpdateModal
status={modal}
onClose={this.handleCloseModal}
firmware={latestFirmware}
installOsuFirmware={this.installOsuFirmware}
installFinalFirmware={this.installFinalFirmware}
flashMCU={this.flashMCU}
/>
</Fragment>
)}
</Card>
)
}

50
src/components/ManagerPage/FlashMcu.js

@ -1,50 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import type { Device } from 'types/common'
import installMcu from 'commands/installMcu'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
type Props = {
device: Device,
deviceInfo: DeviceInfo,
}
type State = {
flashing: boolean,
}
class FlashMcu extends PureComponent<Props, State> {
state = {
flashing: false,
}
flashMCU = async () => {
const { device, deviceInfo } = this.props
const { flashing } = this.state
if (!flashing) {
this.setState({ flashing: true })
await installMcu
.send({
devicePath: device.path,
targetId: deviceInfo.targetId,
version: deviceInfo.seVersion,
})
.toPromise()
this.setState({ flashing: false })
}
}
render() {
return (
<div>
<h1>Flashing MCU</h1>
<button onClick={this.flashMCU}>flash</button>
</div>
)
}
}
export default FlashMcu

27
src/components/ManagerPage/ManagerApp.js

@ -12,17 +12,17 @@ import Box from 'components/base/Box'
import Text from 'components/base/Text'
import Button from 'components/base/Button'
const Container = styled(Box).attrs({
export const Container = styled(Box).attrs({
horizontal: true,
my: 2,
mx: 3,
p: 4,
boxShadow: 0,
bg: 'white',
boxShadow: p => (p.noShadow ? -1 : 0),
borderRadius: 4,
flow: 2,
})`
width: calc(50% - 30px);
background: white;
line-height: normal;
`
@ -63,10 +63,27 @@ function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) {
{version}
</Text>
</Box>
<Button outline onClick={onInstall}>
<Button
outline
onClick={onInstall}
event={'Manager Install Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
>
{t('app:manager.apps.install')}
</Button>
<Button outline onClick={onUninstall} outlineColor="grey">
<Button
outline
onClick={onUninstall}
event={'Manager Uninstall Click'}
eventProperties={{
appName: name,
appVersion: version,
}}
outlineColor="grey"
>
<Trash size={16} fill="grey" />
</Button>
</Container>

48
src/components/ManagerPage/ManagerGenuineCheck.js

@ -0,0 +1,48 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { i } from 'helpers/staticPath'
import GenuineCheck from 'components/GenuineCheck'
import Box from 'components/base/Box'
import Space from 'components/base/Space'
import Text from 'components/base/Text'
import TrackPage from 'analytics/TrackPage'
type Props = {
t: T,
onSuccess: void => void,
}
class ManagerGenuineCheck extends PureComponent<Props> {
render() {
const { t, onSuccess } = this.props
return (
<Box align="center">
<TrackPage category="Manager" name="Genuine Check" />
<Space of={60} />
<Box align="center" style={{ maxWidth: 460 }}>
<img
src={i('logos/connectDevice.png')}
alt="connect your device"
style={{ marginBottom: 30, maxWidth: 362, width: '100%' }}
/>
<Text ff="Museo Sans|Regular" fontSize={7} color="black" style={{ marginBottom: 10 }}>
{t('app:manager.device.title')}
</Text>
<Text ff="Museo Sans|Light" fontSize={5} color="grey" align="center">
{t('app:manager.device.desc')}
</Text>
</Box>
<Space of={40} />
<GenuineCheck shouldRenderRetry onSuccess={onSuccess} />
</Box>
)
}
}
export default translate()(ManagerGenuineCheck)

15
src/components/ManagerPage/UpdateFirmwareButton.js

@ -4,7 +4,7 @@ import { translate } from 'react-i18next'
import type { T } from 'types/common'
import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants'
// import { EXPERIMENTAL_FIRMWARE_UPDATE } from 'config/constants'
import Button from 'components/base/Button'
import Text from 'components/base/Text'
@ -18,16 +18,23 @@ type FirmwareInfos = {
type Props = {
t: T,
firmware: ?FirmwareInfos,
installFirmware: () => void,
onClick: () => void,
}
const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) =>
const UpdateFirmwareButton = ({ t, firmware, onClick }: Props) =>
firmware ? (
<Fragment>
<Text ff="Open Sans|Regular" fontSize={4} style={{ marginLeft: 'auto', marginRight: 15 }}>
{t('app:manager.firmware.latest', { version: getCleanVersion(firmware.name) })}
</Text>
<Button primary onClick={installFirmware} disabled={!EXPERIMENTAL_FIRMWARE_UPDATE}>
<Button
primary
onClick={onClick}
event={'Manager Firmware Update Click'}
eventProperties={{
firmwareName: firmware.name,
}}
>
{t('app:manager.firmware.update')}
</Button>
</Fragment>

68
src/components/ManagerPage/index.js

@ -1,52 +1,46 @@
// @flow
/* eslint-disable react/jsx-no-literals */ // FIXME: remove
import React, { PureComponent } from 'react'
import invariant from 'invariant'
import type { Device } from 'types/common'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
import Workflow from 'components/Workflow'
import WorkflowWithIcon from 'components/Workflow/WorkflowWithIcon'
import Dashboard from './Dashboard'
import FlashMcu from './FlashMcu'
type Error = {
message: string,
stack: string,
import ManagerGenuineCheck from './ManagerGenuineCheck'
type Props = {}
type State = {
isGenuine: ?boolean,
device: ?Device,
deviceInfo: ?DeviceInfo,
}
class ManagerPage extends PureComponent<*, *> {
class ManagerPage extends PureComponent<Props, State> {
state = {
isGenuine: null,
device: null,
deviceInfo: null,
}
// prettier-ignore
handleSuccessGenuine = ({ device, deviceInfo }: { device: Device, deviceInfo: DeviceInfo }) => { // eslint-disable-line react/no-unused-prop-types
this.setState({ isGenuine: true, device, deviceInfo })
}
render() {
return (
<Workflow
renderFinalUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<p>UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.isOSU}</p>
)}
renderMcuUpdate={(device: Device, deviceInfo: DeviceInfo) => (
<FlashMcu device={device} deviceInfo={deviceInfo} />
)}
renderDashboard={(device: Device, deviceInfo: DeviceInfo) => (
<Dashboard device={device} deviceInfo={deviceInfo} />
)}
renderDefault={(
device: ?Device,
deviceInfo: ?DeviceInfo,
isGenuine: ?boolean,
errors: {
dashboardError: ?Error,
genuineError: ?Error,
},
) => (
<WorkflowWithIcon
device={device}
deviceInfo={deviceInfo}
errors={errors}
isGenuine={isGenuine}
/>
)}
/>
)
const { isGenuine, device, deviceInfo } = this.state
if (!isGenuine) {
return <ManagerGenuineCheck onSuccess={this.handleSuccessGenuine} />
}
invariant(device, 'Inexistant device considered genuine')
invariant(deviceInfo, 'Inexistant device infos for genuine device')
return <Dashboard device={device} deviceInfo={deviceInfo} />
}
}

30
src/components/Onboarding/helperComponents.js

@ -27,7 +27,7 @@ export const Description = styled(Box).attrs({
color: 'grey',
})`
margin: 10px auto 25px;
max-width: 600px;
max-width: 640px;
`
export const Inner = styled(Box).attrs({
@ -52,17 +52,31 @@ export const OnboardingFooterWrapper = styled(Box).attrs({
border-bottom-left-radius: ${radii[1]}px;
border-bottom-right-radius: ${radii[1]}px;
`
// LIVE LOGO
export function LiveLogo({ icon, ...p }: { icon: any }) {
return <LiveLogoContainer {...p}>{icon}</LiveLogoContainer>
}
export const LiveLogoContainer = styled(Box).attrs({
borderRadius: '50%',
alignItems: 'center',
justifyContent: 'center',
})`
background-color: white;
box-shadow: 0 2px 24px 0 #00000014;
width: ${p => (p.width ? p.width : 80)}
height: ${p => (p.height ? p.height : 80)}
`
// INSTRUCTION LIST
type StepType = {
icon: any,
desc: string,
desc: any,
}
export function OptionRow({ step }: { step: StepType }) {
export function OptionRow({ step, ...p }: { step: StepType }) {
const { icon, desc } = step
return (
<Box horizontal m="7px" style={{ minWidth: 420 }}>
<Box justify="center">{icon}</Box>
<Box {...p}>{icon}</Box>
<Box justify="center" shrink>
<OptionRowDesc>{desc}</OptionRowDesc>
</Box>
@ -91,12 +105,13 @@ export function DisclaimerBox({ disclaimerNotes, ...p }: { disclaimerNotes: any
<DisclaimerBoxIconContainer>
<IconSensitiveOperationShield />
</DisclaimerBoxIconContainer>
{disclaimerNotes.map(note => <OptionRow key={note.key} step={note} />)}
{disclaimerNotes.map(note => <OptionRow justify="center" key={note.key} step={note} />)}
</Box>
</DisclaimerBoxContainer>
)
}
// Not enough styled as a warning
const DisclaimerBoxContainer = styled(Box).attrs({
shrink: 1,
grow: true,
@ -125,7 +140,10 @@ export const GenuineCheckCardWrapper = styled(Box).attrs({
height: 74px;
transition: all ease-in-out 0.2s;
color: ${p => (p.isDisabled ? p.theme.colors.grey : p.theme.colors.black)};
border: ${p => `1px ${p.isDisabled ? 'dashed' : 'solid'} ${p.theme.colors.fog}`};
border: ${p =>
`1px ${p.isDisabled ? 'dashed' : 'solid'} ${
p.isError ? p.theme.colors.alertRed : p.theme.colors.fog
}`};
pointer-events: ${p => (p.isDisabled ? 'none' : 'auto')};
background-color: ${p => (p.isDisabled ? p.theme.colors.lightGrey : p.theme.colors.white)};
opacity: ${p => (p.isDisabled ? 0.7 : 1)};

1
src/components/Onboarding/index.js

@ -170,6 +170,7 @@ const Container = styled(Box).attrs({
const StepContainer = styled(Box).attrs({
p: 40,
})``
export default compose(
connect(
mapStateToProps,

28
src/components/Onboarding/steps/Analytics.js

@ -6,6 +6,8 @@ import { connect } from 'react-redux'
import { saveSettings } from 'actions/settings'
import Box from 'components/base/Box'
import CheckBox from 'components/base/CheckBox'
import TrackPage from 'analytics/TrackPage'
import Track from 'analytics/Track'
import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents'
import OnboardingFooter from '../OnboardingFooter'
@ -19,7 +21,7 @@ type State = {
}
const INITIAL_STATE = {
analyticsToggle: false,
analyticsToggle: true,
sentryLogsToggle: true,
}
@ -46,11 +48,17 @@ class Analytics extends PureComponent<StepProps, State> {
}
render() {
const { nextStep, t } = this.props
const { nextStep, t, onboarding } = this.props
const { analyticsToggle, sentryLogsToggle } = this.state
return (
<FixedTopContainer>
<TrackPage
category="Onboarding"
name="Analytics"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<StepContainerInner>
<Title>{t('onboarding:analytics.title')}</Title>
<Description>{t('onboarding:analytics.desc')}</Description>
@ -61,6 +69,14 @@ class Analytics extends PureComponent<StepProps, State> {
<AnalyticsText>{t('onboarding:analytics.sentryLogs.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<Track
onUpdate
event={
sentryLogsToggle
? 'Sentry Logs Enabled Onboarding'
: 'Sentry Logs Disabled Onboarding'
}
/>
<CheckBox isChecked={sentryLogsToggle} onChange={this.handleSentryLogsToggle} />
</Box>
</Container>
@ -70,6 +86,14 @@ class Analytics extends PureComponent<StepProps, State> {
<AnalyticsText>{t('onboarding:analytics.shareAnalytics.desc')}</AnalyticsText>
</Box>
<Box justifyContent="center">
<Track
onUpdate
event={
analyticsToggle
? 'Analytics Enabled Onboarding'
: 'Analytics Disabled Onboarding'
}
/>
<CheckBox isChecked={analyticsToggle} onChange={this.handleAnalyticsToggle} />
</Box>
</Container>

94
src/components/Onboarding/steps/Finish.js

@ -1,20 +1,22 @@
// @flow
import React from 'react'
import React, { Component } from 'react'
import { shell } from 'electron'
import styled from 'styled-components'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import ConfettiParty from 'components/ConfettiParty'
import TrackPage from 'analytics/TrackPage'
import IconCheckCircle from 'icons/CheckCircle'
import IconCheckFull from 'icons/CheckFull'
import IconSocialTwitter from 'icons/Twitter'
import IconSocialReddit from 'icons/Reddit'
import IconSocialGithub from 'icons/Github'
import type { StepProps } from '..'
import { Title, Description } from '../helperComponents'
import { Title, Description, LiveLogo } from '../helperComponents'
const ConfettiLayer = styled.div`
position: absolute;
@ -47,33 +49,69 @@ const socialMedia = [
},
]
export default (props: StepProps) => {
const { finish, t } = props
return (
<Box sticky justifyContent="center">
<ConfettiLayer>
<ConfettiParty />
</ConfettiLayer>
<Box alignItems="center">
<Box color="positiveGreen">
<IconCheckCircle size={44} />
</Box>
<Box pt={5} align="center" mb={5}>
<Title>{t('onboarding:finish.title')}</Title>
<Description>{t('onboarding:finish.desc')}</Description>
</Box>
<Button primary padded onClick={() => finish()}>
{t('onboarding:finish.openAppButton')}
</Button>
<Box alignItems="center" mt={7}>
<FollowUsDesc>{t('onboarding:finish.followUsLabel')}</FollowUsDesc>
</Box>
<Box horizontal mt={3} flow={5} color="grey">
{socialMedia.map(socMed => <SocialMediaBox key={socMed.key} socMed={socMed} />)}
export default class Finish extends Component<StepProps, *> {
state = { emit: false }
onMouseUp = () => this.setState({ emit: false })
onMouseDown = () => {
this.setState({ emit: true })
}
onMouseLeave = () => {
this.setState({ emit: false })
}
render() {
const { finish, t, onboarding } = this.props
const { emit } = this.state
return (
<Box sticky justifyContent="center">
<TrackPage
category="Onboarding"
name="Finish"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<ConfettiLayer>
<ConfettiParty emit={emit} />
</ConfettiLayer>
<Box alignItems="center">
<Box
style={{ position: 'relative' }}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
onMouseLeave={this.onMouseLeave}
>
<LiveLogo
style={{ width: 64, height: 64 }}
icon={
<img
draggable="false"
alt=""
src={i('ledgerlive-logo.svg')}
width={40}
height={40}
/>
}
/>
<Box color="positiveGreen" style={{ position: 'absolute', right: 0, bottom: 0 }}>
<IconCheckFull size={18} />
</Box>
</Box>
<Box pt={5} align="center">
<Title>{t('onboarding:finish.title')}</Title>
<Description>{t('onboarding:finish.desc')}</Description>
</Box>
<Box p={5}>
<Button primary padded onClick={() => finish()}>
{t('onboarding:finish.openAppButton')}
</Button>
</Box>
<Box horizontal mt={3} flow={5} color="grey">
{socialMedia.map(socMed => <SocialMediaBox key={socMed.key} socMed={socMed} />)}
</Box>
</Box>
</Box>
</Box>
)
)
}
}
type SocMed = {

14
src/components/Onboarding/steps/GenuineCheck/GenuineCheckErrorPage.js

@ -4,27 +4,35 @@ import React, { Fragment } from 'react'
import { i } from 'helpers/staticPath'
import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TrackPage from 'analytics/TrackPage'
import { Title, Description, OnboardingFooterWrapper } from '../../helperComponents'
export function GenuineCheckErrorPage({
redoGenuineCheck,
contactSupport,
isLedgerNano,
onboarding,
t,
}: {
redoGenuineCheck: () => void,
contactSupport: () => void,
isLedgerNano: boolean | null,
onboarding: OnboardingState,
t: T,
}) {
return (
<Box sticky pt={50}>
<TrackPage
category="Onboarding"
name="Genuine Check Error Page"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<Box grow alignItems="center" justifyContent="center">
{isLedgerNano ? (
{onboarding.isLedgerNano ? (
<Fragment>
<Title>{t('onboarding:genuineCheck.errorPage.ledgerNano.title')}</Title>
<Description>{t('onboarding:genuineCheck.errorPage.ledgerNano.desc')}</Description>

46
src/components/Onboarding/steps/GenuineCheck/GenuineCheckUnavailable.js

@ -7,10 +7,11 @@ import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
import FakeLink from 'components/base/FakeLink'
import IconCross from 'icons/Cross'
import IconExclamationCircle from 'icons/ExclamationCircle'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TranslatedError from 'components/TranslatedError'
import { track } from 'analytics/segment'
import { OnboardingFooterWrapper } from '../../helperComponents'
@ -29,7 +30,13 @@ export function GenuineCheckUnavailableFooter({
{t('app:common.back')}
</Button>
<Box horizontal ml="auto">
<Button padded disabled={false} onClick={() => nextStep()} mx={2}>
<Button
padded
disabled={false}
event="Onboarding Skip Genuine Check"
onClick={() => nextStep()}
mx={2}
>
{t('app:common.skipThisStep')}
</Button>
<Button padded onClick={nextStep} disabled primary>
@ -50,18 +57,33 @@ export function GenuineCheckUnavailableMessage({
onboarding: OnboardingState,
}) {
return (
<Box align="center" flow={1} color={colors.alertRed}>
<FakeLink ff="Open Sans|Regular" fontSize={4} underline onClick={handleOpenGenuineCheckModal}>
<Box
horizontal
align="center"
flow={2}
color={colors.alertRed}
ff="Open Sans|SemiBold"
fontSize={4}
>
<IconExclamationCircle size={16} />
<span>
<TranslatedError error={onboarding.genuine.genuineCheckUnavailable} />
</span>
<FakeLink
color="alertRed"
ff="Open Sans|SemiBold"
fontSize={4}
underline
onClick={() => {
handleOpenGenuineCheckModal()
track('Genuine Check Retry', {
flowType: onboarding.flowType,
deviceType: onboarding.isLedgerNano ? 'Nano S' : 'Blue',
})
}}
>
{t('app:common.retry')}
</FakeLink>
<Box horizontal justify="center">
<Box justifyContent="center">
<IconCross size={12} />
</Box>
<Box ff="Open Sans|Regular" style={{ maxWidth: 150 }} fontSize={2} ml={1}>
<TranslatedError error={onboarding.genuine.genuineCheckUnavailable} />
</Box>
</Box>
</Box>
)
}

42
src/components/Onboarding/steps/GenuineCheck/index.js

@ -9,10 +9,12 @@ import { colors } from 'styles/theme'
import { updateGenuineCheck } from 'reducers/onboarding'
import Box from 'components/base/Box'
import TrackPage from 'analytics/TrackPage'
import Button from 'components/base/Button'
import RadioGroup from 'components/base/RadioGroup'
import GenuineCheckModal from 'components/GenuineCheckModal'
import IconCross from 'icons/Cross'
import IconCheck from 'icons/Check'
import {
@ -91,7 +93,9 @@ class GenuineCheck extends PureComponent<StepProps, State> {
}
}
handleOpenGenuineCheckModal = () => this.setState({ isGenuineCheckModalOpened: true })
handleOpenGenuineCheckModal = () => {
this.setState({ isGenuineCheckModalOpened: true })
}
handleCloseGenuineCheckModal = (cb?: Function) =>
this.setState(
state => ({ ...state, isGenuineCheckModalOpened: false }),
@ -147,7 +151,7 @@ class GenuineCheck extends PureComponent<StepProps, State> {
redoGenuineCheck={this.redoGenuineCheck}
contactSupport={this.contactSupport}
t={this.props.t}
isLedgerNano={this.props.onboarding.isLedgerNano}
onboarding={this.props.onboarding}
/>
)
@ -161,6 +165,12 @@ class GenuineCheck extends PureComponent<StepProps, State> {
return (
<FixedTopContainer>
<TrackPage
category="Onboarding"
name="Genuine Check"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<StepContainerInner>
<Title>{t('onboarding:genuineCheck.title')}</Title>
{onboarding.flowType === 'restoreDevice' ? (
@ -211,7 +221,10 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</GenuineCheckCardWrapper>
</Box>
<Box mt={3}>
<GenuineCheckCardWrapper isDisabled={!genuine.recoveryStepPass}>
<GenuineCheckCardWrapper
isDisabled={!genuine.recoveryStepPass}
isError={genuine.genuineCheckUnavailable}
>
<Box justify="center">
<Box horizontal>
<IconOptionRow color={!genuine.recoveryStepPass ? 'grey' : 'wallet'}>
@ -230,11 +243,9 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</Box>
</Box>
) : genuine.genuineCheckUnavailable ? (
<GenuineCheckUnavailableMessage
handleOpenGenuineCheckModal={this.handleOpenGenuineCheckModal}
onboarding={onboarding}
t={t}
/>
<Box color="alertRed">
<IconCross size={16} />
</Box>
) : (
<Button
primary
@ -247,6 +258,15 @@ class GenuineCheck extends PureComponent<StepProps, State> {
</Box>
)}
</GenuineCheckCardWrapper>
{genuine.genuineCheckUnavailable && (
<Box mt={4}>
<GenuineCheckUnavailableMessage
handleOpenGenuineCheckModal={this.handleOpenGenuineCheckModal}
onboarding={onboarding}
t={t}
/>
</Box>
)}
</Box>
</StepContainerInner>
{genuine.genuineCheckUnavailable ? (
@ -263,9 +283,9 @@ class GenuineCheck extends PureComponent<StepProps, State> {
<GenuineCheckModal
isOpened={isGenuineCheckModalOpened}
onClose={this.handleCloseGenuineCheckModal}
onGenuineCheckPass={this.handleGenuineCheckPass}
onGenuineCheckFailed={this.handleGenuineCheckFailed}
onGenuineCheckUnavailable={this.handleGenuineCheckUnavailable}
onSuccess={this.handleGenuineCheckPass}
onFail={this.handleGenuineCheckFailed}
onUnavailable={this.handleGenuineCheckUnavailable}
/>
</FixedTopContainer>
)

16
src/components/Onboarding/steps/Init.js

@ -7,13 +7,15 @@ import { colors } from 'styles/theme'
import styled from 'styled-components'
import { flowType } from 'reducers/onboarding'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import TrackPage from 'analytics/TrackPage'
import IconPlus from 'icons/Plus'
import IconRecover from 'icons/Recover'
import IconCheck from 'icons/Check'
import IconExternalLink from 'icons/ExternalLink'
import IconChevronRight from 'icons/ChevronRight'
import { i } from 'helpers/staticPath'
import { Title } from '../helperComponents'
import { Title, LiveLogo } from '../helperComponents'
import type { StepProps } from '..'
@ -63,11 +65,13 @@ class Init extends PureComponent<StepProps, *> {
]
return (
<Box sticky justifyContent="center">
<GrowScroll full justifyContent="center" py={7}>
<TrackPage category="Onboarding" name="Init" />
<Box align="center">
<Box color="wallet">
<img alt="" src={i('ledgerlive-logo.svg')} width={50} height={50} />
</Box>
<LiveLogo
style={{ width: 64, height: 64 }}
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={40} height={40} />}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:init.title')}</Title>
</Box>
@ -75,7 +79,7 @@ class Init extends PureComponent<StepProps, *> {
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
</Box>
</Box>
</Box>
</GrowScroll>
)
}
}

32
src/components/Onboarding/steps/NoDevice.js

@ -2,14 +2,16 @@
import React, { PureComponent } from 'react'
import { shell } from 'electron'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import IconUser from 'icons/User'
import GrowScroll from 'components/base/GrowScroll'
import TrackPage from 'analytics/TrackPage'
import IconCart from 'icons/Cart'
import IconTruck from 'icons/Truck'
import IconInfoCircle from 'icons/InfoCircle'
import Button from '../../base/Button/index'
import { Title, OnboardingFooterWrapper } from '../helperComponents'
import { Title, OnboardingFooterWrapper, LiveLogo } from '../helperComponents'
import { OptionFlowCard } from './Init'
import type { StepProps } from '..'
@ -46,18 +48,22 @@ class NoDevice extends PureComponent<StepProps, *> {
]
return (
<Box sticky pt={130}>
<Box grow alignItems="center">
<Box color="wallet">
<IconUser size={36} />
<Box sticky>
<GrowScroll pb={7} pt={130}>
<TrackPage category="Onboarding" name="No Device" />
<Box grow alignItems="center">
<LiveLogo
style={{ width: 64, height: 64 }}
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={40} height={40} />}
/>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title>
</Box>
<Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
</Box>
</Box>
<Box m={5} style={{ maxWidth: 480 }}>
<Title>{t('onboarding:noDevice.title')}</Title>
</Box>
<Box pt={4} flow={4}>
{optionCards.map(card => <OptionFlowCard key={card.key} card={card} />)}
</Box>
</Box>
</GrowScroll>
<OnboardingFooterWrapper>
<Button padded outlineGrey onClick={() => prevStep()} mr="auto">
{t('app:common.back')}

3
src/components/Onboarding/steps/SelectDevice.js

@ -10,6 +10,8 @@ import { rgba } from 'styles/helpers'
import { isLedgerNano } from 'reducers/onboarding'
import Box from 'components/base/Box'
import TrackPage from 'analytics/TrackPage'
import IconCheckCirle from 'icons/Check'
import { Title, Inner, FixedTopContainer, StepContainerInner } from '../helperComponents'
@ -36,6 +38,7 @@ class SelectDevice extends PureComponent<StepProps, {}> {
const { t, onboarding, jumpStep } = this.props
return (
<FixedTopContainer>
<TrackPage category="Onboarding" name="Select Device" flowType={onboarding.flowType} />
<StepContainerInner>
<Box mb={5}>
<Title>{t('onboarding:selectDevice.title')}</Title>

14
src/components/Onboarding/steps/SelectPIN/SelectPINblue.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import type { T } from 'types/common'
@ -29,7 +30,16 @@ class SelectPIN extends PureComponent<Props, *> {
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.blue.step2'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.initialize.instructions.blue.step2">
{'Tap on'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Configure as new device'}
</Text>
</Trans>
</Box>
),
},
{
key: 'step3',

14
src/components/Onboarding/steps/SelectPIN/SelectPINnano.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import type { T } from 'types/common'
@ -34,7 +35,16 @@ class SelectPINnano extends PureComponent<Props, *> {
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.initialize.instructions.nano.step3'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.initialize.instructions.nano.step3">
{'Press the right button to select'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Configure as new device'}
</Text>
</Trans>
</Box>
),
},
{
key: 'step4',

14
src/components/Onboarding/steps/SelectPIN/SelectPINrestoreBlue.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import type { T } from 'types/common'
@ -29,7 +30,16 @@ class SelectPINrestoreBlue extends PureComponent<Props, *> {
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.blue.step2'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.restore.instructions.blue.step2">
{'Tap on'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Restore configuration'}
</Text>
</Trans>
</Box>
),
},
{
key: 'step3',

18
src/components/Onboarding/steps/SelectPIN/SelectPINrestoreNano.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import type { T } from 'types/common'
@ -34,7 +35,20 @@ class SelectPINrestoreNano extends PureComponent<Props, *> {
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:selectPIN.restore.instructions.nano.step3'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:selectPIN.restore.instructions.nano.step3">
{'Press the left button to cancel'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Initialize as new device?'}
</Text>
{'Press the right button to select'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Restore configuration?'}
</Text>
</Trans>
</Box>
),
},
{
key: 'step4',

36
src/components/Onboarding/steps/SelectPIN/index.js

@ -3,7 +3,9 @@
import React from 'react'
import Box from 'components/base/Box'
import TrackPage from 'analytics/TrackPage'
import GrowScroll from 'components/base/GrowScroll'
import { Title, FixedTopContainer } from '../../helperComponents'
import OnboardingFooter from '../../OnboardingFooter'
import SelectPINnano from './SelectPINnano'
@ -18,21 +20,29 @@ export default (props: StepProps) => {
return (
<FixedTopContainer>
{onboarding.flowType === 'restoreDevice' ? (
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.restore.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINrestoreNano /> : <SelectPINrestoreBlue />}
<GrowScroll pb={7}>
<TrackPage
category="Onboarding"
name="Choose PIN"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
{onboarding.flowType === 'restoreDevice' ? (
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.restore.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINrestoreNano /> : <SelectPINrestoreBlue />}
</Box>
</Box>
</Box>
) : (
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.initialize.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />}
) : (
<Box grow alignItems="center">
<Title>{t('onboarding:selectPIN.initialize.title')}</Title>
<Box align="center" mt={7}>
{onboarding.isLedgerNano ? <SelectPINnano /> : <SelectPINblue />}
</Box>
</Box>
</Box>
)}
)}
</GrowScroll>
<OnboardingFooter horizontal flow={2} t={t} nextStep={nextStep} prevStep={prevStep} />
</FixedTopContainer>
)

17
src/components/Onboarding/steps/SetPassword.js

@ -8,6 +8,7 @@ import { setEncryptionKey } from 'helpers/db'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TrackPage from 'analytics/TrackPage'
import IconChevronRight from 'icons/ChevronRight'
@ -72,7 +73,7 @@ class SetPassword extends PureComponent<StepProps, State> {
}
render() {
const { nextStep, prevStep, t, settings } = this.props
const { nextStep, prevStep, t, settings, onboarding } = this.props
const { newPassword, currentPassword, incorrectPassword, confirmPassword } = this.state
const isPasswordEnabled = settings.password.isEnabled === true
@ -97,6 +98,12 @@ class SetPassword extends PureComponent<StepProps, State> {
return (
<FixedTopContainer>
<TrackPage
category="Onboarding"
name="Set Password"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<StepContainerInner>
<Fragment>
<Box alignItems="center">
@ -128,7 +135,13 @@ class SetPassword extends PureComponent<StepProps, State> {
{t('app:common.back')}
</Button>
<Box horizontal ml="auto">
<Button padded disabled={false} onClick={() => nextStep()} mx={2}>
<Button
padded
event="Onboarding Skip Password"
onClick={() => nextStep()}
disabled={false}
mx={2}
>
{t('app:common.skipThisStep')}
</Button>
<Button

11
src/components/Onboarding/steps/Start.js

@ -5,17 +5,22 @@ import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import TrackPage from 'analytics/TrackPage'
import type { StepProps } from '..'
import { Title } from '../helperComponents'
import { Title, LiveLogo } from '../helperComponents'
export default (props: StepProps) => {
const { jumpStep, t } = props
return (
<Box sticky justifyContent="center">
<TrackPage category="Onboarding" name="Start" />
<Box alignItems="center">
<img alt="" src={i('get-started-onb.svg')} />
<Box my={4}>
<LiveLogo
style={{ width: 80, height: 80 }}
icon={<img src={i('ledgerlive-logo.svg')} alt="" width={50} height={50} />}
/>
<Box my={5}>
<Title>{t('onboarding:start.title')}</Title>
</Box>
<Button padded primary onClick={() => jumpStep('init')}>

19
src/components/Onboarding/steps/WriteSeed/WriteSeedBlue.js

@ -1,12 +1,13 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import IconChevronRight from 'icons/ChevronRight'
import {
@ -35,7 +36,21 @@ class WriteSeedBlue extends PureComponent<Props, *> {
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.initialize.blue.step2'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:writeSeed.initialize.blue.step2">
{'Tap'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Next'}
</Text>
{'to move to the next words. Repeat the process until the'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Confirmation'}
</Text>
{'screen appears.'}
</Trans>
</Box>
),
},
{
key: 'step3',

27
src/components/Onboarding/steps/WriteSeed/WriteSeedNano.js

@ -1,12 +1,13 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import IconChevronRight from 'icons/ChevronRight'
import {
@ -30,12 +31,32 @@ class WriteSeedNano extends PureComponent<Props, *> {
{
key: 'step1',
icon: <IconOptionRow>{'1.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.initialize.nano.step1'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:writeSeed.initialize.nano.step1">
{'Copy the word displayed below'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Word #1'}
</Text>
{'in position 1 on a blank Recovery sheet.'}
</Trans>
</Box>
),
},
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.initialize.nano.step2'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:writeSeed.initialize.nano.step2">
{'Press the right button to display'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Word #2'}
</Text>
{'and repeat the process until all 24 words are copied on the Recovery sheet.'}
</Trans>
</Box>
),
},
{
key: 'step3',

27
src/components/Onboarding/steps/WriteSeed/WriteSeedRestore.js

@ -1,10 +1,11 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import { translate, Trans } from 'react-i18next'
import { colors } from 'styles/theme'
import { i } from 'helpers/staticPath'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import type { T } from 'types/common'
import type { OnboardingState } from 'reducers/onboarding'
@ -38,12 +39,32 @@ class WriteSeedRestore extends PureComponent<Props, *> {
{
key: 'step2',
icon: <IconOptionRow>{'2.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.nano.step2'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:writeSeed.restore.nano.step2">
{'Select the first letters of'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Word #1'}
</Text>
{'by pressing the right or left button. Press both buttons to confirm each letter.'}
</Trans>
</Box>
),
},
{
key: 'step3',
icon: <IconOptionRow>{'3.'}</IconOptionRow>,
desc: t('onboarding:writeSeed.restore.nano.step3'),
desc: (
<Box style={{ display: 'block' }}>
<Trans i18nKey="onboarding:writeSeed.restore.nano.step3">
{'Select'}
<Text ff="Open Sans|SemiBold" color="dark">
{'Word #1'}
</Text>
{'from the suggested words. Press both buttons to continue.'}
</Trans>
</Box>
),
},
{
key: 'step4',

30
src/components/Onboarding/steps/WriteSeed/index.js

@ -3,9 +3,11 @@
import React from 'react'
import Box from 'components/base/Box'
import TrackPage from 'analytics/TrackPage'
import OnboardingFooter from '../../OnboardingFooter'
import GrowScroll from 'components/base/GrowScroll'
import OnboardingFooter from '../../OnboardingFooter'
import WriteSeedNano from './WriteSeedNano'
import WriteSeedBlue from './WriteSeedBlue'
import WriteSeedRestore from './WriteSeedRestore'
@ -17,15 +19,23 @@ export default (props: StepProps) => {
return (
<FixedTopContainer>
<Box grow alignItems="center">
{onboarding.flowType === 'restoreDevice' ? (
<WriteSeedRestore onboarding={onboarding} />
) : onboarding.isLedgerNano ? (
<WriteSeedNano />
) : (
<WriteSeedBlue />
)}
</Box>
<GrowScroll pb={7}>
<TrackPage
category="Onboarding"
name="Recovery Phase"
flowType={onboarding.flowType}
deviceType={onboarding.isLedgerNano ? 'Nano S' : 'Blue'}
/>
<Box grow alignItems="center">
{onboarding.flowType === 'restoreDevice' ? (
<WriteSeedRestore onboarding={onboarding} />
) : onboarding.isLedgerNano ? (
<WriteSeedNano />
) : (
<WriteSeedBlue />
)}
</Box>
</GrowScroll>
<OnboardingFooter
horizontal
align="center"

12
src/components/OperationsList/index.js

@ -25,6 +25,7 @@ import IconAngleDown from 'icons/AngleDown'
import Box, { Card } from 'components/base/Box'
import Text from 'components/base/Text'
import Defer from 'components/base/Defer'
import Track from 'analytics/Track'
import SectionTitle from './SectionTitle'
import OperationC from './Operation'
@ -129,6 +130,17 @@ export class OperationsList extends PureComponent<Props, State> {
</Card>
</Box>
))}
{groupedOperations.completed ? (
<Track
onMount
event="OperationsListEndReached"
totalSections={groupedOperations.sections.length}
totalOperations={groupedOperations.sections.reduce(
(sum, s) => sum + s.data.length,
0,
)}
/>
) : null}
{!groupedOperations.completed ? (
<ShowMore onClick={this.fetchMoreOperations}>
<span>{t('app:common.showMore')}</span>

1
src/components/RenderError.js

@ -116,6 +116,7 @@ ${error.stack}
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}

2
src/components/SettingsPage/SettingsSection.js

@ -102,7 +102,7 @@ export function SettingsSectionRow({
}) {
return (
<SettingsSectionRowContainer onClick={onClick} tabIndex={-1}>
<Box grow shrink style={{ marginRight: '20%' }}>
<Box grow shrink style={{ marginRight: '10%' }}>
<Box ff="Open Sans|SemiBold" color="dark" fontSize={4}>
{title}
</Box>

12
src/components/SettingsPage/sections/About.js

@ -14,6 +14,7 @@ import { Tabbable } from 'components/base/Box'
import { openModal } from 'reducers/modals'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import TrackPage from 'analytics/TrackPage'
import {
SettingsSection as Section,
@ -38,12 +39,6 @@ const ITEMS = [
desc: t => t('app:settings.about.faqDesc'),
url: 'https://support.ledgerwallet.com/hc/en-us',
},
{
key: 'contact',
title: t => t('app:settings.about.contactUs'),
desc: t => t('app:settings.about.contactUsDesc'),
url: 'https://support.ledgerwallet.com/hc/en-us/requests/new',
},
{
key: 'terms',
title: t => t('app:settings.about.terms'),
@ -61,6 +56,7 @@ class SectionAbout extends PureComponent<Props> {
return (
<Section>
<TrackPage category="Settings" name="About" />
<Header
icon={<IconHelp size={16} />}
title={t('app:settings.tabs.about')}
@ -103,9 +99,9 @@ class AboutRowItem extends PureComponent<{
const { onClick, title, desc, url } = this.props
const boundOnClick = () => onClick(url)
return (
<Row onClick={boundOnClick} title={title} desc={desc}>
<Row title={title} desc={desc}>
<Tabbable p={2} borderRadius={1} onClick={boundOnClick}>
<IconExternalLink size={16} />
<IconExternalLink style={{ cursor: 'pointer' }} size={16} />
</Tabbable>
</Row>
)

8
src/components/SettingsPage/sections/Currencies.js

@ -18,6 +18,7 @@ import type { SettingsState } from 'reducers/settings'
import { currenciesSelector } from 'reducers/accounts'
import { currencySettingsDefaults } from 'helpers/SettingsDefaults'
import TrackPage from 'analytics/TrackPage'
import SelectCurrency from 'components/SelectCurrency'
import StepperNumber from 'components/base/StepperNumber'
import ExchangeSelect from 'components/SelectExchange'
@ -94,6 +95,7 @@ class TabCurrencies extends PureComponent<Props, State> {
const defaults = currencySettingsDefaults(currency)
return (
<Section key={currency.id}>
<TrackPage category="Settings" name="Currencies" />
<Header
icon={<IconCurrencies size={16} />}
title={t('app:settings.tabs.currencies')}
@ -113,9 +115,11 @@ class TabCurrencies extends PureComponent<Props, State> {
{currency !== intermediaryCurrency ? (
<Row
title={t('app:settings.currencies.exchange', {
ticker: `${currency.ticker}${intermediaryCurrency.ticker}`,
ticker: currency.ticker,
})}
desc={t('app:settings.currencies.exchangeDesc', {
currencyName: currency.name,
})}
desc={t('app:settings.currencies.exchangeDesc')}
>
<ExchangeSelect
small

74
src/components/SettingsPage/sections/Display.js

@ -13,7 +13,7 @@ import {
import type { SettingsState as Settings } from 'reducers/settings'
import type { T } from 'types/common'
import Box from 'components/base/Box'
import TrackPage from 'analytics/TrackPage'
import SelectExchange from 'components/SelectExchange'
import Select from 'components/base/Select'
import RadioGroup from 'components/base/RadioGroup'
@ -52,9 +52,9 @@ type Props = {
type State = {
cachedMarketIndicator: string,
cachedLanguageKey: string,
cachedLanguageKey: ?string,
cachedCounterValue: ?Object,
cachedRegion: string,
cachedRegion: ?string,
}
class TabProfile extends PureComponent<Props, State> {
@ -131,9 +131,12 @@ class TabProfile extends PureComponent<Props, State> {
const counterValueCurrency = counterValueCurrencyLocalSelector(settings)
const counterValueExchange = counterValueExchangeLocalSelector(settings)
const languages = languageKeys.map(key => ({ value: key, label: t(`language:${key}`) }))
const currentLanguage = languages.find(l => l.value === cachedLanguageKey)
const languages = [{ value: null, label: t(`language:system`) }].concat(
languageKeys.map(key => ({ value: key, label: t(`language:${key}`) })),
)
const regionsFiltered = regions.filter(({ language }) => cachedLanguageKey === language)
const currentLanguage = languages.find(l => l.value === cachedLanguageKey)
const currentRegion =
regionsFiltered.find(({ region }) => cachedRegion === region) || regionsFiltered[0]
@ -143,6 +146,7 @@ class TabProfile extends PureComponent<Props, State> {
return (
<Section>
<TrackPage category="Settings" name="Display" />
<Header
icon={<IconDisplay size={16} />}
title={t('app:settings.tabs.display')}
@ -153,16 +157,27 @@ class TabProfile extends PureComponent<Props, State> {
title={t('app:settings.display.counterValue')}
desc={t('app:settings.display.counterValueDesc')}
>
<Box flow={2}>
<Select
small
minWidth={250}
onChange={this.handleChangeCounterValue}
itemToString={item => (item ? item.name : '')}
renderSelected={item => item && item.name}
options={fiats}
value={cvOption}
/>
<Select
small
minWidth={250}
onChange={this.handleChangeCounterValue}
itemToString={item => (item ? item.name : '')}
renderSelected={item => item && item.name}
options={fiats}
value={cvOption}
/>
</Row>
{counterValueCurrency ? (
<Row
title={t('app:settings.display.exchange', {
ticker: counterValueCurrency.ticker,
fiat: counterValueCurrency.name,
})}
desc={t('app:settings.display.exchangeDesc', {
fiat: counterValueCurrency.name,
ticker: counterValueCurrency.ticker,
})}
>
<SelectExchange
small
from={intermediaryCurrency}
@ -171,8 +186,8 @@ class TabProfile extends PureComponent<Props, State> {
onChange={this.handleChangeExchange}
minWidth={200}
/>
</Box>
</Row>
</Row>
) : null}
<Row
title={t('app:settings.display.language')}
desc={t('app:settings.display.languageDesc')}
@ -187,16 +202,21 @@ class TabProfile extends PureComponent<Props, State> {
options={languages}
/>
</Row>
<Row title={t('app:settings.display.region')} desc={t('app:settings.display.regionDesc')}>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
</Row>
{regionsFiltered.length === 0 ? null : (
<Row
title={t('app:settings.display.region')}
desc={t('app:settings.display.regionDesc')}
>
<Select
small
minWidth={250}
onChange={this.handleChangeRegion}
renderSelected={item => item && item.name}
value={currentRegion}
options={regionsFiltered}
/>
</Row>
)}
<Row title={t('app:settings.display.stock')} desc={t('app:settings.display.stockDesc')}>
<RadioGroup
items={this.getMarketIndicators()}

39
src/components/SettingsPage/sections/Profile.js

@ -15,6 +15,8 @@ import hardReset from 'helpers/hardReset'
import type { SettingsState } from 'reducers/settings'
import type { T } from 'types/common'
import Track from 'analytics/Track'
import TrackPage from 'analytics/TrackPage'
import ExportLogsBtn from 'components/ExportLogsBtn'
import CheckBox from 'components/base/CheckBox'
import Box from 'components/base/Box'
@ -145,6 +147,7 @@ class TabProfile extends PureComponent<Props, State> {
const isPasswordEnabled = settings.password.isEnabled === true
return (
<Section>
<TrackPage category="Settings" name="Profile" />
<Header
icon={<IconUser size={16} />}
title={t('app:settings.tabs.profile')}
@ -155,6 +158,7 @@ class TabProfile extends PureComponent<Props, State> {
title={t('app:settings.profile.password')}
desc={t('app:settings.profile.passwordDesc')}
>
<Track onUpdate event={isPasswordEnabled ? 'PasswordEnabled' : 'PasswordDisabled'} />
<Box horizontal flow={2} align="center">
{isPasswordEnabled && (
<Button onClick={this.handleOpenPasswordModal}>
@ -164,19 +168,11 @@ class TabProfile extends PureComponent<Props, State> {
<CheckBox isChecked={isPasswordEnabled} onChange={this.handleChangePasswordCheck} />
</Box>
</Row>
<Row
title={t('app:settings.profile.developerMode')}
desc={t('app:settings.profile.developerModeDesc')}
>
<CheckBox
isChecked={settings.developerMode}
onChange={developerMode => saveSettings({ developerMode })}
/>
</Row>
<Row
title={t('app:settings.profile.reportErrors')}
desc={t('app:settings.profile.reportErrorsDesc')}
>
<Track onUpdate event={settings.sentryLogs ? 'SentryEnabled' : 'SentryDisabled'} />
<CheckBox
isChecked={settings.sentryLogs}
onChange={sentryLogs => saveSettings({ sentryLogs })}
@ -186,30 +182,44 @@ class TabProfile extends PureComponent<Props, State> {
title={t('app:settings.profile.analytics')}
desc={t('app:settings.profile.analyticsDesc')}
>
<Track
onUpdate
event={settings.shareAnalytics ? 'AnalyticsEnabled' : 'AnalyticsDisabled'}
/>
<CheckBox
isChecked={settings.shareAnalytics}
onChange={shareAnalytics => saveSettings({ shareAnalytics })}
/>
</Row>
<Row
title={t('app:settings.profile.developerMode')}
desc={t('app:settings.profile.developerModeDesc')}
>
<Track onUpdate event={settings.developerMode ? 'DevModeEnabled' : 'DevModeDisabled'} />
<CheckBox
isChecked={settings.developerMode}
onChange={developerMode => saveSettings({ developerMode })}
/>
</Row>
<Row
title={t('app:settings.profile.softResetTitle')}
desc={t('app:settings.profile.softResetDesc')}
>
<Button primary onClick={this.handleOpenSoftResetModal}>
<Button primary onClick={this.handleOpenSoftResetModal} event="ClearCacheIntent">
{t('app:settings.profile.softReset')}
</Button>
</Row>
<Row title={t('app:settings.exportLogs.title')} desc={t('app:settings.exportLogs.desc')}>
<ExportLogsBtn />
</Row>
<Row
title={t('app:settings.profile.hardResetTitle')}
desc={t('app:settings.profile.hardResetDesc')}
>
<Button danger onClick={this.handleOpenHardResetModal}>
<Button danger onClick={this.handleOpenHardResetModal} event="HardResetIntent">
{t('app:settings.profile.hardReset')}
</Button>
</Row>
<Row title={t('app:settings.exportLogs.title')} desc={t('app:settings.exportLogs.desc')}>
<ExportLogsBtn />
</Row>
</Body>
<ConfirmModal
@ -230,6 +240,7 @@ class TabProfile extends PureComponent<Props, State> {
onClose={this.handleCloseHardResetModal}
onReject={this.handleCloseHardResetModal}
onConfirm={this.handleHardReset}
confirmText={t('app:common.reset')}
title={t('app:settings.hardResetModal.title')}
desc={t('app:settings.hardResetModal.desc')}
renderIcon={this.hardResetIconRender}

8
src/components/TopBar/index.js

@ -119,9 +119,11 @@ class TopBar extends PureComponent<Props> {
<Box justifyContent="center">
<Bar />
</Box>
<ItemContainer isInteractive justifyContent="center" onClick={this.handleLock}>
<IconLock size={16} />
</ItemContainer>
<Tooltip render={() => t('app:common.lock')}>
<ItemContainer isInteractive justifyContent="center" onClick={this.handleLock}>
<IconLock size={16} />
</ItemContainer>
</Tooltip>
</Fragment>
)}
</Box>

15
src/components/TranslatedError.js

@ -12,21 +12,24 @@ import type { T } from 'types/common'
type Props = {
error: ?Error,
t: T,
field: 'title' | 'description',
}
class TranslatedError extends PureComponent<Props> {
static defaultProps = {
field: 'title',
}
render() {
const { t, error } = this.props
const { t, error, field } = this.props
if (!error) return null
if (typeof error === 'string') return error
if (error.name) {
const translation = t(`errors:${error.name}`, error)
if (translation) {
return translation
}
const translation = t(`errors:${error.name}.${field}`, error)
// FIXME in case the error don't exist in t we should not return and fallback code after. I just don't know how to check this. FIXME
return translation
}
logger.warn(`TranslatedError: no translation for '${error.name}'`, error)
return error.message || error.name || t('errors:generic')
return error.message || error.name || t(`errors:generic.${field}`)
}
}

3
src/components/UpdateNotifier/UpdateDownloaded.js

@ -35,7 +35,8 @@ const Container = styled(Box).attrs({
px: 3,
bg: 'wallet',
color: 'white',
mt: '-35px',
mt: '-50px',
mb: '35px',
style: p => ({
transform: `translate3d(0, ${p.offset}%, 0)`,
}),

81
src/components/Workflow/EnsureDashboard.js

@ -1,81 +0,0 @@
// @flow
import { PureComponent } from 'react'
import isEqual from 'lodash/isEqual'
import type { Node } from 'react'
import type { Device } from 'types/common'
import getDeviceInfo from 'commands/getDeviceInfo'
import type { DeviceInfo } from 'helpers/devices/getDeviceInfo'
type Error = {
message: string,
stack: string,
}
type Props = {
device: ?Device,
children: (deviceInfo: ?DeviceInfo, error: ?Error) => Node,
}
type State = {
deviceInfo: ?DeviceInfo,
error: ?Error,
}
class EnsureDashboard extends PureComponent<Props, State> {
static defaultProps = {
children: null,
device: null,
}
state = {
deviceInfo: null,
error: null,
}
componentDidMount() {
this.checkForDashboard()
}
componentDidUpdate({ device }: Props) {
if (this.props.device !== device && this.props.device) {
this.checkForDashboard()
}
}
componentWillUnmount() {
this._unmounting = true
}
_checking = false
_unmounting = false
checkForDashboard = async () => {
const { device } = this.props
if (device && !this._checking) {
this._checking = true
try {
const deviceInfo = await getDeviceInfo.send({ devicePath: device.path }).toPromise()
if (!isEqual(this.state.deviceInfo, deviceInfo) || this.state.error) {
!this._unmounting && this.setState({ deviceInfo, error: null })
}
} catch (err) {
if (!isEqual(err, this.state.error)) {
!this._unmounting && this.setState({ error: err, deviceInfo: null })
}
}
this._checking = false
}
}
render() {
const { deviceInfo, error } = this.state
const { children } = this.props
return children(deviceInfo, error)
}
}
export default EnsureDashboard

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save