diff --git a/README.md b/README.md index bcf05541..e0a9745b 100644 --- a/README.md +++ b/README.md @@ -5,81 +5,161 @@ :warning: Disclaimer: this project is under active development. Use at your own risks. -## Installation + -#### Requirements +> Ledger Live Desktop is a new generation Ledger Wallet application build with React, Redux and Electron to run natively on the web. The main goal of the app is to provide our users with a single wallet for all crypto currencies supported by our devices. To learn more check out [Ledger](https://www.ledgerwallet.com/?utm_source=redirection&utm_medium=variable) -Project has been tested with +## Architecture -- [NodeJS](https://nodejs.org) v9.3.0 -- [Yarn](https://yarnpkg.com) v1.3.0 +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. + +

+ +

+ +## Setup + +### Requirements + +- [NodeJS](https://nodejs.org) LTS +- [Yarn](https://yarnpkg.com) LTS - [Python](https://www.python.org/) v2.7.10 (used by [node-gyp](https://github.com/nodejs/node-gyp) to build native addons) - You will also need a C++ compiler -#### Optional +### Optional -- `Museo Sans` font - for Ledger guys, [follow that link](https://drive.google.com/drive/folders/14R6kGFtx53DuqTyIOjnT7BGogzeyMSzN), download `museosans.zip` and extract it inside the `static/fonts/museosans` directory +- In the application we use `Museo Sans` font. To include it in the app, you need to have a zip file `museosans.zip` which you should extract and place inside the `static/fonts/museosans` directory -#### Setup +## Install -1. Install dependencies +1. Clone or fork the repo + +```bash +git clone git@github.com:LedgerHQ/ledger-live-desktop.git +``` + +2. Install dependencies ```bash yarn ``` -2. Create `.env` file +## Run + +Launch the app ```bash -# ENV VARIABLES -# ------------- +yarn start +``` + +## Build -# Where errors will be tracked (you may not want to edit this line) -# SENTRY_URL= +```bash +# Build & package the whole app +# Creates a .dmg for Mac, .exe installer for Windows, or .AppImage for Linux +# Output files will be created in dist/ folder +yarn dist +``` -# OPTIONAL ENV VARIABLES -# ---------------------- +**Note:** Use `yarn dist:dir` to speed up the process: it will skip the packaging step. Handy for debugging builds. You can also use `BUNDLE_ANALYZER=1 yarn dist:dir` to generate [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) report. -# API base url, fallback to our API if not set -API_BASE_URL=http://... +--- -# Setup device debug mode -DEBUG_DEVICE=0 +## Config (optional helpers) -# Developer tools position (used only in dev) -# can be one of: right, bottom, undocked, detach -DEV_TOOLS_MODE=bottom +### Environment variables -# Filter debug output -DEBUG=lwd*,-lwd:syncb +(you can use a .env or export environment variables) -# hide the dev window +```bash +DEV_TOOLS_MODE=bottom # devtools position Options: right, bottom, undocked, detach HIDE_DEV_WINDOW=0 + +## flags for development purpose +DEBUG_DEVICE=1 +DEBUG_NETWORK=1 +DEBUG_COMMANDS=1 +DEBUG_DB=1 +DEBUG_ACTION=1 +DEBUG_TAB_KEY=1 +DEBUG_LIBCORE=1 +DEBUG_WS=1 +LEDGER_RESET_ALL=1 +LEDGER_DEBUG_ALL_LANGS=1 +SKIP_GENUINE=1 +SKIP_ONBOARDING=1 +SHOW_LEGACY_NEW_ACCOUNT=1 +HIGHLIGHT_I18N=1 + +## constants +GET_CALLS_TIMEOUT=30000 +GET_CALLS_RETRY=2 +SYNC_MAX_CONCURRENT=6 +SYNC_BOOT_DELAY=2000 +SYNC_ALL_INTERVAL=60000 +CHECK_APP_INTERVAL_WHEN_INVALID=600 +CHECK_APP_INTERVAL_WHEN_VALID=1200 +CHECK_UPDATE_DELAY=5000 +DEVICE_DISCONNECT_DEBOUNCE=500 ``` -#### Development commands +### Launch storybook -```bash -# Launch the app -yarn start +We use [storybook](https://storybook.js.org/) for UI development. -# Launch the storybook +```bash yarn storybook +``` -# Code quality checks +### Run code quality checks + +```bash yarn lint # launch eslint yarn prettier # launch prettier yarn flow # launch flow yarn test # launch unit tests ``` -#### Building from source +### Programmaically reset hard the app + +Stop the app and to clean accounts, settings, etc, run ```bash -# Build & package the whole app -# Creates a .dmg for Mac, .exe installer for Windows, or .AppImage for Linux -# Output files will be created in dist/ folder -yarn dist +rm -rf ~/Library/Application\ Support/Electron/ ``` -**Note:** Use `yarn dist:dir` to speed up the process: it will skip the packaging step. Handy for debugging builds. You can also use `BUNDLE_ANALYZER=1 yarn dist:dir` to generate [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) report. +## File structure + +``` +. +├── dist : output folder generate by the build +├── scripts : commands (for building, releasing,...) +├── src +│   ├── internals : code that run on the 'internal' thread. +│   ├── main : code that run on the 'main' thread. +│   ├── renderer : code that run on the 'renderer' thread +│   ├── components : all the React components +| └── modals : sub levels for the modals +│   ├── api : related to HTTP APIs +│   ├── bridge : an abstraction on top of blockchains apis (libcore / js impls) +│   ├── commands : an abstraction to run code over the internal thread +│   ├── icons : all the icons of our app, as React components. +│   ├── config : contains the constants,... +│   ├── helpers : generic folder for our business logic (might be reorganized in the future) +│   ├── middlewares : redux middlewares +│   ├── actions : redux actions +│   ├── reducers : redux reducers +│   ├── sentry : for our bug tracker +│   ├── stories : for storybook +│   ├── styles : theme +│   ├── logger.js : abstraction for all our console.log s +│   └── types : global flow types +├── static +│   ├── docs +│   ├── fonts +│   ├── i18n +│   ├── images +│   └── videos +├── webpack : build configuration +└── yarn.lock +``` diff --git a/package.json b/package.json index 73a0d7c9..c13ba64b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@ledgerhq/hw-transport": "^4.13.0", "@ledgerhq/hw-transport-node-hid": "^4.13.0", "@ledgerhq/ledger-core": "2.0.0-rc.1", - "@ledgerhq/live-common": "2.30.0", + "@ledgerhq/live-common": "2.31.0", "async": "^2.6.1", "axios": "^0.18.0", "babel-runtime": "^6.26.0", @@ -96,6 +96,7 @@ "secp256k1": "3.3.1", "semaphore": "^1.1.0", "semver": "^5.5.0", + "smoothscroll-polyfill": "^0.4.3", "source-map": "0.7.3", "source-map-support": "^0.5.4", "styled-components": "^3.3.2", diff --git a/src/api/Ledger.js b/src/api/Ledger.js index 2c84ab5d..f84d0f7c 100644 --- a/src/api/Ledger.js +++ b/src/api/Ledger.js @@ -1,7 +1,6 @@ // @flow import type { Currency } from '@ledgerhq/live-common/lib/types' - -const BASE_URL = process.env.LEDGER_REST_API_BASE || 'https://api.ledgerwallet.com/' +import { LEDGER_REST_API_BASE } from 'config/constants' export const blockchainBaseURL = ({ ledgerExplorerId }: Currency): ?string => - ledgerExplorerId ? `${BASE_URL}blockchain/v2/${ledgerExplorerId}` : null + ledgerExplorerId ? `${LEDGER_REST_API_BASE}blockchain/v2/${ledgerExplorerId}` : null diff --git a/src/api/Ripple.js b/src/api/Ripple.js index 52a7fe2f..8f0d5fd4 100644 --- a/src/api/Ripple.js +++ b/src/api/Ripple.js @@ -1,7 +1,6 @@ // @flow import logger from 'logger' import { RippleAPI } from 'ripple-lib' -import type { CryptoCurrency } from '@ledgerhq/live-common/lib/types' import { parseCurrencyUnit, getCryptoCurrencyById, @@ -10,14 +9,11 @@ import { const rippleUnit = getCryptoCurrencyById('ripple').units[0] -const apiEndpoint = { - ripple: 'wss://s1.ripple.com', -} +export const defaultEndpoint = 'wss://s2.ripple.com' -export const apiForCurrency = (currency: CryptoCurrency) => { - const api = new RippleAPI({ - server: apiEndpoint[currency.id], - }) +export const apiForEndpointConfig = (endpointConfig: ?string = null) => { + const server = endpointConfig || defaultEndpoint + const api = new RippleAPI({ server }) api.on('error', (errorCode, errorMessage) => { logger.warn(`Ripple API error: ${errorCode}: ${errorMessage}`) }) diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index 27353db0..a8fb4621 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -10,7 +10,8 @@ import { getDerivations } from 'helpers/derivations' import getAddress from 'commands/getAddress' import signTransaction from 'commands/signTransaction' import { - apiForCurrency, + apiForEndpointConfig, + defaultEndpoint, parseAPIValue, parseAPICurrencyObject, formatAPICurrencyXRP, @@ -47,7 +48,7 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( ) async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { - const api = apiForCurrency(a.currency) + const api = apiForEndpointConfig(a.endpointConfig) try { await api.connect() const amount = formatAPICurrencyXRP(t.amount) @@ -217,10 +218,11 @@ const txToOperation = (account: Account) => ({ return op } -const getServerInfo = (perCurrencyId => currency => { - if (perCurrencyId[currency.id]) return perCurrencyId[currency.id]() +const getServerInfo = (map => endpointConfig => { + if (!endpointConfig) endpointConfig = '' + if (map[endpointConfig]) return map[endpointConfig]() const f = throttle(async () => { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig(endpointConfig) try { await api.connect() const res = await api.getServerInfo() @@ -232,7 +234,7 @@ const getServerInfo = (perCurrencyId => currency => { api.disconnect() } }, 60000) - perCurrencyId[currency.id] = f + map[endpointConfig] = f return f() })({}) @@ -244,10 +246,10 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig() try { await api.connect() - const serverInfo = await getServerInfo(currency) + const serverInfo = await getServerInfo() const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) const maxLedgerVersion = Number(ledgers[1]) @@ -342,7 +344,7 @@ const RippleJSBridge: WalletBridge = { return { unsubscribe } }, - synchronize: ({ currency, freshAddress, blockHeight }) => + synchronize: ({ endpointConfig, freshAddress, blockHeight }) => Observable.create(o => { let finished = false const unsubscribe = () => { @@ -350,11 +352,11 @@ const RippleJSBridge: WalletBridge = { } async function main() { - const api = apiForCurrency(currency) + const api = apiForEndpointConfig(endpointConfig) try { await api.connect() if (finished) return - const serverInfo = await getServerInfo(currency) + const serverInfo = await getServerInfo(endpointConfig) if (finished) return const ledgers = serverInfo.completeLedgers.split('-') const minLedgerVersion = Number(ledgers[0]) @@ -456,7 +458,7 @@ const RippleJSBridge: WalletBridge = { isValidTransaction: (a, t) => (t.amount > 0 && t.recipient && true) || false, canBeSpent: async (a, t) => { - const r = await getServerInfo(a.currency) + const r = await getServerInfo(a.endpointConfig) return t.amount + t.fee + parseAPIValue(r.validatedLedger.reserveBaseXRP) <= a.balance }, @@ -495,6 +497,13 @@ const RippleJSBridge: WalletBridge = { ), ), }), + + getDefaultEndpointConfig: () => defaultEndpoint, + + validateEndpointConfig: async endpointConfig => { + const api = apiForEndpointConfig(endpointConfig) + await api.connect() + }, } export default RippleJSBridge diff --git a/src/bridge/types.js b/src/bridge/types.js index 75bbe777..530befe4 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -111,4 +111,7 @@ export interface WalletBridge { // Implement an optimistic response for signAndBroadcast. // you likely should add the operation in account.pendingOperations but maybe you want to clean it (because maybe some are replaced / cancelled by this one?) addPendingOperation?: (account: Account, optimisticOperation: Operation) => Account; + + getDefaultEndpointConfig?: () => string; + validateEndpointConfig?: (endpointConfig: string) => Promise; } diff --git a/src/commands/getIsGenuine.js b/src/commands/getIsGenuine.js index 8b9cfa2e..c0b66b8b 100644 --- a/src/commands/getIsGenuine.js +++ b/src/commands/getIsGenuine.js @@ -6,7 +6,7 @@ import { fromPromise } from 'rxjs/observable/fromPromise' import getIsGenuine from 'helpers/devices/getIsGenuine' import { withDevice } from 'helpers/deviceAccess' -type Input = * +type Input = * // FIXME ! type Result = string const cmd: Command = createCommand('getIsGenuine', ({ devicePath, targetId }) => diff --git a/src/components/AccountPage/index.js b/src/components/AccountPage/index.js index 312c6d35..5aadb530 100644 --- a/src/components/AccountPage/index.js +++ b/src/components/AccountPage/index.js @@ -16,8 +16,15 @@ import type { T } from 'types/common' import { rgba } from 'styles/helpers' +import { saveSettings } from 'actions/settings' import { accountSelector } from 'reducers/accounts' -import { counterValueCurrencySelector, localeSelector } from 'reducers/settings' +import { + counterValueCurrencySelector, + localeSelector, + selectedTimeRangeSelector, + timeRangeDaysByKey, +} from 'reducers/settings' +import type { TimeRange } from 'reducers/settings' import { openModal } from 'reducers/modals' import IconAccountSettings from 'icons/AccountSettings' @@ -63,10 +70,12 @@ const mapStateToProps = (state, props) => ({ account: accountSelector(state, { accountId: props.match.params.id }), counterValue: counterValueCurrencySelector(state), settings: localeSelector(state), + selectedTimeRange: selectedTimeRangeSelector(state), }) const mapDispatchToProps = { openModal, + saveSettings, } type Props = { @@ -74,30 +83,20 @@ type Props = { t: T, account?: Account, openModal: Function, + saveSettings: ({ selectedTimeRange: TimeRange }) => *, + selectedTimeRange: TimeRange, } -type State = { - selectedTime: string, - daysCount: number, -} - -class AccountPage extends PureComponent { - state = { - selectedTime: 'month', - daysCount: 30, +class AccountPage extends PureComponent { + handleChangeSelectedTime = item => { + this.props.saveSettings({ selectedTimeRange: item.key }) } - handleChangeSelectedTime = item => - this.setState({ - selectedTime: item.key, - daysCount: item.value, - }) - _cacheBalance = null render() { - const { account, openModal, t, counterValue } = this.props - const { selectedTime, daysCount } = this.state + const { account, openModal, t, counterValue, selectedTimeRange } = this.props + const daysCount = timeRangeDaysByKey[selectedTimeRange] // Don't even throw if we jumped in wrong account route if (!account) { @@ -148,7 +147,7 @@ class AccountPage extends PureComponent { chartId={`account-chart-${account.id}`} counterValue={counterValue} daysCount={daysCount} - selectedTime={selectedTime} + selectedTimeRange={selectedTimeRange} renderHeader={({ totalBalance, sinceBalance, refBalance }) => ( @@ -165,7 +164,7 @@ class AccountPage extends PureComponent { @@ -177,7 +176,7 @@ class AccountPage extends PureComponent { totalBalance={totalBalance} sinceBalance={sinceBalance} refBalance={refBalance} - since={selectedTime} + since={selectedTimeRange} /> { totalBalance={totalBalance} sinceBalance={sinceBalance} refBalance={refBalance} - since={selectedTime} + since={selectedTimeRange} /> diff --git a/src/components/BalanceSummary/index.js b/src/components/BalanceSummary/index.js index 78d47a7c..9c04ec94 100644 --- a/src/components/BalanceSummary/index.js +++ b/src/components/BalanceSummary/index.js @@ -14,10 +14,10 @@ type Props = { chartColor: string, chartId: string, accounts: Account[], - selectedTime: string, + selectedTimeRange: string, daysCount: number, renderHeader?: ({ - selectedTime: *, + selectedTimeRange: *, totalBalance: number, sinceBalance: number, refBalance: number, @@ -31,7 +31,7 @@ const BalanceSummary = ({ counterValue, daysCount, renderHeader, - selectedTime, + selectedTimeRange, }: Props) => { const account = accounts.length === 1 ? accounts[0] : undefined return ( @@ -43,7 +43,7 @@ const BalanceSummary = ({ {renderHeader ? ( {renderHeader({ - selectedTime, + selectedTimeRange, // FIXME refactor these totalBalance: balanceEnd, sinceBalance: balanceStart, @@ -59,7 +59,7 @@ const BalanceSummary = ({ data={balanceHistory} height={200} currency={counterValue} - tickXScale={selectedTime} + tickXScale={selectedTimeRange} renderTickY={val => formatShort(counterValue.units[0], val)} renderTooltip={ isAvailable && !account diff --git a/src/components/CalculateBalance.js b/src/components/CalculateBalance.js index 709f7114..d94f0b6e 100644 --- a/src/components/CalculateBalance.js +++ b/src/components/CalculateBalance.js @@ -79,7 +79,12 @@ const mapStateToProps = (state: State, props: OwnProps) => { } } +const hash = ({ balanceHistory, balanceEnd }) => `${balanceHistory.length}_${balanceEnd}` + class CalculateBalance extends Component { + shouldComponentUpdate(nextProps) { + return hash(nextProps) !== hash(this.props) + } render() { const { children } = this.props return children(this.props) diff --git a/src/components/DashboardPage/index.js b/src/components/DashboardPage/index.js index 6894b765..4267a429 100644 --- a/src/components/DashboardPage/index.js +++ b/src/components/DashboardPage/index.js @@ -13,7 +13,13 @@ import type { T } from 'types/common' import { colors } from 'styles/theme' import { accountsSelector } from 'reducers/accounts' -import { counterValueCurrencySelector, localeSelector } from 'reducers/settings' +import { + counterValueCurrencySelector, + localeSelector, + selectedTimeRangeSelector, + timeRangeDaysByKey, +} from 'reducers/settings' +import type { TimeRange } from 'reducers/settings' import { reorderAccounts } from 'actions/accounts' import { saveSettings } from 'actions/settings' @@ -35,6 +41,7 @@ const mapStateToProps = createStructuredSelector({ accounts: accountsSelector, counterValue: counterValueCurrencySelector, locale: localeSelector, + selectedTimeRange: selectedTimeRangeSelector, }) const mapDispatchToProps = { @@ -48,20 +55,11 @@ type Props = { accounts: Account[], push: Function, counterValue: Currency, + selectedTimeRange: TimeRange, + saveSettings: ({ selectedTimeRange: TimeRange }) => *, } -type State = { - selectedTime: string, - daysCount: number, -} - -class DashboardPage extends PureComponent { - state = { - // save to user preference? - selectedTime: 'month', - daysCount: 30, - } - +class DashboardPage extends PureComponent { onAccountClick = account => this.props.push(`/account/${account.id}`) handleGreeting = () => { @@ -77,17 +75,15 @@ class DashboardPage extends PureComponent { return 'app:dashboard.greeting.morning' } - handleChangeSelectedTime = item => - this.setState({ - selectedTime: item.key, - daysCount: item.value, - }) + handleChangeSelectedTime = item => { + this.props.saveSettings({ selectedTimeRange: item.key }) + } _cacheBalance = null render() { - const { accounts, t, counterValue } = this.props - const { selectedTime, daysCount } = this.state + const { accounts, t, counterValue, selectedTimeRange } = this.props + const daysCount = timeRangeDaysByKey[selectedTimeRange] const timeFrame = this.handleGreeting() const totalAccounts = accounts.length @@ -111,7 +107,7 @@ class DashboardPage extends PureComponent { @@ -122,14 +118,14 @@ class DashboardPage extends PureComponent { chartId="dashboard-chart" chartColor={colors.wallet} accounts={accounts} - selectedTime={selectedTime} + selectedTimeRange={selectedTimeRange} daysCount={daysCount} - renderHeader={({ totalBalance, selectedTime, sinceBalance, refBalance }) => ( + renderHeader={({ totalBalance, selectedTimeRange, sinceBalance, refBalance }) => ( diff --git a/src/components/ExchangePage/index.js b/src/components/ExchangePage/index.js index f3f9064d..f5c7f129 100644 --- a/src/components/ExchangePage/index.js +++ b/src/components/ExchangePage/index.js @@ -41,7 +41,7 @@ class ExchangePage extends PureComponent { ] return ( - + {t('app:exchange.title')} diff --git a/src/components/FeesField/RippleKind.js b/src/components/FeesField/RippleKind.js index 2677ea45..f40a22e5 100644 --- a/src/components/FeesField/RippleKind.js +++ b/src/components/FeesField/RippleKind.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import type { Account } from '@ledgerhq/live-common/lib/types' -import { apiForCurrency, parseAPIValue } from 'api/Ripple' +import { apiForEndpointConfig, parseAPIValue } from 'api/Ripple' import InputCurrency from 'components/base/InputCurrency' import GenericContainer from './GenericContainer' @@ -24,7 +24,7 @@ class FeesField extends Component { this.sync() } async sync() { - const api = apiForCurrency(this.props.account.currency) + const api = apiForEndpointConfig(this.props.account.endpointConfig) try { await api.connect() const info = await api.getServerInfo() diff --git a/src/components/GenuineCheckModal/index.js b/src/components/GenuineCheckModal/index.js index 8b27eaa7..8afe0b3b 100644 --- a/src/components/GenuineCheckModal/index.js +++ b/src/components/GenuineCheckModal/index.js @@ -1,6 +1,6 @@ // @flow -import React, { PureComponent } from 'react' +import React, { PureComponent, Fragment } from 'react' import { translate } from 'react-i18next' import type { T } from 'types/common' @@ -11,14 +11,44 @@ import WorkflowDefault from 'components/Workflow/WorkflowDefault' type Props = { t: T, - onGenuineCheck: (isGenuine: boolean) => void, + 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 { renderBody = ({ onClose }) => { - const { t, onGenuineCheck } = this.props + 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 @@ -28,14 +58,22 @@ class GenuineCheck extends PureComponent { {t('app:genuinecheck.modal.title')} onGenuineCheck(isGenuine)} renderDefault={(device, deviceInfo, isGenuine, errors) => ( - + + + + )} /> diff --git a/src/components/Onboarding/helperComponents.js b/src/components/Onboarding/helperComponents.js index 4f9be4c5..4f33cb11 100644 --- a/src/components/Onboarding/helperComponents.js +++ b/src/components/Onboarding/helperComponents.js @@ -4,6 +4,7 @@ import styled from 'styled-components' import { radii } from 'styles/theme' import Box from 'components/base/Box' +import GrowScroll from 'components/base/GrowScroll' import IconSensitiveOperationShield from 'icons/illustrations/SensitiveOperationShield' // GENERAL @@ -16,6 +17,8 @@ export const Title = styled(Box).attrs({ text-align: center; ` +export const StepContainerInner = styled(GrowScroll).attrs({ pb: 6, align: 'center' })`` + export const Description = styled(Box).attrs({ ff: 'Museo Sans|Light', fontSize: 5, diff --git a/src/components/Onboarding/steps/Analytics.js b/src/components/Onboarding/steps/Analytics.js index d92720bc..94bdea55 100644 --- a/src/components/Onboarding/steps/Analytics.js +++ b/src/components/Onboarding/steps/Analytics.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux' import { saveSettings } from 'actions/settings' import Box from 'components/base/Box' import CheckBox from 'components/base/CheckBox' -import { Title, Description, FixedTopContainer } from '../helperComponents' +import { Title, Description, FixedTopContainer, StepContainerInner } from '../helperComponents' import OnboardingFooter from '../OnboardingFooter' import type { StepProps } from '..' @@ -51,7 +51,7 @@ class Analytics extends PureComponent { return ( - + {t('onboarding:analytics.title')} {t('onboarding:analytics.desc')} @@ -74,7 +74,7 @@ class Analytics extends PureComponent { - + { handleOpenGenuineCheckModal = () => this.setState({ isGenuineCheckModalOpened: true }) handleCloseGenuineCheckModal = (cb?: Function) => - this.setState(state => ({ ...state, isGenuineCheckModalOpened: false }), () => cb && cb()) + this.setState( + state => ({ ...state, isGenuineCheckModalOpened: false }), + () => { + // FIXME: meh + if (cb && typeof cb === 'function') { + cb() + } + }, + ) + + handleGenuineCheckPass = () => { + this.handleCloseGenuineCheckModal(() => { + this.props.updateGenuineCheck({ + isDeviceGenuine: true, + genuineCheckUnavailable: null, + }) + }) + } + handleGenuineCheckFailed = () => { + this.handleCloseGenuineCheckModal(() => { + this.props.updateGenuineCheck({ + isGenuineFail: true, + isDeviceGenuine: false, + genuineCheckUnavailable: null, + }) + }) + } - handleGenuineCheck = isGenuine => { + handleGenuineCheckUnavailable = error => { this.handleCloseGenuineCheckModal(() => { this.props.updateGenuineCheck({ - isDeviceGenuine: isGenuine, + isDeviceGenuine: false, + genuineCheckUnavailable: error, }) }) } @@ -128,7 +159,7 @@ class GenuineCheck extends PureComponent { return ( - + {t('onboarding:genuineCheck.title')} {onboarding.isLedgerNano ? ( {t('onboarding:genuineCheck.descNano')} @@ -193,6 +224,20 @@ class GenuineCheck extends PureComponent { {t('onboarding:genuineCheck.isGenuinePassed')} + ) : genuine.genuineCheckUnavailable ? ( + + + + + + {t('app:common.retry')} + + + ) : (