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/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 2ec2ba3a..28c8c5c8 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 ( + diff --git a/src/components/ManagerPage/AppsList.js b/src/components/ManagerPage/AppsList.js index 0a001674..244adb41 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,13 @@ 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 Progress from 'components/base/Progress' -import type { Device, T } from 'types/common' +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' @@ -32,6 +38,7 @@ const ICONS_FALLBACK = { } type Status = 'loading' | 'idle' | 'busy' | 'success' | 'error' +type Mode = '' | 'installing' | 'uninstalling' type LedgerApp = { name: string, @@ -54,6 +61,8 @@ type State = { status: Status, error: string | null, appsList: LedgerApp[], + app: string, + mode: Mode, } class AppsList extends PureComponent { @@ -61,6 +70,8 @@ class AppsList extends PureComponent { status: 'loading', error: null, appsList: [], + app: '', + mode: '', } componentDidMount() { @@ -86,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 ( @@ -138,27 +196,7 @@ class AppsList extends PureComponent { )} - ( - - {status === 'busy' ? ( - {'Loading...'} - ) : status === 'error' ? ( - -
{'error happened'}
- {error} - -
- ) : status === 'success' ? ( - - {'success'} - - - ) : null} -
- )} - /> + {this.renderModal()}
) } @@ -169,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 new file mode 100644 index 00000000..238a063e --- /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('app:manager.title')} + + + {t('app:manager.subtitle')} + + + + + + + + + +) + +export default translate()(Dashboard) 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/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..3dea16b3 --- /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('app:manager.device.title')} + + + {t('app: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/src/components/SettingsPage/sections/Display.js b/src/components/SettingsPage/sections/Display.js index 4e5df09f..1945e034 100644 --- a/src/components/SettingsPage/sections/Display.js +++ b/src/components/SettingsPage/sections/Display.js @@ -173,12 +173,6 @@ class TabProfile extends PureComponent { />
- + 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/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/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/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 deleted file mode 100644 index 1af0c815..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 c0d18749..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 new file mode 100644 index 00000000..212764c6 --- /dev/null +++ b/src/middlewares/sentry.js @@ -0,0 +1,14 @@ +import { ipcRenderer } from 'electron' +import { sentryLogsBooleanSelector } from 'reducers/settings' + +let isSentryInstalled = false + +export default store => next => action => { + next(action) + const state = store.getState() + const sentryLogs = sentryLogsBooleanSelector(state) + if (sentryLogs !== isSentryInstalled) { + isSentryInstalled = sentryLogs + ipcRenderer.send('sentryLogsChanged', { value: sentryLogs }) + } +} 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..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 2dc4bd99..1806aff1 100644 --- a/src/renderer/init.js +++ b/src/renderer/init.js @@ -14,7 +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 { getLanguage, sentryLogsBooleanSelector } from 'reducers/settings' import libcoreGetVersion from 'commands/libcoreGetVersion' import db from 'helpers/db' @@ -22,6 +22,7 @@ import dbMiddleware from 'middlewares/db' import CounterValues from 'helpers/countervalues' import hardReset from 'helpers/hardReset' +import sentry from 'sentry/browser' import App from 'components/App' import 'styles/global' @@ -59,6 +60,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/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..d556339f --- /dev/null +++ b/src/sentry/install.js @@ -0,0 +1,24 @@ +// @flow + +require('../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) +} diff --git a/static/i18n/en/app.yml b/static/i18n/en/app.yml index ad8847b2..02806506 100644 --- a/static/i18n/en/app.yml +++ b/static/i18n/en/app.yml @@ -151,16 +151,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/webpack/plugins.js b/webpack/plugins.js index af1b4b64..e1bfb18d 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__: JSON.stringify(SENTRY_URL || null), 'process.env.NODE_ENV': JSON.stringify(__ENV__), }), ]