From 15650a0e1ddcf22f8ba6ebdbb272a95569780692 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Tue, 12 Jun 2018 16:19:07 +0200 Subject: [PATCH 01/16] refactor manager page into smaller components --- src/components/ManagerPage/AppsList.js | 12 +- src/components/ManagerPage/Dashboard.js | 51 +++++ src/components/ManagerPage/PlugYourDevice.js | 2 + src/components/ManagerPage/Workflow.js | 193 ++---------------- src/components/ManagerPage/WorkflowDefault.js | 187 +++++++++++++++++ src/components/ManagerPage/index.js | 101 ++++----- static/i18n/en/manager.yml | 21 ++ 7 files changed, 323 insertions(+), 244 deletions(-) create mode 100644 src/components/ManagerPage/Dashboard.js create mode 100644 src/components/ManagerPage/WorkflowDefault.js create mode 100644 static/i18n/en/manager.yml diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 0a001674..6e88cec5 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -4,6 +4,8 @@ import React, { PureComponent } from 'react' import styled from 'styled-components' import { translate } from 'react-i18next' +import type { Device, T } from 'types/common' + import listApps from 'commands/listApps' import installApp from 'commands/installApp' import uninstallApp from 'commands/uninstallApp' @@ -11,9 +13,10 @@ import uninstallApp from 'commands/uninstallApp' import Box from 'components/base/Box' import Modal, { ModalBody } from 'components/base/Modal' import Tooltip from 'components/base/Tooltip' -import ExclamationCircle from 'icons/ExclamationCircle' +import Text from 'components/base/Text' -import type { Device, T } from 'types/common' +import ExclamationCircle from 'icons/ExclamationCircle' +import Update from 'icons/Update' import ManagerApp from './ManagerApp' import AppSearchBar from './AppSearchBar' @@ -142,8 +145,11 @@ class AppsList extends PureComponent { isOpened={status !== 'idle' && status !== 'loading'} render={() => ( + {status === 'busy' ? ( - {'Loading...'} + + + ) : status === 'error' ? (
{'error happened'}
diff --git a/src/components/ManagerPage/Dashboard.js b/src/components/ManagerPage/Dashboard.js new file mode 100644 index 00000000..6207ddad --- /dev/null +++ b/src/components/ManagerPage/Dashboard.js @@ -0,0 +1,51 @@ +// @flow +import React from 'react' +import { translate } from 'react-i18next' + +import type { T, Device } from 'types/common' + +import Box from 'components/base/Box' +import Text from 'components/base/Text' + +import AppsList from './AppsList' +import FirmwareUpdate from './FirmwareUpdate' + +type DeviceInfo = { + targetId: number | string, + version: string, + final: boolean, + mcu: boolean, +} + +type Props = { + t: T, + device: Device, + deviceInfo: DeviceInfo, +} + +const Dashboard = ({ device, deviceInfo, t }: Props) => ( + + + + {t('manager:title')} + + + {t('manager:subtitle')} + + + + + + + + + +) + +export default translate()(Dashboard) diff --git a/src/components/ManagerPage/PlugYourDevice.js b/src/components/ManagerPage/PlugYourDevice.js index cd9a818f..3a57cd25 100644 --- a/src/components/ManagerPage/PlugYourDevice.js +++ b/src/components/ManagerPage/PlugYourDevice.js @@ -8,6 +8,8 @@ import type { T } from 'types/common' import Box, { Card } from 'components/base/Box' import Button from 'components/base/Button' +// TODO: NOT IN USE, REMOVE + type Props = { t: T, } diff --git a/src/components/ManagerPage/Workflow.js b/src/components/ManagerPage/Workflow.js index b44a12c6..f2eea510 100644 --- a/src/components/ManagerPage/Workflow.js +++ b/src/components/ManagerPage/Workflow.js @@ -1,23 +1,9 @@ // @flow import React, { PureComponent } from 'react' -import styled from 'styled-components' -import { Trans, translate } from 'react-i18next' -import isNull from 'lodash/isNull' import type { Node } from 'react' -import type { Device, T } from 'types/common' - -import { i } from 'helpers/staticPath' -import Box from 'components/base/Box' -import Space from 'components/base/Space' -import Spinner from 'components/base/Spinner' -import Text from 'components/base/Text' - -import IconCheck from 'icons/Check' -import IconExclamationCircle from 'icons/ExclamationCircle' -import IconUsb from 'icons/Usb' -import IconHome from 'icons/Home' +import type { Device } from 'types/common' import EnsureDevice from './EnsureDevice' import EnsureDashboard from './EnsureDashboard' @@ -36,7 +22,12 @@ type Error = { } type Props = { - t: T, + renderDefault: ( + device: ?Device, + deviceInfo: ?DeviceInfo, + dashboardError: ?Error, + isGenuine: ?boolean, + ) => Node, renderMcuUpdate: (deviceInfo: DeviceInfo) => Node, renderFinalUpdate: (deviceInfo: DeviceInfo) => Node, renderDashboard: (device: Device, deviceInfo: DeviceInfo) => Node, @@ -44,73 +35,15 @@ type Props = { } type State = {} -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 StepCheck = ({ checked, hasErrors }: { checked: ?boolean, hasErrors?: boolean }) => ( - - {checked ? ( - - - - ) : hasErrors ? ( - - - - ) : ( - - )} - -) - -StepCheck.defaultProps = { - hasErrors: false, -} - -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; -` - class Workflow extends PureComponent { render() { - const { renderDashboard, renderFinalUpdate, renderMcuUpdate, renderError, t } = this.props + const { + renderDashboard, + renderFinalUpdate, + renderMcuUpdate, + renderError, + renderDefault, + } = this.props return ( {(device: Device) => ( @@ -141,101 +74,7 @@ class Workflow extends PureComponent { return renderDashboard(device, deviceInfo) } - return ( - - - - connect your device - - {t('app:manager.plugYourDevice:title')} - - - {t('app:manager.plugYourDevice:desc')} - - - - {/* DEVICE CHECK */} - - - - - - - - {'Connect your '} - Ledger device - {' to your computer and enter your '} - PIN code - {' on your device'} - - - - - - - {/* DASHBOARD CHECK */} - - - - - - - - - - {'Go to the '} - {'dashboard'} - {' on your device'} - - - - - - - {/* GENUINE CHECK */} - - - - - - - - - - {'Confirm '} - {'authentication'} - {' on your device'} - - - - - - - - ) + return renderDefault(device, deviceInfo, dashboardError, isGenuine) }} )} @@ -246,4 +85,4 @@ class Workflow extends PureComponent { } } -export default translate()(Workflow) +export default Workflow diff --git a/src/components/ManagerPage/WorkflowDefault.js b/src/components/ManagerPage/WorkflowDefault.js new file mode 100644 index 00000000..1a2b3310 --- /dev/null +++ b/src/components/ManagerPage/WorkflowDefault.js @@ -0,0 +1,187 @@ +// @flow +import React from 'react' +import { Trans, translate } from 'react-i18next' +import styled from 'styled-components' +import isNull from 'lodash/isNull' +import type { Device, T } from 'types/common' + +import { i } from 'helpers/staticPath' + +import Box from 'components/base/Box' +import Space from 'components/base/Space' +import Text from 'components/base/Text' +import Spinner from 'components/base/Spinner' + +import IconCheck from 'icons/Check' +import IconExclamationCircle from 'icons/ExclamationCircle' +import IconUsb from 'icons/Usb' +import IconHome from 'icons/Home' + +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 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 StepCheck = ({ checked, hasErrors }: { checked: ?boolean, hasErrors?: boolean }) => ( + + {checked ? ( + + + + ) : hasErrors ? ( + + + + ) : ( + + )} + +) + +StepCheck.defaultProps = { + hasErrors: false, +} + +type DeviceInfo = { + targetId: number | string, + version: string, + final: boolean, + mcu: boolean, +} + +type Error = { + message: string, + stack: string, +} + +type Props = { + t: T, + device: ?Device, + deviceInfo: ?DeviceInfo, + dashboardError: ?Error, + isGenuine: boolean, +} + +const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: Props) => ( + + + + connect your device + + {t('manager:device.title')} + + + {t('manager:device.desc')} + + + + {/* DEVICE CHECK */} + + + + + + + + {'Connect your '} + Ledger device + {' to your computer and enter your '} + PIN code + {' on your device'} + + + + + + + {/* DASHBOARD CHECK */} + + + + + + + + + + {'Go to the '} + {'dashboard'} + {' on your device'} + + + + + + + {/* GENUINE CHECK */} + + + + + + + + + + {'Confirm '} + {'authentication'} + {' on your device'} + + + + + + + +) + +export default translate()(WorkflowDefault) diff --git a/src/components/ManagerPage/index.js b/src/components/ManagerPage/index.js index df97c45c..ce8ce62e 100644 --- a/src/components/ManagerPage/index.js +++ b/src/components/ManagerPage/index.js @@ -1,17 +1,13 @@ // @flow -import React, { PureComponent } from 'react' -import { translate } from 'react-i18next' +import React from 'react' import type { Node } from 'react' -import type { T, Device } from 'types/common' +import type { Device } from 'types/common' -import Box from 'components/base/Box' -import Text from 'components/base/Text' - -import AppsList from './AppsList' -import FirmwareUpdate from './FirmwareUpdate' import Workflow from './Workflow' +import WorkflowDefault from './WorkflowDefault' +import Dashboard from './Dashboard' type DeviceInfo = { targetId: number | string, @@ -25,61 +21,38 @@ type Error = { stack: string, } -type Props = { - t: T, -} - -type State = { - modalOpen: boolean, -} - -class ManagerPage extends PureComponent { - renderDashboard = (device: Device, deviceInfo: DeviceInfo) => { - const { t } = this.props - return ( - - - - {t('app:manager.title')} - - - {t('app:manager.subtitle')} - - - - - - - - - - ) - } - - render(): Node { - return ( - { - if (dashboardError) return Dashboard Error: {dashboardError.message} - if (genuineError) return Genuine Error: {genuineError.message} - return Error - }} - renderFinalUpdate={(deviceInfo: DeviceInfo) => ( -

UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}

- )} - renderMcuUpdate={(deviceInfo: DeviceInfo) => ( -

FLASH MCU (TEMPLATE + ACTION WIP) {deviceInfo.mcu}

- )} - renderDashboard={this.renderDashboard} - /> - ) - } +function ManagerPage(): Node { + return ( + { + if (dashboardError) return Dashboard Error: {dashboardError.message} + if (genuineError) return Genuine Error: {genuineError.message} + return Error + }} + renderFinalUpdate={(deviceInfo: DeviceInfo) => ( +

UPDATE FINAL FIRMARE (TEMPLATE + ACTION WIP) {deviceInfo.final}

+ )} + renderMcuUpdate={(deviceInfo: DeviceInfo) => ( +

FLASH MCU (TEMPLATE + ACTION WIP) {deviceInfo.mcu}

+ )} + renderDashboard={(device: Device, deviceInfo: DeviceInfo) => ( + + )} + renderDefault={( + device: ?Device, + deviceInfo: ?DeviceInfo, + dashboardError: ?Error, + isGenuine: ?boolean, + ) => ( + + )} + /> + ) } -export default translate()(ManagerPage) +export default ManagerPage diff --git a/static/i18n/en/manager.yml b/static/i18n/en/manager.yml new file mode 100644 index 00000000..48d16bdc --- /dev/null +++ b/static/i18n/en/manager.yml @@ -0,0 +1,21 @@ +tabs: + apps: Apps + device: My device +apps: + install: Install + all: Apps + help: To update an app, you have to uninstall the app and re install it. +firmware: + update: Update firmware + updateTitle: Firmware update + latest: A new firmware {{version}} is available +title: Manager +subtitle: Get all your apps here +device: + title: Plug your device + desc: Please connect your Ledger device and follow the steps below to access the manager + cta: Plug my device +errors: + noDevice: Please make sur your device is connected (TEMPLATE NEEDED) + noDashboard: Please make sure your device is on the dashboard screen (TEMPLATED NEEDED) + noGenuine: You did not approve request on your device or your device is not genuine (TEMPLATE NEEDED) \ No newline at end of file From 55387b06c444357893310c5bbc7d6a0543f6e162 Mon Sep 17 00:00:00 2001 From: Anastasia Poupeney Date: Wed, 13 Jun 2018 15:13:21 +0200 Subject: [PATCH 02/16] wip sentry logs --- src/helpers/db.js | 2 +- src/helpers/user.js | 15 +++++++++++++++ src/init-sentry.js | 6 +++--- src/main/index.js | 2 +- src/middlewares/sentry.js | 19 +++++++++++++++++++ src/reducers/settings.js | 3 ++- src/renderer/createStore.js | 4 ++-- src/renderer/index.js | 4 ++-- src/renderer/init.js | 3 +++ src/renderer/sentry/browser.js | 19 +++++++++++++++++++ src/renderer/sentry/node.js | 0 11 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 src/helpers/user.js create mode 100644 src/middlewares/sentry.js create mode 100644 src/renderer/sentry/browser.js create mode 100644 src/renderer/sentry/node.js diff --git a/src/helpers/db.js b/src/helpers/db.js index 19968185..fb1dfa65 100644 --- a/src/helpers/db.js +++ b/src/helpers/db.js @@ -6,7 +6,7 @@ import get from 'lodash/get' import { decodeAccountsModel, encodeAccountsModel } from 'reducers/accounts' -type DBKey = 'settings' | 'accounts' | 'countervalues' +type DBKey = 'settings' | 'accounts' | 'countervalues' | 'user' const encryptionKey = {} diff --git a/src/helpers/user.js b/src/helpers/user.js new file mode 100644 index 00000000..15a8b7cb --- /dev/null +++ b/src/helpers/user.js @@ -0,0 +1,15 @@ +// @flow + +import db from 'helpers/db' +import uuid from 'uuid/v4' + +// a user is an anonymous way to identify a same instance of the app + +export default () => { + let user = db.get('user') + if (!user) { + user = { id: uuid() } + db.set('user', user) + } + return user +} diff --git a/src/init-sentry.js b/src/init-sentry.js index 1af0c815..663c6840 100644 --- a/src/init-sentry.js +++ b/src/init-sentry.js @@ -1,7 +1,7 @@ const { SENTRY_URL } = process.env if (__PROD__ && SENTRY_URL) { - const Raven = require('raven') - const ravenConfig = { captureUnhandledRejections: true } - Raven.config(SENTRY_URL, ravenConfig).install() + // const Raven = require('raven') + // const ravenConfig = { captureUnhandledRejections: true } + // Raven.config(SENTRY_URL, ravenConfig).install() } diff --git a/src/main/index.js b/src/main/index.js index c0d18749..3670ac42 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,7 +4,7 @@ process.setMaxListeners(0) require('../env') require('../globals') -require('../init-sentry') +// require('../init-sentry') require('./app') setImmediate(() => require('./bridge')) diff --git a/src/middlewares/sentry.js b/src/middlewares/sentry.js new file mode 100644 index 00000000..479be983 --- /dev/null +++ b/src/middlewares/sentry.js @@ -0,0 +1,19 @@ +const Raven = require('raven-js') +require('../env') + +import { sentryLogsBooleanSelector } from 'reducers/settings' + +const { SENTRY_URL } = process.env + +let isSentryInstalled = false + +export default store => next => action => { + next(action) + if (__PROD__ && SENTRY_URL) { + const state = store.getState() + const sentryLogs = sentryLogsBooleanSelector(state) + // if (sentryLogs !== isSentryInstalled) { + // + // } + } +} diff --git a/src/reducers/settings.js b/src/reducers/settings.js index fbe330f6..fc1aa1ca 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -73,7 +73,7 @@ const INITIAL_STATE: SettingsState = { developerMode: !!process.env.__DEV__, loaded: false, shareAnalytics: false, - sentryLogs: false, + sentryLogs: true, lastUsedVersion: __APP_VERSION__, } @@ -214,5 +214,6 @@ export const exchangeSettingsForAccountSelector: ESFAS = createSelector( ) export const marketIndicatorSelector = (state: State) => state.settings.marketIndicator +export const sentryLogsBooleanSelector = (state: State) => state.settings.sentryLogs export default handleActions(handlers, INITIAL_STATE) diff --git a/src/renderer/createStore.js b/src/renderer/createStore.js index 6e14fa5e..75f46711 100644 --- a/src/renderer/createStore.js +++ b/src/renderer/createStore.js @@ -6,7 +6,7 @@ import thunk from 'redux-thunk' import createHistory from 'history/createHashHistory' import type { HashHistory } from 'history' import logger from 'middlewares/logger' - +import sentry from 'middlewares/sentry' import reducers from 'reducers' type Props = { @@ -20,7 +20,7 @@ export default ({ state, history, dbMiddleware }: Props) => { if (!history) { history = createHistory() } - const middlewares = [routerMiddleware(history), thunk, logger] + const middlewares = [routerMiddleware(history), thunk, logger, sentry] if (dbMiddleware) { middlewares.push(dbMiddleware) } diff --git a/src/renderer/index.js b/src/renderer/index.js index d65ad8bf..5ae7edce 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -7,8 +7,8 @@ require('../env') const { SENTRY_URL } = process.env if (__PROD__ && SENTRY_URL) { - Raven.config(SENTRY_URL, { allowSecretKey: true }).install() - window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) + // Raven.config(SENTRY_URL, { allowSecretKey: true }).install() + // window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) } require('./init') diff --git a/src/renderer/init.js b/src/renderer/init.js index 6ef76a10..7bc6fe3d 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -15,6 +15,7 @@ import { fetchAccounts } from 'actions/accounts' import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' import { getLanguage } from 'reducers/settings' +import { sentryLogsBooleanSelector } from 'reducers/settings' import libcoreGetVersion from 'commands/libcoreGetVersion' import db from 'helpers/db' @@ -22,6 +23,7 @@ import dbMiddleware from 'middlewares/db' import CounterValues from 'helpers/countervalues' import hardReset from 'helpers/hardReset' +import sentry from 'renderer/sentry/browser' import App from 'components/App' import 'styles/global' @@ -50,6 +52,7 @@ async function init() { const state = store.getState() const language = getLanguage(state) const locked = isLocked(state) + sentry(() => sentryLogsBooleanSelector(store.getState())) moment.locale(language) diff --git a/src/renderer/sentry/browser.js b/src/renderer/sentry/browser.js new file mode 100644 index 00000000..9ee54b29 --- /dev/null +++ b/src/renderer/sentry/browser.js @@ -0,0 +1,19 @@ +const Raven = require('raven-js') +require('../../env') +import user from 'helpers/user' + +const { SENTRY_URL } = process.env + +export default shouldSendCallback => { + Raven.config(SENTRY_URL, { + allowSecretKey: true, + release: __APP_VERSION__, + environment: __DEV__ ? 'development' : 'production', + shouldSendCallback, + }) + .setUserContext({ + ip_address: null, + id: user().id, + }) + .install() +} diff --git a/src/renderer/sentry/node.js b/src/renderer/sentry/node.js new file mode 100644 index 00000000..e69de29b From 0a6e864b0921082caacdae14fb568dce22aaa7f0 Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 13 Jun 2018 14:49:49 +0200 Subject: [PATCH 03/16] ui modal for install apps --- README.md | 9 +- src/components/ManagerPage/AppsList.js | 106 ++++++++++++------ src/components/ManagerPage/Dashboard.js | 4 +- .../ManagerPage/FinalFirmwareUpdate.js | 2 +- src/components/ManagerPage/FirmwareUpdate.js | 10 +- src/components/ManagerPage/ManagerApp.js | 5 +- src/components/ManagerPage/PlugYourDevice.js | 6 +- .../ManagerPage/UpdateFirmwareButton.js | 4 +- src/components/ManagerPage/WorkflowDefault.js | 4 +- src/components/base/Progress/index.js | 82 ++++++++++++++ src/helpers/deviceAccess.js | 1 + static/i18n/en/app.yml | 19 +++- static/i18n/en/manager.yml | 21 ---- 13 files changed, 189 insertions(+), 84 deletions(-) create mode 100644 src/components/base/Progress/index.js delete mode 100644 static/i18n/en/manager.yml diff --git a/README.md b/README.md index 6598c835..bcf05541 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,15 @@ yarn # Where errors will be tracked (you may not want to edit this line) # SENTRY_URL= -# api base url -API_BASE_URL=http://... - # OPTIONAL ENV VARIABLES # ---------------------- +# API base url, fallback to our API if not set +API_BASE_URL=http://... + +# Setup device debug mode +DEBUG_DEVICE=0 + # Developer tools position (used only in dev) # can be one of: right, bottom, undocked, detach DEV_TOOLS_MODE=bottom diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 6e88cec5..244adb41 100644 --- a/src/components/ManagerPage/AppsList.js +++ b/src/components/ManagerPage/AppsList.js @@ -14,9 +14,12 @@ import Box from 'components/base/Box' import Modal, { ModalBody } from 'components/base/Modal' import Tooltip from 'components/base/Tooltip' import Text from 'components/base/Text' +import Progress from 'components/base/Progress' import ExclamationCircle from 'icons/ExclamationCircle' import Update from 'icons/Update' +import Trash from 'icons/Trash' +import CheckCircle from 'icons/CheckCircle' import ManagerApp from './ManagerApp' import AppSearchBar from './AppSearchBar' @@ -35,6 +38,7 @@ const ICONS_FALLBACK = { } type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error' +type Mode = '' | 'installing' | 'uninstalling' type LedgerApp = { name: string, @@ -57,6 +61,8 @@ type State = { status: Status, error: string | null, appsList: LedgerApp[], + app: string, + mode: Mode, } class AppsList extends PureComponent { @@ -64,6 +70,8 @@ class AppsList extends PureComponent { status: 'loading', error: null, appsList: [], + app: '', + mode: '', } componentDidMount() { @@ -89,40 +97,87 @@ class AppsList extends PureComponent { } } - handleInstallApp = (args: { app: any }) => async () => { - const appParams = args.app - this.setState({ status: 'busy' }) + handleInstallApp = (args: { app: any, name: string }) => async () => { + const { app: appParams, name } = args + this.setState({ status: 'busy', app: name, mode: 'installing' }) try { const { device: { path: devicePath }, } = this.props const data = { appParams, devicePath } await installApp.send(data).toPromise() - this.setState({ status: 'success' }) + this.setState({ status: 'success', app: '' }) } catch (err) { - this.setState({ status: 'error', error: err.message }) + this.setState({ status: 'error', error: err.message, app: '', mode: '' }) } } - handleUninstallApp = (args: { app: any }) => async () => { - const appParams = args.app - this.setState({ status: 'busy' }) + handleUninstallApp = (args: { app: any, name: string }) => async () => { + const { app: appParams, name } = args + this.setState({ status: 'busy', app: name, mode: 'uninstalling' }) try { const { device: { path: devicePath }, } = this.props const data = { appParams, devicePath } await uninstallApp.send(data).toPromise() - this.setState({ status: 'success' }) + this.setState({ status: 'success', app: '' }) } catch (err) { - this.setState({ status: 'error', error: err.message }) + this.setState({ status: 'error', error: err.message, app: '', mode: '' }) } } - handleCloseModal = () => this.setState({ status: 'idle' }) + handleCloseModal = () => this.setState({ status: 'idle', mode: '' }) + + renderModal = () => { + const { t } = this.props + const { app, status, error, mode } = this.state + + return ( + ( + + {status === 'busy' || status === 'idle' ? ( + + {mode === 'installing' ? : } + + {t(`app:manager.apps.${mode}`, { app })} + + + + + + ) : status === 'error' ? ( + +
{'error happened'}
+ {error} + +
+ ) : status === 'success' ? ( + + + + + + {t( + `app:manager.apps.${ + mode === 'installing' ? 'installSuccess' : 'uninstallSuccess' + }`, + { app }, + )} + + + + ) : null} +
+ )} + /> + ) + } renderList() { - const { status, error, appsList } = this.state + const { appsList } = this.state return ( @@ -141,30 +196,7 @@ class AppsList extends PureComponent { )} - ( - - - {status === 'busy' ? ( - - - - ) : status === 'error' ? ( - -
{'error happened'}
- {error} - -
- ) : status === 'success' ? ( - - {'success'} - - - ) : null} -
- )} - /> + {this.renderModal()}
) } @@ -175,7 +207,7 @@ class AppsList extends PureComponent { - {t('app:manager.allApps')} + {t('app:manager.apps.all')} ( diff --git a/src/components/ManagerPage/Dashboard.js b/src/components/ManagerPage/Dashboard.js index 6207ddad..238a063e 100644 --- a/src/components/ManagerPage/Dashboard.js +++ b/src/components/ManagerPage/Dashboard.js @@ -27,10 +27,10 @@ const Dashboard = ({ device, deviceInfo, t }: Props) => ( - {t('manager:title')} + {t('app:manager.title')} - {t('manager:subtitle')} + {t('app:manager.subtitle')} diff --git a/src/components/ManagerPage/FinalFirmwareUpdate.js b/src/components/ManagerPage/FinalFirmwareUpdate.js index 0cec51f2..c8ea6422 100644 --- a/src/components/ManagerPage/FinalFirmwareUpdate.js +++ b/src/components/ManagerPage/FinalFirmwareUpdate.js @@ -41,7 +41,7 @@ class FirmwareUpdate extends PureComponent { return ( - {t('app:manager.firmwareUpdate')} + {t('app:manager.firmware.update')} diff --git a/src/components/ManagerPage/FirmwareUpdate.js b/src/components/ManagerPage/FirmwareUpdate.js index b501b59e..37ce7967 100644 --- a/src/components/ManagerPage/FirmwareUpdate.js +++ b/src/components/ManagerPage/FirmwareUpdate.js @@ -1,11 +1,12 @@ // @flow import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' import isEqual from 'lodash/isEqual' import isEmpty from 'lodash/isEmpty' import invariant from 'invariant' import logger from 'logger' -import type { Device } from 'types/common' +import type { Device, T } from 'types/common' import getLatestFirmwareForDevice from 'commands/getLatestFirmwareForDevice' import installOsuFirmware from 'commands/installOsuFirmware' @@ -31,6 +32,7 @@ type DeviceInfos = { } type Props = { + t: T, device: Device, infos: DeviceInfos, } @@ -96,7 +98,7 @@ class FirmwareUpdate extends PureComponent { } render() { - const { infos } = this.props + const { infos, t } = this.props const { latestFirmware } = this.state return ( @@ -115,7 +117,7 @@ class FirmwareUpdate extends PureComponent { - Firmware {infos.version} + {t('app:manager.firmware.installed', { version: infos.version })} @@ -125,4 +127,4 @@ class FirmwareUpdate extends PureComponent { } } -export default FirmwareUpdate +export default translate()(FirmwareUpdate) diff --git a/src/components/ManagerPage/ManagerApp.js b/src/components/ManagerPage/ManagerApp.js index 84735592..475b50d3 100644 --- a/src/components/ManagerPage/ManagerApp.js +++ b/src/components/ManagerPage/ManagerApp.js @@ -52,8 +52,7 @@ type Props = { onUninstall: Function, } -function ManagerApp(props: Props) { - const { name, version, icon, onInstall, onUninstall, t } = props +function ManagerApp({ name, version, icon, onInstall, onUninstall, t }: Props) { const iconUrl = `https://api.ledgerwallet.com/update/assets/icons/${icon}` return ( @@ -65,7 +64,7 @@ function ManagerApp(props: Props) { + ) diff --git a/src/components/ManagerPage/UpdateFirmwareButton.js b/src/components/ManagerPage/UpdateFirmwareButton.js index 25bde360..5feea0e3 100644 --- a/src/components/ManagerPage/UpdateFirmwareButton.js +++ b/src/components/ManagerPage/UpdateFirmwareButton.js @@ -22,10 +22,10 @@ const UpdateFirmwareButton = ({ t, firmware, installFirmware }: Props) => firmware ? ( - {t('app:manager.latestFirmware', { version: firmware.name })} + {t('app:manager.firmware.latest', { version: firmware.name })} ) : null diff --git a/src/components/ManagerPage/WorkflowDefault.js b/src/components/ManagerPage/WorkflowDefault.js index 1a2b3310..3dea16b3 100644 --- a/src/components/ManagerPage/WorkflowDefault.js +++ b/src/components/ManagerPage/WorkflowDefault.js @@ -111,10 +111,10 @@ const WorkflowDefault = ({ device, deviceInfo, dashboardError, isGenuine, t }: P style={{ marginBottom: 30, maxWidth: 362, width: '100%' }} /> - {t('manager:device.title')} + {t('app:manager.device.title')} - {t('manager:device.desc')} + {t('app:manager.device.desc')} diff --git a/src/components/base/Progress/index.js b/src/components/base/Progress/index.js new file mode 100644 index 00000000..ee1d9f75 --- /dev/null +++ b/src/components/base/Progress/index.js @@ -0,0 +1,82 @@ +// @flow +import React, { Component } from 'react' +import styled, { keyframes } from 'styled-components' + +import Box from 'components/base/Box' + +const inifiteAnimation = keyframes` + 0% { + left: 0 + } + 100% { + left: 102% + } +` + +const fillInAnimation = keyframes` + 0% { + transform: translate3d(-110%, 0, 0); + } + 50% { + transform: translate3d(-30%, 0, 0); + } + 100% { + transform: translate3d(0); + } +` + +const Bar = styled(Box).attrs({ + color: 'fog', + borderRadius: '2.5px', +})` + height: 5px; + width: 100%; + position: relative; + background-color: currentColor; + overflow: hidden; +` + +const Progression = styled(Bar).attrs({ + color: 'wallet', +})` + position: absolute; + top: 0; + left: 0; + ${p => + p.infinite + ? ` + animation: 1000ms ${inifiteAnimation} ease-out infinite; + ` + : ` + animation: ${p.timing}ms ${fillInAnimation} ease-out; + animation-fill-mode: forwards; + `}; +` + +type Props = { + infinite: boolean, + timing?: number, + color?: string, +} + +type State = {} + +class Progress extends Component { + static defaultProps = { + infinite: false, + timing: 3000, + color: 'wallet', + } + + render() { + const { infinite, color, timing } = this.props + const styles = infinite ? { width: '20%' } : { width: '100%' } + return ( + + + + ) + } +} + +export default Progress diff --git a/src/helpers/deviceAccess.js b/src/helpers/deviceAccess.js index b71920a8..f2210b64 100644 --- a/src/helpers/deviceAccess.js +++ b/src/helpers/deviceAccess.js @@ -19,6 +19,7 @@ export const withDevice: WithDevice = devicePath => { return job => takeSemaphorePromise(sem, async () => { const t = await retry(() => TransportNodeHid.open(devicePath), { maxRetry: 1 }) + if (process.env.DEBUG_DEVICE) t.setDebugMode(true) try { const res = await job(t) // $FlowFixMe diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index 1a13fd61..6fbc454c 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -132,16 +132,23 @@ manager: tabs: apps: Apps device: My device - installApps: Install - installFirmware: Update firmware - allApps: Apps apps: + install: Install + all: Apps + installing: 'Installing {{app}}...' + uninstalling: 'Uninstalling {{app}}...' + installSuccess: '{{app}} app successfully installed' + uninstallSuccess: '{{app}} app successfully uninstalled' + alreadyInstalled: '{{app}} app is already installed' help: To update an app, you have to uninstall the app and re install it. + firmware: + installed: 'Firmware {{version}}' + update: Update firmware + updateTitle: Firmware update + latest: 'A new firmware {{version}} is available' title: Manager subtitle: Get all your apps here - firmwareUpdate: Firmware update - latestFirmware: A new firmware {{version}} is available - plugYourDevice: + device: title: Plug your device desc: Please connect your Ledger device and follow the steps below to access the manager cta: Plug my device diff --git a/static/i18n/en/manager.yml b/static/i18n/en/manager.yml deleted file mode 100644 index 48d16bdc..00000000 --- a/static/i18n/en/manager.yml +++ /dev/null @@ -1,21 +0,0 @@ -tabs: - apps: Apps - device: My device -apps: - install: Install - all: Apps - help: To update an app, you have to uninstall the app and re install it. -firmware: - update: Update firmware - updateTitle: Firmware update - latest: A new firmware {{version}} is available -title: Manager -subtitle: Get all your apps here -device: - title: Plug your device - desc: Please connect your Ledger device and follow the steps below to access the manager - cta: Plug my device -errors: - noDevice: Please make sur your device is connected (TEMPLATE NEEDED) - noDashboard: Please make sure your device is on the dashboard screen (TEMPLATED NEEDED) - noGenuine: You did not approve request on your device or your device is not genuine (TEMPLATE NEEDED) \ No newline at end of file From 070ae1c5322a8cd3165df96245589407b9c2ba6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 15:53:06 +0200 Subject: [PATCH 04/16] connect Sentry to main & internal processes --- src/commands/testCrash.js | 8 ++++---- src/init-sentry.js | 7 ------- src/internals/index.js | 9 ++++++++- src/main/bridge.js | 21 ++++++++++++++++++++- src/main/index.js | 1 - src/middlewares/sentry.js | 17 ++++++----------- src/renderer/index.js | 11 ----------- src/renderer/init.js | 5 ++--- src/renderer/sentry/browser.js | 19 ------------------- src/renderer/sentry/node.js | 0 src/sentry/browser.js | 9 +++++++++ src/sentry/install.js | 26 ++++++++++++++++++++++++++ src/sentry/node.js | 8 ++++++++ 13 files changed, 83 insertions(+), 58 deletions(-) delete mode 100644 src/init-sentry.js delete mode 100644 src/renderer/sentry/browser.js delete mode 100644 src/renderer/sentry/node.js create mode 100644 src/sentry/browser.js create mode 100644 src/sentry/install.js create mode 100644 src/sentry/node.js diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js index 0725988a..5675a1ee 100644 --- a/src/commands/testCrash.js +++ b/src/commands/testCrash.js @@ -8,10 +8,10 @@ import { createCommand, Command } from 'helpers/ipc' type Input = void type Result = void -const cmd: Command = createCommand('testCrash', () => - Observable.create(() => { +const cmd: Command = createCommand('testCrash', () => { + return Observable.create(() => { process.exit(1) - }), -) + }) +}) export default cmd diff --git a/src/init-sentry.js b/src/init-sentry.js deleted file mode 100644 index 663c6840..00000000 --- a/src/init-sentry.js +++ /dev/null @@ -1,7 +0,0 @@ -const { SENTRY_URL } = process.env - -if (__PROD__ && SENTRY_URL) { - // const Raven = require('raven') - // const ravenConfig = { captureUnhandledRejections: true } - // Raven.config(SENTRY_URL, ravenConfig).install() -} diff --git a/src/internals/index.js b/src/internals/index.js index 94601aaa..f2879c08 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -3,14 +3,18 @@ import commands from 'commands' import logger from 'logger' import uuid from 'uuid/v4' import { setImplementation } from 'api/network' +import sentry from 'sentry/node' require('../env') -require('../init-sentry') process.title = 'Internal' const defers = {} +let sentryEnabled = process.env.INITIAL_SENTRY_ENABLED || false + +sentry(() => sentryEnabled, process.env.SENTRY_USER_ID) + if (process.env.DEBUG_NETWORK) { setImplementation(networkArg => { const id = uuid() @@ -92,6 +96,9 @@ process.on('message', m => { } else { defer.reject(payload.error) } + } else if (m.type === 'sentryLogsChanged') { + const { payload } = m + sentryEnabled = payload.value } }) diff --git a/src/main/bridge.js b/src/main/bridge.js index ff9af1ab..bd118629 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -7,6 +7,8 @@ import { ipcMain, app } from 'electron' import { ipcMainListenReceiveCommands } from 'helpers/ipc' import path from 'path' import logger from 'logger' +import sentry from 'sentry/node' +import user from 'helpers/user' import setupAutoUpdater, { quitAndInstall } from './autoUpdate' @@ -17,6 +19,11 @@ const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite') let internalProcess +let sentryEnabled = false +const userId = user().id + +sentry(() => sentryEnabled, userId) + const killInternalProcess = () => { if (internalProcess) { logger.log('killing internal process...') @@ -30,7 +37,12 @@ const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist const bootInternalProcess = () => { logger.log('booting internal process...') internalProcess = fork(forkBundlePath, { - env: { ...process.env, LEDGER_LIVE_SQLITE_PATH }, + env: { + ...process.env, + LEDGER_LIVE_SQLITE_PATH, + INITIAL_SENTRY_ENABLED: sentryEnabled, + SENTRY_USER_ID: userId, + }, }) internalProcess.on('message', handleGlobalInternalMessage) internalProcess.on('exit', code => { @@ -102,6 +114,13 @@ ipcMain.on('executeHttpQueryPayload', (event, payload) => { p.send({ type: 'executeHttpQueryPayload', payload }) }) +ipcMain.on('sentryLogsChanged', (event, payload) => { + sentryEnabled = payload.value + const p = internalProcess + if (!p) return + p.send({ type: 'sentryLogsChanged', payload }) +}) + // TODO move this to "command" pattern ipcMain.on('updater', (event, { type, data }) => { const handler = { diff --git a/src/main/index.js b/src/main/index.js index 3670ac42..65952b72 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,7 +4,6 @@ process.setMaxListeners(0) require('../env') require('../globals') -// require('../init-sentry') require('./app') setImmediate(() => require('./bridge')) diff --git a/src/middlewares/sentry.js b/src/middlewares/sentry.js index 479be983..212764c6 100644 --- a/src/middlewares/sentry.js +++ b/src/middlewares/sentry.js @@ -1,19 +1,14 @@ -const Raven = require('raven-js') -require('../env') - +import { ipcRenderer } from 'electron' import { sentryLogsBooleanSelector } from 'reducers/settings' -const { SENTRY_URL } = process.env - let isSentryInstalled = false export default store => next => action => { next(action) - if (__PROD__ && SENTRY_URL) { - const state = store.getState() - const sentryLogs = sentryLogsBooleanSelector(state) - // if (sentryLogs !== isSentryInstalled) { - // - // } + const state = store.getState() + const sentryLogs = sentryLogsBooleanSelector(state) + if (sentryLogs !== isSentryInstalled) { + isSentryInstalled = sentryLogs + ipcRenderer.send('sentryLogsChanged', { value: sentryLogs }) } } diff --git a/src/renderer/index.js b/src/renderer/index.js index 5ae7edce..5164fc17 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,14 +1,3 @@ require('@babel/polyfill') - -const Raven = require('raven-js') - require('../env') - -const { SENTRY_URL } = process.env - -if (__PROD__ && SENTRY_URL) { - // Raven.config(SENTRY_URL, { allowSecretKey: true }).install() - // window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) -} - require('./init') diff --git a/src/renderer/init.js b/src/renderer/init.js index 7bc6fe3d..c9e77136 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -14,8 +14,7 @@ import events from 'renderer/events' import { fetchAccounts } from 'actions/accounts' import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' -import { getLanguage } from 'reducers/settings' -import { sentryLogsBooleanSelector } from 'reducers/settings' +import { getLanguage, sentryLogsBooleanSelector } from 'reducers/settings' import libcoreGetVersion from 'commands/libcoreGetVersion' import db from 'helpers/db' @@ -23,7 +22,7 @@ import dbMiddleware from 'middlewares/db' import CounterValues from 'helpers/countervalues' import hardReset from 'helpers/hardReset' -import sentry from 'renderer/sentry/browser' +import sentry from 'sentry/browser' import App from 'components/App' import 'styles/global' diff --git a/src/renderer/sentry/browser.js b/src/renderer/sentry/browser.js deleted file mode 100644 index 9ee54b29..00000000 --- a/src/renderer/sentry/browser.js +++ /dev/null @@ -1,19 +0,0 @@ -const Raven = require('raven-js') -require('../../env') -import user from 'helpers/user' - -const { SENTRY_URL } = process.env - -export default shouldSendCallback => { - Raven.config(SENTRY_URL, { - allowSecretKey: true, - release: __APP_VERSION__, - environment: __DEV__ ? 'development' : 'production', - shouldSendCallback, - }) - .setUserContext({ - ip_address: null, - id: user().id, - }) - .install() -} diff --git a/src/renderer/sentry/node.js b/src/renderer/sentry/node.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/sentry/browser.js b/src/sentry/browser.js new file mode 100644 index 00000000..ac5fc55c --- /dev/null +++ b/src/sentry/browser.js @@ -0,0 +1,9 @@ +// @flow + +import Raven from 'raven-js' +import user from 'helpers/user' +import install from './install' + +export default (shouldSendCallback: () => boolean) => { + install(Raven, shouldSendCallback, user().id) +} diff --git a/src/sentry/install.js b/src/sentry/install.js new file mode 100644 index 00000000..acfb33e0 --- /dev/null +++ b/src/sentry/install.js @@ -0,0 +1,26 @@ +// @flow + +require('../env') + +const { SENTRY_URL } = process.env + +export default (Raven: any, shouldSendCallback: () => boolean, userId: string) => { + if (!SENTRY_URL) return + let r = Raven.config(SENTRY_URL, { + captureUnhandledRejections: true, + allowSecretKey: true, + release: __APP_VERSION__, + environment: __DEV__ ? 'development' : 'production', + shouldSendCallback, + }) + const user = { + ip_address: null, + id: userId, + } + if (r.setUserContext) { + r = r.setUserContext(user) + } else if (r.setContext) { + r = r.setContext({ user }) + } + r.install() +} diff --git a/src/sentry/node.js b/src/sentry/node.js new file mode 100644 index 00000000..a617487b --- /dev/null +++ b/src/sentry/node.js @@ -0,0 +1,8 @@ +// @flow + +import Raven from 'raven' +import install from './install' + +export default (shouldSendCallback: () => boolean, userId: string) => { + install(Raven, shouldSendCallback, userId) +} From 99aa530d11c7f8c35ac641304440676f52c522ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 16:41:06 +0200 Subject: [PATCH 05/16] should fix Sentry to work in prod --- src/sentry/install.js | 6 ++---- webpack/plugins.js | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sentry/install.js b/src/sentry/install.js index acfb33e0..d556339f 100644 --- a/src/sentry/install.js +++ b/src/sentry/install.js @@ -2,11 +2,9 @@ require('../env') -const { SENTRY_URL } = process.env - export default (Raven: any, shouldSendCallback: () => boolean, userId: string) => { - if (!SENTRY_URL) return - let r = Raven.config(SENTRY_URL, { + if (!__SENTRY_URL__) return + let r = Raven.config(__SENTRY_URL__, { captureUnhandledRejections: true, allowSecretKey: true, release: __APP_VERSION__, diff --git a/webpack/plugins.js b/webpack/plugins.js index af1b4b64..a19bfd87 100644 --- a/webpack/plugins.js +++ b/webpack/plugins.js @@ -3,7 +3,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') const pkg = require('../package.json') require('../src/globals') -const { BUNDLE_ANALYZER } = process.env +const { BUNDLE_ANALYZER, SENTRY_URL } = process.env module.exports = type => { const plugins = [ @@ -12,6 +12,7 @@ module.exports = type => { __GLOBAL_STYLES__: JSON.stringify(__GLOBAL_STYLES__), __DEV__, __PROD__, + __SENTRY_URL__: SENTRY_URL, 'process.env.NODE_ENV': JSON.stringify(__ENV__), }), ] From 9a421c4754b8fbbe9c2e06e56dbadc6cc1b5246c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 16:54:53 +0200 Subject: [PATCH 06/16] fix build --- webpack/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/plugins.js b/webpack/plugins.js index a19bfd87..e1bfb18d 100644 --- a/webpack/plugins.js +++ b/webpack/plugins.js @@ -12,7 +12,7 @@ module.exports = type => { __GLOBAL_STYLES__: JSON.stringify(__GLOBAL_STYLES__), __DEV__, __PROD__, - __SENTRY_URL__: SENTRY_URL, + __SENTRY_URL__: JSON.stringify(SENTRY_URL || null), 'process.env.NODE_ENV': JSON.stringify(__ENV__), }), ] From e412d9b5329cb3f39d423c1fa953a065174d9718 Mon Sep 17 00:00:00 2001 From: Anastasia Poupeney Date: Wed, 13 Jun 2018 15:13:21 +0200 Subject: [PATCH 07/16] wip sentry logs --- src/helpers/db.js | 2 +- src/helpers/user.js | 15 +++++++++++++++ src/init-sentry.js | 6 +++--- src/main/index.js | 2 +- src/middlewares/sentry.js | 19 +++++++++++++++++++ src/reducers/settings.js | 3 ++- src/renderer/createStore.js | 4 ++-- src/renderer/index.js | 4 ++-- src/renderer/init.js | 3 +++ src/renderer/sentry/browser.js | 19 +++++++++++++++++++ src/renderer/sentry/node.js | 0 11 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 src/helpers/user.js create mode 100644 src/middlewares/sentry.js create mode 100644 src/renderer/sentry/browser.js create mode 100644 src/renderer/sentry/node.js diff --git a/src/helpers/db.js b/src/helpers/db.js index 19968185..fb1dfa65 100644 --- a/src/helpers/db.js +++ b/src/helpers/db.js @@ -6,7 +6,7 @@ import get from 'lodash/get' import { decodeAccountsModel, encodeAccountsModel } from 'reducers/accounts' -type DBKey = 'settings' | 'accounts' | 'countervalues' +type DBKey = 'settings' | 'accounts' | 'countervalues' | 'user' const encryptionKey = {} diff --git a/src/helpers/user.js b/src/helpers/user.js new file mode 100644 index 00000000..15a8b7cb --- /dev/null +++ b/src/helpers/user.js @@ -0,0 +1,15 @@ +// @flow + +import db from 'helpers/db' +import uuid from 'uuid/v4' + +// a user is an anonymous way to identify a same instance of the app + +export default () => { + let user = db.get('user') + if (!user) { + user = { id: uuid() } + db.set('user', user) + } + return user +} diff --git a/src/init-sentry.js b/src/init-sentry.js index 1af0c815..663c6840 100644 --- a/src/init-sentry.js +++ b/src/init-sentry.js @@ -1,7 +1,7 @@ const { SENTRY_URL } = process.env if (__PROD__ && SENTRY_URL) { - const Raven = require('raven') - const ravenConfig = { captureUnhandledRejections: true } - Raven.config(SENTRY_URL, ravenConfig).install() + // const Raven = require('raven') + // const ravenConfig = { captureUnhandledRejections: true } + // Raven.config(SENTRY_URL, ravenConfig).install() } diff --git a/src/main/index.js b/src/main/index.js index c0d18749..3670ac42 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,7 +4,7 @@ process.setMaxListeners(0) require('../env') require('../globals') -require('../init-sentry') +// require('../init-sentry') require('./app') setImmediate(() => require('./bridge')) diff --git a/src/middlewares/sentry.js b/src/middlewares/sentry.js new file mode 100644 index 00000000..479be983 --- /dev/null +++ b/src/middlewares/sentry.js @@ -0,0 +1,19 @@ +const Raven = require('raven-js') +require('../env') + +import { sentryLogsBooleanSelector } from 'reducers/settings' + +const { SENTRY_URL } = process.env + +let isSentryInstalled = false + +export default store => next => action => { + next(action) + if (__PROD__ && SENTRY_URL) { + const state = store.getState() + const sentryLogs = sentryLogsBooleanSelector(state) + // if (sentryLogs !== isSentryInstalled) { + // + // } + } +} diff --git a/src/reducers/settings.js b/src/reducers/settings.js index fbe330f6..fc1aa1ca 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -73,7 +73,7 @@ const INITIAL_STATE: SettingsState = { developerMode: !!process.env.__DEV__, loaded: false, shareAnalytics: false, - sentryLogs: false, + sentryLogs: true, lastUsedVersion: __APP_VERSION__, } @@ -214,5 +214,6 @@ export const exchangeSettingsForAccountSelector: ESFAS = createSelector( ) export const marketIndicatorSelector = (state: State) => state.settings.marketIndicator +export const sentryLogsBooleanSelector = (state: State) => state.settings.sentryLogs export default handleActions(handlers, INITIAL_STATE) diff --git a/src/renderer/createStore.js b/src/renderer/createStore.js index 6e14fa5e..75f46711 100644 --- a/src/renderer/createStore.js +++ b/src/renderer/createStore.js @@ -6,7 +6,7 @@ import thunk from 'redux-thunk' import createHistory from 'history/createHashHistory' import type { HashHistory } from 'history' import logger from 'middlewares/logger' - +import sentry from 'middlewares/sentry' import reducers from 'reducers' type Props = { @@ -20,7 +20,7 @@ export default ({ state, history, dbMiddleware }: Props) => { if (!history) { history = createHistory() } - const middlewares = [routerMiddleware(history), thunk, logger] + const middlewares = [routerMiddleware(history), thunk, logger, sentry] if (dbMiddleware) { middlewares.push(dbMiddleware) } diff --git a/src/renderer/index.js b/src/renderer/index.js index d65ad8bf..5ae7edce 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -7,8 +7,8 @@ require('../env') const { SENTRY_URL } = process.env if (__PROD__ && SENTRY_URL) { - Raven.config(SENTRY_URL, { allowSecretKey: true }).install() - window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) + // Raven.config(SENTRY_URL, { allowSecretKey: true }).install() + // window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) } require('./init') diff --git a/src/renderer/init.js b/src/renderer/init.js index 6ef76a10..7bc6fe3d 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -15,6 +15,7 @@ import { fetchAccounts } from 'actions/accounts' import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' import { getLanguage } from 'reducers/settings' +import { sentryLogsBooleanSelector } from 'reducers/settings' import libcoreGetVersion from 'commands/libcoreGetVersion' import db from 'helpers/db' @@ -22,6 +23,7 @@ import dbMiddleware from 'middlewares/db' import CounterValues from 'helpers/countervalues' import hardReset from 'helpers/hardReset' +import sentry from 'renderer/sentry/browser' import App from 'components/App' import 'styles/global' @@ -50,6 +52,7 @@ async function init() { const state = store.getState() const language = getLanguage(state) const locked = isLocked(state) + sentry(() => sentryLogsBooleanSelector(store.getState())) moment.locale(language) diff --git a/src/renderer/sentry/browser.js b/src/renderer/sentry/browser.js new file mode 100644 index 00000000..9ee54b29 --- /dev/null +++ b/src/renderer/sentry/browser.js @@ -0,0 +1,19 @@ +const Raven = require('raven-js') +require('../../env') +import user from 'helpers/user' + +const { SENTRY_URL } = process.env + +export default shouldSendCallback => { + Raven.config(SENTRY_URL, { + allowSecretKey: true, + release: __APP_VERSION__, + environment: __DEV__ ? 'development' : 'production', + shouldSendCallback, + }) + .setUserContext({ + ip_address: null, + id: user().id, + }) + .install() +} diff --git a/src/renderer/sentry/node.js b/src/renderer/sentry/node.js new file mode 100644 index 00000000..e69de29b From beba014abdfd905b5caa08d312b3686559c0150c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 15:53:06 +0200 Subject: [PATCH 08/16] connect Sentry to main & internal processes --- src/commands/testCrash.js | 8 ++++---- src/init-sentry.js | 7 ------- src/internals/index.js | 9 ++++++++- src/main/bridge.js | 21 ++++++++++++++++++++- src/main/index.js | 1 - src/middlewares/sentry.js | 17 ++++++----------- src/renderer/index.js | 11 ----------- src/renderer/init.js | 5 ++--- src/renderer/sentry/browser.js | 19 ------------------- src/renderer/sentry/node.js | 0 src/sentry/browser.js | 9 +++++++++ src/sentry/install.js | 26 ++++++++++++++++++++++++++ src/sentry/node.js | 8 ++++++++ 13 files changed, 83 insertions(+), 58 deletions(-) delete mode 100644 src/init-sentry.js delete mode 100644 src/renderer/sentry/browser.js delete mode 100644 src/renderer/sentry/node.js create mode 100644 src/sentry/browser.js create mode 100644 src/sentry/install.js create mode 100644 src/sentry/node.js diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js index 0725988a..5675a1ee 100644 --- a/src/commands/testCrash.js +++ b/src/commands/testCrash.js @@ -8,10 +8,10 @@ import { createCommand, Command } from 'helpers/ipc' type Input = void type Result = void -const cmd: Command = createCommand('testCrash', () => - Observable.create(() => { +const cmd: Command = createCommand('testCrash', () => { + return Observable.create(() => { process.exit(1) - }), -) + }) +}) export default cmd diff --git a/src/init-sentry.js b/src/init-sentry.js deleted file mode 100644 index 663c6840..00000000 --- a/src/init-sentry.js +++ /dev/null @@ -1,7 +0,0 @@ -const { SENTRY_URL } = process.env - -if (__PROD__ && SENTRY_URL) { - // const Raven = require('raven') - // const ravenConfig = { captureUnhandledRejections: true } - // Raven.config(SENTRY_URL, ravenConfig).install() -} diff --git a/src/internals/index.js b/src/internals/index.js index 94601aaa..f2879c08 100644 --- a/src/internals/index.js +++ b/src/internals/index.js @@ -3,14 +3,18 @@ import commands from 'commands' import logger from 'logger' import uuid from 'uuid/v4' import { setImplementation } from 'api/network' +import sentry from 'sentry/node' require('../env') -require('../init-sentry') process.title = 'Internal' const defers = {} +let sentryEnabled = process.env.INITIAL_SENTRY_ENABLED || false + +sentry(() => sentryEnabled, process.env.SENTRY_USER_ID) + if (process.env.DEBUG_NETWORK) { setImplementation(networkArg => { const id = uuid() @@ -92,6 +96,9 @@ process.on('message', m => { } else { defer.reject(payload.error) } + } else if (m.type === 'sentryLogsChanged') { + const { payload } = m + sentryEnabled = payload.value } }) diff --git a/src/main/bridge.js b/src/main/bridge.js index ff9af1ab..bd118629 100644 --- a/src/main/bridge.js +++ b/src/main/bridge.js @@ -7,6 +7,8 @@ import { ipcMain, app } from 'electron' import { ipcMainListenReceiveCommands } from 'helpers/ipc' import path from 'path' import logger from 'logger' +import sentry from 'sentry/node' +import user from 'helpers/user' import setupAutoUpdater, { quitAndInstall } from './autoUpdate' @@ -17,6 +19,11 @@ const LEDGER_LIVE_SQLITE_PATH = path.resolve(app.getPath('userData'), 'sqlite') let internalProcess +let sentryEnabled = false +const userId = user().id + +sentry(() => sentryEnabled, userId) + const killInternalProcess = () => { if (internalProcess) { logger.log('killing internal process...') @@ -30,7 +37,12 @@ const forkBundlePath = path.resolve(__dirname, `${__DEV__ ? '../../' : './'}dist const bootInternalProcess = () => { logger.log('booting internal process...') internalProcess = fork(forkBundlePath, { - env: { ...process.env, LEDGER_LIVE_SQLITE_PATH }, + env: { + ...process.env, + LEDGER_LIVE_SQLITE_PATH, + INITIAL_SENTRY_ENABLED: sentryEnabled, + SENTRY_USER_ID: userId, + }, }) internalProcess.on('message', handleGlobalInternalMessage) internalProcess.on('exit', code => { @@ -102,6 +114,13 @@ ipcMain.on('executeHttpQueryPayload', (event, payload) => { p.send({ type: 'executeHttpQueryPayload', payload }) }) +ipcMain.on('sentryLogsChanged', (event, payload) => { + sentryEnabled = payload.value + const p = internalProcess + if (!p) return + p.send({ type: 'sentryLogsChanged', payload }) +}) + // TODO move this to "command" pattern ipcMain.on('updater', (event, { type, data }) => { const handler = { diff --git a/src/main/index.js b/src/main/index.js index 3670ac42..65952b72 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -4,7 +4,6 @@ process.setMaxListeners(0) require('../env') require('../globals') -// require('../init-sentry') require('./app') setImmediate(() => require('./bridge')) diff --git a/src/middlewares/sentry.js b/src/middlewares/sentry.js index 479be983..212764c6 100644 --- a/src/middlewares/sentry.js +++ b/src/middlewares/sentry.js @@ -1,19 +1,14 @@ -const Raven = require('raven-js') -require('../env') - +import { ipcRenderer } from 'electron' import { sentryLogsBooleanSelector } from 'reducers/settings' -const { SENTRY_URL } = process.env - let isSentryInstalled = false export default store => next => action => { next(action) - if (__PROD__ && SENTRY_URL) { - const state = store.getState() - const sentryLogs = sentryLogsBooleanSelector(state) - // if (sentryLogs !== isSentryInstalled) { - // - // } + const state = store.getState() + const sentryLogs = sentryLogsBooleanSelector(state) + if (sentryLogs !== isSentryInstalled) { + isSentryInstalled = sentryLogs + ipcRenderer.send('sentryLogsChanged', { value: sentryLogs }) } } diff --git a/src/renderer/index.js b/src/renderer/index.js index 5ae7edce..5164fc17 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -1,14 +1,3 @@ require('@babel/polyfill') - -const Raven = require('raven-js') - require('../env') - -const { SENTRY_URL } = process.env - -if (__PROD__ && SENTRY_URL) { - // Raven.config(SENTRY_URL, { allowSecretKey: true }).install() - // window.addEventListener('unhandledrejection', event => Raven.captureException(event.reason)) -} - require('./init') diff --git a/src/renderer/init.js b/src/renderer/init.js index 7bc6fe3d..c9e77136 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -14,8 +14,7 @@ import events from 'renderer/events' import { fetchAccounts } from 'actions/accounts' import { fetchSettings } from 'actions/settings' import { isLocked } from 'reducers/application' -import { getLanguage } from 'reducers/settings' -import { sentryLogsBooleanSelector } from 'reducers/settings' +import { getLanguage, sentryLogsBooleanSelector } from 'reducers/settings' import libcoreGetVersion from 'commands/libcoreGetVersion' import db from 'helpers/db' @@ -23,7 +22,7 @@ import dbMiddleware from 'middlewares/db' import CounterValues from 'helpers/countervalues' import hardReset from 'helpers/hardReset' -import sentry from 'renderer/sentry/browser' +import sentry from 'sentry/browser' import App from 'components/App' import 'styles/global' diff --git a/src/renderer/sentry/browser.js b/src/renderer/sentry/browser.js deleted file mode 100644 index 9ee54b29..00000000 --- a/src/renderer/sentry/browser.js +++ /dev/null @@ -1,19 +0,0 @@ -const Raven = require('raven-js') -require('../../env') -import user from 'helpers/user' - -const { SENTRY_URL } = process.env - -export default shouldSendCallback => { - Raven.config(SENTRY_URL, { - allowSecretKey: true, - release: __APP_VERSION__, - environment: __DEV__ ? 'development' : 'production', - shouldSendCallback, - }) - .setUserContext({ - ip_address: null, - id: user().id, - }) - .install() -} diff --git a/src/renderer/sentry/node.js b/src/renderer/sentry/node.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/sentry/browser.js b/src/sentry/browser.js new file mode 100644 index 00000000..ac5fc55c --- /dev/null +++ b/src/sentry/browser.js @@ -0,0 +1,9 @@ +// @flow + +import Raven from 'raven-js' +import user from 'helpers/user' +import install from './install' + +export default (shouldSendCallback: () => boolean) => { + install(Raven, shouldSendCallback, user().id) +} diff --git a/src/sentry/install.js b/src/sentry/install.js new file mode 100644 index 00000000..acfb33e0 --- /dev/null +++ b/src/sentry/install.js @@ -0,0 +1,26 @@ +// @flow + +require('../env') + +const { SENTRY_URL } = process.env + +export default (Raven: any, shouldSendCallback: () => boolean, userId: string) => { + if (!SENTRY_URL) return + let r = Raven.config(SENTRY_URL, { + captureUnhandledRejections: true, + allowSecretKey: true, + release: __APP_VERSION__, + environment: __DEV__ ? 'development' : 'production', + shouldSendCallback, + }) + const user = { + ip_address: null, + id: userId, + } + if (r.setUserContext) { + r = r.setUserContext(user) + } else if (r.setContext) { + r = r.setContext({ user }) + } + r.install() +} diff --git a/src/sentry/node.js b/src/sentry/node.js new file mode 100644 index 00000000..a617487b --- /dev/null +++ b/src/sentry/node.js @@ -0,0 +1,8 @@ +// @flow + +import Raven from 'raven' +import install from './install' + +export default (shouldSendCallback: () => boolean, userId: string) => { + install(Raven, shouldSendCallback, userId) +} From fdfff4e10fb7ffda8a74915003bc1c0eb811cc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 16:41:06 +0200 Subject: [PATCH 09/16] should fix Sentry to work in prod --- src/sentry/install.js | 6 ++---- webpack/plugins.js | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sentry/install.js b/src/sentry/install.js index acfb33e0..d556339f 100644 --- a/src/sentry/install.js +++ b/src/sentry/install.js @@ -2,11 +2,9 @@ require('../env') -const { SENTRY_URL } = process.env - export default (Raven: any, shouldSendCallback: () => boolean, userId: string) => { - if (!SENTRY_URL) return - let r = Raven.config(SENTRY_URL, { + if (!__SENTRY_URL__) return + let r = Raven.config(__SENTRY_URL__, { captureUnhandledRejections: true, allowSecretKey: true, release: __APP_VERSION__, diff --git a/webpack/plugins.js b/webpack/plugins.js index af1b4b64..a19bfd87 100644 --- a/webpack/plugins.js +++ b/webpack/plugins.js @@ -3,7 +3,7 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') const pkg = require('../package.json') require('../src/globals') -const { BUNDLE_ANALYZER } = process.env +const { BUNDLE_ANALYZER, SENTRY_URL } = process.env module.exports = type => { const plugins = [ @@ -12,6 +12,7 @@ module.exports = type => { __GLOBAL_STYLES__: JSON.stringify(__GLOBAL_STYLES__), __DEV__, __PROD__, + __SENTRY_URL__: SENTRY_URL, 'process.env.NODE_ENV': JSON.stringify(__ENV__), }), ] From c2a5ae5ea5f05411df580e502d063128d2ae2cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 16:54:53 +0200 Subject: [PATCH 10/16] fix build --- webpack/plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/plugins.js b/webpack/plugins.js index a19bfd87..e1bfb18d 100644 --- a/webpack/plugins.js +++ b/webpack/plugins.js @@ -12,7 +12,7 @@ module.exports = type => { __GLOBAL_STYLES__: JSON.stringify(__GLOBAL_STYLES__), __DEV__, __PROD__, - __SENTRY_URL__: SENTRY_URL, + __SENTRY_URL__: JSON.stringify(SENTRY_URL || null), 'process.env.NODE_ENV': JSON.stringify(__ENV__), }), ] From 468946f4ec59bd548adfcca45b12ffbe05cd6f4b Mon Sep 17 00:00:00 2001 From: Anastasia Poupeney Date: Wed, 13 Jun 2018 17:00:12 +0200 Subject: [PATCH 11/16] remove not needed changes --- src/commands/testCrash.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js index 5675a1ee..0725988a 100644 --- a/src/commands/testCrash.js +++ b/src/commands/testCrash.js @@ -8,10 +8,10 @@ import { createCommand, Command } from 'helpers/ipc' type Input = void type Result = void -const cmd: Command = createCommand('testCrash', () => { - return Observable.create(() => { +const cmd: Command = createCommand('testCrash', () => + Observable.create(() => { process.exit(1) - }) -}) + }), +) export default cmd From 85ae87f6717735219a28c21311c27d40015bd0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 17:41:26 +0200 Subject: [PATCH 12/16] fix empty exchange section to appear in settings! --- src/components/SettingsPage/sections/Display.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/SettingsPage/sections/Display.js b/src/components/SettingsPage/sections/Display.js index a57892f9..55d13b3f 100644 --- a/src/components/SettingsPage/sections/Display.js +++ b/src/components/SettingsPage/sections/Display.js @@ -173,10 +173,6 @@ class TabProfile extends PureComponent { /> - Date: Wed, 13 Jun 2018 17:43:36 +0200 Subject: [PATCH 13/16] fix ci error --- src/commands/testCrash.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js index 5675a1ee..72695e53 100644 --- a/src/commands/testCrash.js +++ b/src/commands/testCrash.js @@ -8,10 +8,8 @@ import { createCommand, Command } from 'helpers/ipc' type Input = void type Result = void -const cmd: Command = createCommand('testCrash', () => { - return Observable.create(() => { +const cmd: Command = createCommand('testCrash', () => Observable.create(() => { process.exit(1) - }) -}) + })) export default cmd From 2163bf6b1fe23f16311f0b98ee022cce518de3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 17:36:13 +0200 Subject: [PATCH 14/16] add TopGradient for MainSideBar --- src/components/MainSideBar/TopGradient.js | 19 +++++++++++++++++++ src/components/MainSideBar/index.js | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 src/components/MainSideBar/TopGradient.js diff --git a/src/components/MainSideBar/TopGradient.js b/src/components/MainSideBar/TopGradient.js new file mode 100644 index 00000000..3cf0e899 --- /dev/null +++ b/src/components/MainSideBar/TopGradient.js @@ -0,0 +1,19 @@ +// @flow + +import React from 'react' +import styled from 'styled-components' + +const TopGradientBox = styled.div` + width: 100%; + height: 80px; + position: absolute; + top: 0; + left: 0; + background: linear-gradient(#ffffff 40%, rgba(255, 255, 255, 0)); + z-index: 2; + pointer-events: none; +` + +const el = + +export default () => el diff --git a/src/components/MainSideBar/index.js b/src/components/MainSideBar/index.js index b8a0688d..a55a1466 100644 --- a/src/components/MainSideBar/index.js +++ b/src/components/MainSideBar/index.js @@ -32,6 +32,7 @@ import IconExchange from 'icons/Exchange' import AccountListItem from './AccountListItem' import AddAccountButton from './AddAccountButton' +import TopGradient from './TopGradient' const mapStateToProps = state => ({ accounts: accountsSelector(state), @@ -84,6 +85,7 @@ class MainSideBar extends PureComponent { return ( + From ef303c042f5873ee74af115904ccd712c6c5e7e5 Mon Sep 17 00:00:00 2001 From: Anastasia Poupeney Date: Wed, 13 Jun 2018 18:34:20 +0200 Subject: [PATCH 15/16] merge issues fix --- src/commands/testCrash.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/testCrash.js b/src/commands/testCrash.js index 72695e53..0725988a 100644 --- a/src/commands/testCrash.js +++ b/src/commands/testCrash.js @@ -8,8 +8,10 @@ import { createCommand, Command } from 'helpers/ipc' type Input = void type Result = void -const cmd: Command = createCommand('testCrash', () => Observable.create(() => { +const cmd: Command = createCommand('testCrash', () => + Observable.create(() => { process.exit(1) - })) + }), +) export default cmd From a132bb250c06b2193945e5545ded92f6b877a6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Wed, 13 Jun 2018 18:39:53 +0200 Subject: [PATCH 16/16] fix jsx code --- src/components/SettingsPage/sections/Display.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SettingsPage/sections/Display.js b/src/components/SettingsPage/sections/Display.js index 81fcb5e2..1945e034 100644 --- a/src/components/SettingsPage/sections/Display.js +++ b/src/components/SettingsPage/sections/Display.js @@ -173,7 +173,6 @@ class TabProfile extends PureComponent { /> -