Browse Source

Merge pull request #1791 from juan-cortes/LL-931

LL-931 Add a banner to promote the mobile version of the Live + Banner refactoring
develop
Gaëtan Renaudeau 6 years ago
committed by GitHub
parent
commit
c3813233ad
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      package.json
  2. 51
      scripts/create-release-shasums.sh
  3. 1
      scripts/release.sh
  4. 59
      src/commands/autoUpdate.js
  5. 4
      src/commands/index.js
  6. 30
      src/commands/quitAndInstallElectronUpdate.js
  7. 17
      src/components/App.js
  8. 39
      src/components/DashboardPage/index.js
  9. 9
      src/components/MainSideBar/index.js
  10. 130
      src/components/TopBanner.js
  11. 4
      src/components/TopBar/index.js
  12. 103
      src/components/UpdateNotifier/UpdateDownloaded.js
  13. 48
      src/components/UpdateNotifier/UpdateInstalled.js
  14. 17
      src/components/UpdateNotifier/index.js
  15. 74
      src/components/Updater/Banner.js
  16. 70
      src/components/Updater/DebugUpdater.js
  17. 37
      src/components/Updater/UpdateDot.js
  18. 95
      src/components/Updater/UpdaterContext.js
  19. 13
      src/components/base/SideBar/SideBarListItem.js
  20. 1
      src/components/base/SideBar/stories.js
  21. 6
      src/components/layout/Default.js
  22. 57
      src/components/modals/ReleaseNotes/index.js
  23. 5
      src/config/constants.js
  24. 6
      src/config/urls.js
  25. 39
      src/icons/Donjon.js
  26. 13
      src/icons/TriangleWarning.js
  27. 30
      src/icons/device/NanoX.js
  28. 1
      src/icons/device/index.js
  29. 63
      src/internals/index.js
  30. 32
      src/main/autoUpdate.js
  31. 24
      src/main/bridge.js
  32. 49
      src/main/commandHandler.js
  33. 86
      src/main/updater/createAppUpdater.js
  34. 62
      src/main/updater/createElectronAppUpdater.js
  35. 40
      src/main/updater/createMockAppUpdater.js
  36. 5
      src/main/updater/errors.js
  37. 111
      src/main/updater/ledger-pubkey.js
  38. 33
      src/main/updater/mocks/mock-privkey-1.asc
  39. 33
      src/main/updater/mocks/mock-privkey-2.asc
  40. 19
      src/main/updater/mocks/mock-pubkey-1.asc
  41. 19
      src/main/updater/mocks/mock-pubkey-2.asc
  42. 52
      src/main/updater/mocks/pubkey-1.asc
  43. 52
      src/main/updater/mocks/pubkey-2.asc
  44. 1
      src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage
  45. 1
      src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum
  46. 16
      src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig
  47. 3
      src/main/updater/mocks/sigko-shasumok/update-info.json
  48. 1
      src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage
  49. 1
      src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum
  50. 16
      src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig
  51. 3
      src/main/updater/mocks/sigok-shasumko/update-info.json
  52. 1
      src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage
  53. 1
      src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum
  54. 16
      src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig
  55. 3
      src/main/updater/mocks/sigok-shasumok/update-info.json
  56. 40
      src/main/updater/pgpHelper.js
  57. 114
      src/main/updater/updater.spec.js
  58. 4
      src/reducers/index.js
  59. 48
      src/reducers/update.js
  60. 25
      src/renderer/events.js
  61. 12
      static/i18n/en/app.json
  62. 143
      yarn.lock

5
package.json

@ -71,13 +71,14 @@
"lru-cache": "^4.1.3",
"measure-scrollbar": "^1.1.0",
"moment": "^2.22.2",
"openpgp": "^4.2.1",
"qrcode": "^1.2.0",
"qrloop": "0.8.1",
"qs": "^6.5.1",
"raven": "^2.5.0",
"raven-js": "^3.24.2",
"react": "^16.6.1",
"react-dom": "^16.4.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-i18next": "^7.7.0",
"react-key-handler": "^1.0.1",
"react-markdown": "^3.3.2",

51
scripts/create-release-shasums.sh

@ -0,0 +1,51 @@
#!/bin/env bash
# Fetch release binaries for all platforms
# and produce a .sha512sum file in the current folder
# exit on error
set -e
[[ "$GH_TOKEN" == "" ]] && echo "GH_TOKEN is unset" && exit 1
function main {
ASSETS_FILTER="(AppImage|zip|exe)"
PKG_VER=$(grep version package.json | sed -E 's/.*: "(.*)",/\1/g')
OUTPUT_FILE="ledger-live-desktop-$PKG_VER.sha512sum"
read -p "> release version ($PKG_VER): " -r RELEASE_VERSION
RELEASE_VERSION=${RELEASE_VERSION:-$PKG_VER}
RELEASES=$(do_request "/repos/LedgerHQ/ledger-live-desktop/releases")
printf """
console.log(
(%s).find(r => r.tag_name === 'v%s').assets
.filter(a => a.name.match(/\\.%s$/))
.map(a => a.browser_download_url)
.join('\\\n')
)
""" "$RELEASES" "$RELEASE_VERSION" "$ASSETS_FILTER" >"$TMP_FILE1"
node "$TMP_FILE1" | tee "$TMP_FILE2"
pushd "$TMP_DIR" >/dev/null
while IFS= read -r line ; do
curl -L -O "$line"
done < "$TMP_FILE2"
sha512sum -- * > "$OLDPWD/$OUTPUT_FILE"
popd >/dev/null
}
TMP_DIR=$(mktemp -d)
TMP_FILE1=$(mktemp)
TMP_FILE2=$(mktemp)
function cleanup {
rm -rf "$TMP_FILE1" "$TMP_FILE2" "$TMP_DIR"
}
function do_request {
curl -H "Authorization: token $GH_TOKEN" "https://api.github.com$1"
}
trap cleanup EXIT
main

1
scripts/release.sh

@ -67,6 +67,7 @@ if [[ $(uname) == 'Linux' ]]; then # only run it on one target, to prevent race
"failed to create a draft release"
fi
runJob "rm -rf ./node_modules/.cache" "Removing node modules cache..." "done" "fail"
runJob "yarn compile" "compiling..." "compiled" "failed to compile" "verbose"
if [[ $(uname) == 'Linux' ]]; then

59
src/commands/autoUpdate.js

@ -0,0 +1,59 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
// import { UPDATE_CHECK_IGNORE, UPDATE_CHECK_FEED } from 'config/constants'
import { UPDATE_CHECK_IGNORE } from 'config/constants'
// import createElectronAppUpdater from 'main/updater/createElectronAppUpdater'
import type { UpdateStatus } from 'components/Updater/UpdaterContext'
type Input = {}
type Result = {
status: UpdateStatus,
payload?: *,
}
const cmd: Command<Input, Result> = createCommand('main:autoUpdate', () =>
Observable.create(o => {
const { autoUpdater } = require('electron-updater')
const sendStatus = (status, payload) => {
o.next({ status, payload })
}
const handleDownload = async _ => {
try {
sendStatus('checking')
// const appUpdater = await createElectronAppUpdater({
// feedURL: UPDATE_CHECK_FEED,
// updateVersion: info.version,
// })
// await appUpdater.verify()
sendStatus('check-success')
} catch (err) {
// don't throw if the check fail for now. it's a white bullet.
if (UPDATE_CHECK_IGNORE) {
// TODO: track the error
sendStatus('check-success')
} else {
o.error(err)
}
}
}
autoUpdater.on('checking-for-update', () => sendStatus('checking-for-update'))
autoUpdater.on('update-available', info => sendStatus('update-available', info))
autoUpdater.on('update-not-available', info => sendStatus('update-not-available', info))
autoUpdater.on('download-progress', p => sendStatus('download-progress', p))
autoUpdater.on('update-downloaded', handleDownload)
autoUpdater.on('error', err => o.error(err))
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.checkForUpdates()
return () => {}
}),
)
export default cmd

4
src/commands/index.js

@ -4,6 +4,7 @@ import invariant from 'invariant'
import type { Command } from 'helpers/ipc'
import debugAppInfosForCurrency from 'commands/debugAppInfosForCurrency'
import autoUpdate from 'commands/autoUpdate'
import firmwarePrepare from 'commands/firmwarePrepare'
import firmwareMain from 'commands/firmwareMain'
import firmwareRepair from 'commands/firmwareRepair'
@ -22,6 +23,7 @@ import libcoreSyncAccount from 'commands/libcoreSyncAccount'
import libcoreValidAddress from 'commands/libcoreValidAddress'
import listenDevices from 'commands/listenDevices'
import ping from 'commands/ping'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
import signTransaction from 'commands/signTransaction'
import testApdu from 'commands/testApdu'
import testCrash from 'commands/testCrash'
@ -29,6 +31,7 @@ import testInterval from 'commands/testInterval'
import uninstallApp from 'commands/uninstallApp'
const all: Array<Command<any, any>> = [
autoUpdate,
debugAppInfosForCurrency,
firmwarePrepare,
firmwareMain,
@ -48,6 +51,7 @@ const all: Array<Command<any, any>> = [
libcoreValidAddress,
listenDevices,
ping,
quitAndInstallElectronUpdate,
signTransaction,
testApdu,
testCrash,

30
src/commands/quitAndInstallElectronUpdate.js

@ -0,0 +1,30 @@
// @flow
import { createCommand, Command } from 'helpers/ipc'
import { Observable } from 'rxjs'
type Input = void
type Result = void
const cmd: Command<Input, Result> = createCommand('main:quitAndInstallElectronUpdate', () =>
Observable.create(o => {
const { app, BrowserWindow } = require('electron')
const { autoUpdater } = require('electron-updater')
const browserWindows = BrowserWindow.getAllWindows()
// Fixes quitAndInstall not quitting on macOS, as suggested on
// https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-306709572
app.removeAllListeners('window-all-closed')
browserWindows.forEach(browserWindow => {
browserWindow.removeAllListeners('close')
})
// couldn't find a way to catch if fail ¯\_(ツ)_/¯
autoUpdater.quitAndInstall(false)
o.complete()
return () => {}
}),
)
export default cmd

17
src/components/App.js

@ -16,6 +16,7 @@ import ThrowBlock from 'components/ThrowBlock'
import Default from 'components/layout/Default'
import CounterValues from 'helpers/countervalues'
import { BridgeSyncProvider } from 'bridge/BridgeSyncContext'
import { UpdaterProvider } from 'components/Updater/UpdaterContext'
const App = ({
store,
@ -31,13 +32,15 @@ const App = ({
<CounterValues.PollingProvider>
<I18nextProvider i18n={i18n} initialLanguage={language}>
<ThemeProvider theme={theme}>
<ThrowBlock>
<ConnectedRouter history={history}>
<Switch>
<Route component={Default} />
</Switch>
</ConnectedRouter>
</ThrowBlock>
<UpdaterProvider>
<ThrowBlock>
<ConnectedRouter history={history}>
<Switch>
<Route component={Default} />
</Switch>
</ConnectedRouter>
</ThrowBlock>
</UpdaterProvider>
</ThemeProvider>
</I18nextProvider>
</CounterValues.PollingProvider>

39
src/components/DashboardPage/index.js

@ -3,6 +3,7 @@
import React, { PureComponent, Fragment } from 'react'
import uniq from 'lodash/uniq'
import { compose } from 'redux'
import IconNanoX from 'icons/device/NanoX'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
@ -24,17 +25,21 @@ import { saveSettings } from 'actions/settings'
import TrackPage from 'analytics/TrackPage'
import RefreshAccountsOrdering from 'components/RefreshAccountsOrdering'
import UpdateNotifier from 'components/UpdateNotifier'
import UpdateBanner from 'components/Updater/Banner'
import BalanceInfos from 'components/BalanceSummary/BalanceInfos'
import BalanceSummary from 'components/BalanceSummary'
import Box from 'components/base/Box'
import PillsDaysCount from 'components/PillsDaysCount'
import OperationsList from 'components/OperationsList'
import StickyBackToTop from 'components/StickyBackToTop'
import styled from 'styled-components'
import { openURL } from 'helpers/linking'
import EmptyState from './EmptyState'
import CurrentGreetings from './CurrentGreetings'
import SummaryDesc from './SummaryDesc'
import AccountCardList from './AccountCardList'
import TopBanner, { FakeLink } from '../TopBanner'
import { urls } from '../../config/urls'
const mapStateToProps = createStructuredSelector({
accounts: accountsSelector,
@ -84,7 +89,24 @@ class DashboardPage extends PureComponent<Props> {
return (
<Fragment>
<UpdateNotifier />
<TopBannerContainer>
<UpdateBanner />
<TopBanner
content={{
message: t('banners.promoteMobile'),
Icon: IconNanoX,
right: (
<FakeLink onClick={() => openURL(urls.promoNanoX)}>
{t('common.learnMore')}
</FakeLink>
),
}}
status={'dark'}
bannerId={'promoNanoX'}
dismissable
/>
<SeparatorBar />
</TopBannerContainer>
<RefreshAccountsOrdering onMount />
<TrackPage
category="Portfolio"
@ -143,6 +165,19 @@ class DashboardPage extends PureComponent<Props> {
)
}
}
// This forces only one visible top banner at a time
const TopBannerContainer = styled.div`
& > *:not(:first-child) {
display: none;
}
`
// If no banners are present, the SeparatorBar appears
const SeparatorBar = styled.div`
height: 1px;
border-bottom: 1px solid ${p => p.theme.colors.fog};
margin-bottom: 15px;
margin-top: -20px;
`
export default compose(
connect(

9
src/components/MainSideBar/index.js

@ -11,20 +11,19 @@ import type { Location } from 'react-router'
import type { Account } from '@ledgerhq/live-common/lib/types'
import type { T } from 'types/common'
import type { UpdateStatus } from 'reducers/update'
import { MODAL_RECEIVE, MODAL_SEND, MODAL_ADD_ACCOUNTS } from 'config/constants'
import { i } from 'helpers/staticPath'
import { accountsSelector } from 'reducers/accounts'
import { openModal } from 'reducers/modals'
import { getUpdateStatus } from 'reducers/update'
import { developerModeSelector } from 'reducers/settings'
import { SideBarList, SideBarListItem } from 'components/base/SideBar'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Space from 'components/base/Space'
import UpdateDot from 'components/Updater/UpdateDot'
import IconManager from 'icons/Manager'
import IconPieChart from 'icons/PieChart'
@ -39,7 +38,6 @@ import KeyboardContent from '../KeyboardContent'
const mapStateToProps = state => ({
accounts: accountsSelector(state),
updateStatus: getUpdateStatus(state),
developerMode: developerModeSelector(state),
})
@ -54,7 +52,6 @@ type Props = {
location: Location,
push: string => void,
openModal: string => void,
updateStatus: UpdateStatus,
developerMode: boolean,
}
@ -96,7 +93,7 @@ class MainSideBar extends PureComponent<Props> {
handleOpenImportModal = () => this.props.openModal(MODAL_ADD_ACCOUNTS)
render() {
const { t, accounts, location, updateStatus, developerMode } = this.props
const { t, accounts, location, developerMode } = this.props
const { pathname } = location
const addAccountButton = (
@ -122,7 +119,7 @@ class MainSideBar extends PureComponent<Props> {
iconActiveColor="wallet"
onClick={this.handleClickDashboard}
isActive={pathname === '/'}
hasNotif={updateStatus === 'downloaded'}
NotifComponent={UpdateDot}
/>
<SideBarListItem
label={t('send.title')}

130
src/components/TopBanner.js

@ -0,0 +1,130 @@
// @flow
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import { connect } from 'react-redux'
import Box from 'components/base/Box'
import { radii } from 'styles/theme'
import IconCross from 'icons/Cross'
import { createStructuredSelector } from 'reselect'
import { dismissBanner } from '../actions/settings'
import { dismissedBannersSelector } from '../reducers/settings'
export type Content = {
Icon?: React$ComponentType<*>,
message: React$Node,
right?: React$Node,
}
type Props = {
content?: Content,
status: string,
dismissable: boolean,
bannerId?: string,
dismissedBanners: string[],
dismissBanner: string => void,
}
const mapStateToProps = createStructuredSelector({
dismissedBanners: dismissedBannersSelector,
})
const mapDispatchToProps = {
dismissBanner,
}
class TopBanner extends PureComponent<Props> {
static defaultProps = {
status: '',
dismissable: false,
}
onDismiss = () => {
const { bannerId, dismissBanner } = this.props
if (bannerId) {
dismissBanner(bannerId)
}
}
render() {
const { dismissedBanners, bannerId, dismissable, content, status } = this.props
if (!content || (bannerId && dismissedBanners.includes(bannerId))) return null
const { Icon, message, right } = content
return (
<Container status={status}>
{Icon && (
<IconContainer>
<Icon size={16} />
</IconContainer>
)}
{message}
<RightContainer>{right}</RightContainer>
{dismissable && (
<CloseContainer onClick={this.onDismiss}>
<IconCross size={14} />
</CloseContainer>
)}
</Container>
)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TopBanner)
const IconContainer = styled.div`
margin-right: 15px;
display: flex;
align-items: center;
`
const colorForStatus = {
error: 'alertRed',
dark: '#142533',
}
const Container = styled(Box).attrs({
horizontal: true,
align: 'center',
py: '8px',
px: 3,
bg: p => colorForStatus[p.status] || 'wallet',
color: 'white',
mt: -20,
mb: 20,
fontSize: 4,
ff: 'Open Sans|SemiBold',
})`
border-radius: ${radii[1]}px;
`
const RightContainer = styled.div`
margin-left: auto;
`
export const FakeLink = styled.span`
color: white;
text-decoration: underline;
cursor: pointer;
`
const CloseContainer = styled(Box).attrs({
color: 'white',
})`
z-index: 1;
margin-left: 10px;
cursor: pointer;
&:hover {
color: #eee;
}
&:active {
color: #eee;
}
`

4
src/components/TopBar/index.js

@ -42,9 +42,7 @@ const Inner = styled(Box).attrs({
grow: true,
flow: 4,
align: 'center',
})`
border-bottom: 1px solid ${p => p.theme.colors.fog};
`
})``
const Bar = styled.div`
margin-left: 5px;

103
src/components/UpdateNotifier/UpdateDownloaded.js

@ -1,103 +0,0 @@
// @flow
import React, { PureComponent } from 'react'
import { translate } from 'react-i18next'
import { compose } from 'redux'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { getUpdateStatus, getUpdateData } from 'reducers/update'
import { sendEvent } from 'renderer/events'
import type { State } from 'reducers'
import type { UpdateStatus } from 'reducers/update'
import { radii } from 'styles/theme'
import Box from 'components/base/Box'
import Text from 'components/base/Text'
import UpdateIcon from 'icons/Update'
import type { T } from 'types/common'
type Props = {
t: T,
updateStatus: UpdateStatus,
}
const mapStateToProps = (state: State) => ({
updateStatus: getUpdateStatus(state),
updateData: getUpdateData(state),
})
const Container = styled(Box).attrs({
py: '8px',
px: 3,
bg: 'wallet',
color: 'white',
mt: '-50px',
mb: '35px',
style: p => ({
transform: `translate3d(0, ${p.offset}%, 0)`,
}),
})`
border-radius: ${radii[1]}px;
`
const NotifText = styled(Text).attrs({
ff: 'Open Sans|SemiBold',
fontSize: 4,
})``
class UpdateDownloaded extends PureComponent<Props> {
renderStatus() {
const { updateStatus, t } = this.props
switch (updateStatus) {
case 'idle':
case 'checking':
case 'unavailable':
case 'error':
case 'available':
case 'progress':
return null
case 'downloaded':
return (
<Box horizontal flow={3}>
<UpdateIcon size={16} />
<Box grow>
<NotifText>{t('update.newVersionReady')}</NotifText>
</Box>
<Box>
<NotifText
style={{ cursor: 'pointer', textDecoration: 'underline' }}
onClick={() => sendEvent('updater', 'quitAndInstall')}
>
{t('update.relaunch')}
</NotifText>
</Box>
</Box>
)
default:
return null
}
}
render() {
const { updateStatus, ...props } = this.props
const isToggled = updateStatus === 'downloaded'
if (!isToggled) {
return null
}
return <Container {...props}>{this.renderStatus()}</Container>
}
}
export default compose(
connect(
mapStateToProps,
null,
),
translate(),
)(UpdateDownloaded)

48
src/components/UpdateNotifier/UpdateInstalled.js

@ -1,48 +0,0 @@
// @flow
import { PureComponent } from 'react'
import { connect } from 'react-redux'
import semver from 'semver'
import { openModal } from 'reducers/modals'
import { lastUsedVersionSelector } from 'reducers/settings'
import { saveSettings } from 'actions/settings'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import type { State } from 'reducers'
type Props = {
openModal: Function,
saveSettings: Function,
lastUsedVersion: string,
}
const mapStateToProps = (state: State) => ({
lastUsedVersion: lastUsedVersionSelector(state),
})
const mapDispatchToProps = {
openModal,
saveSettings,
}
class UpdateInstalled extends PureComponent<Props> {
componentDidMount() {
const { lastUsedVersion, saveSettings, openModal } = this.props
const currentVersion = __APP_VERSION__
if (semver.gt(currentVersion, lastUsedVersion)) {
openModal(MODAL_RELEASES_NOTES, currentVersion)
saveSettings({ lastUsedVersion: currentVersion })
}
}
render() {
return null
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(UpdateInstalled)

17
src/components/UpdateNotifier/index.js

@ -1,17 +0,0 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import UpdateDownloaded from './UpdateDownloaded'
import UpdateInstalled from './UpdateInstalled'
export default class UpdateNotifier extends PureComponent<{}> {
render() {
return (
<Fragment>
<UpdateDownloaded />
<UpdateInstalled />
</Fragment>
)
}
}

74
src/components/Updater/Banner.js

@ -0,0 +1,74 @@
// @flow
import React, { PureComponent } from 'react'
import { Trans } from 'react-i18next'
import { urls } from 'config/urls'
import { openURL } from 'helpers/linking'
import Spinner from 'components/base/Spinner'
import IconUpdate from 'icons/Update'
import IconDonjon from 'icons/Donjon'
import IconWarning from 'icons/TriangleWarning'
import { withUpdaterContext } from './UpdaterContext'
import type { UpdaterContextType } from './UpdaterContext'
import TopBanner, { FakeLink } from '../TopBanner'
import type { Content } from '../TopBanner'
type Props = {
context: UpdaterContextType,
}
export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error']
const CONTENT_BY_STATUS = (quitAndInstall, reDownload, progress): { [string]: Content } => ({
'download-progress': {
Icon: Spinner,
message: <Trans i18nKey="update.downloadInProgress" />,
right: <Trans i18nKey="update.downloadProgress" values={{ progress }} />,
},
checking: {
Icon: IconDonjon,
message: <Trans i18nKey="update.checking" />,
},
'check-success': {
Icon: IconUpdate,
message: <Trans i18nKey="update.checkSuccess" />,
right: (
<FakeLink onClick={quitAndInstall}>
<Trans i18nKey="update.quitAndInstall" />
</FakeLink>
),
},
error: {
Icon: IconWarning,
message: <Trans i18nKey="update.error" />,
right: (
<FakeLink onClick={reDownload}>
<Trans i18nKey="update.reDownload" />
</FakeLink>
),
},
})
class UpdaterTopBanner extends PureComponent<Props> {
reDownload = () => {
openURL(urls.liveHome)
}
render() {
const { context } = this.props
const { status, quitAndInstall, downloadProgress } = context
if (!VISIBLE_STATUS.includes(status)) return null
const content: ?Content = CONTENT_BY_STATUS(quitAndInstall, this.reDownload, downloadProgress)[
status
]
if (!content) return null
return <TopBanner content={content} status={status} />
}
}
export default withUpdaterContext(UpdaterTopBanner)

70
src/components/Updater/DebugUpdater.js

@ -0,0 +1,70 @@
// @flow
/* eslint-disable react/jsx-no-literals */
import React, { Component } from 'react'
import { withUpdaterContext } from './UpdaterContext'
import type { UpdaterContextType } from './UpdaterContext'
const statusToDebug = ['idle', 'download-progress', 'checking', 'check-success', 'error']
type Props = {
context: UpdaterContextType,
}
class DebugUpdater extends Component<Props> {
render() {
const { context } = this.props
const { status, setStatus, quitAndInstall } = context
return (
<div style={styles.root}>
<h1>
DEBUG UPDATE<br />
------------<br />
</h1>
<b>status:</b> {status}
<div style={{ marginTop: 20 }}>
{statusToDebug.map(s => (
<button key={s} style={styles.btn} onClick={() => setStatus(s)}>
{status === s ? `[${s}]` : s}
</button>
))}
</div>
<div style={{ marginTop: 20 }}>
<b>simulate update</b>
</div>
<div style={{ marginTop: 20 }}>
<button style={styles.btn} onClick={quitAndInstall}>
{'quit and install'}
</button>
</div>
</div>
)
}
}
const styles = {
root: {
position: 'fixed',
bottom: 0,
right: 0,
padding: 10,
fontSize: 10,
background: 'black',
color: 'white',
fontFamily: 'monospace',
zIndex: 1000,
maxWidth: 250,
},
btn: {
cursor: 'pointer',
background: 'lightgreen',
color: 'black',
border: 'none',
marginRight: 10,
marginTop: 10,
padding: '0px 10px',
},
}
export default withUpdaterContext(DebugUpdater)

37
src/components/Updater/UpdateDot.js

@ -0,0 +1,37 @@
// @flow
import React from 'react'
import styled from 'styled-components'
import { colors } from 'styles/theme'
import { withUpdaterContext } from './UpdaterContext'
import { VISIBLE_STATUS } from './Banner'
import type { UpdaterContextType, UpdateStatus } from './UpdaterContext'
type Props = {
context: UpdaterContextType,
}
const getColor = ({ status }: { status: UpdateStatus }) =>
status === 'error' ? colors.alertRed : colors.wallet
const getOpacity = ({ status }: { status: UpdateStatus }) =>
status === 'download-progress' || status === 'checking' ? 0.5 : 1
const Dot = styled.div`
opacity: ${getOpacity};
width: 8px;
height: 8px;
background-color: ${getColor};
border-radius: 50%;
`
function UpdateDot(props: Props) {
const { context } = props
const { status } = context
if (!VISIBLE_STATUS.includes(status)) return null
return <Dot status={status} />
}
export default withUpdaterContext(UpdateDot)

95
src/components/Updater/UpdaterContext.js

@ -0,0 +1,95 @@
// @flow
import React, { Component } from 'react'
import autoUpdate from 'commands/autoUpdate'
import quitAndInstallElectronUpdate from 'commands/quitAndInstallElectronUpdate'
export type UpdateStatus =
| 'idle'
| 'checking-for-update'
| 'update-available'
| 'update-not-available'
| 'download-progress'
| 'update-downloaded'
| 'checking'
| 'check-success'
| 'error'
export type UpdaterContextType = {
status: UpdateStatus,
downloadProgress: number,
quitAndInstall: () => void,
setStatus: UpdateStatus => void,
error: ?Error,
}
type UpdaterProviderProps = {
children: *,
}
type UpdaterProviderState = {
status: UpdateStatus,
downloadProgress: number,
error: ?Error,
}
const UpdaterContext = React.createContext()
class Provider extends Component<UpdaterProviderProps, UpdaterProviderState> {
constructor() {
super()
if (__PROD__) {
this.sub = autoUpdate.send({}).subscribe({
next: e => {
if (e.status === 'download-progress') {
const downloadProgress = e.payload && e.payload.percent ? e.payload.percent : 0
this.setState({ status: e.status, downloadProgress })
} else {
this.setStatus(e.status)
}
},
error: error => this.setState({ status: 'error', error }),
})
}
this.state = {
status: 'idle',
downloadProgress: 0,
error: null,
}
}
componentWillUnmount() {
if (this.sub) {
this.sub.unsubscribe()
}
}
sub = null
setStatus = (status: UpdateStatus) => this.setState({ status })
setDownloadProgress = (downloadProgress: number) => this.setState({ downloadProgress })
quitAndInstall = () => quitAndInstallElectronUpdate.send().toPromise()
render() {
const { status, downloadProgress, error } = this.state
const value = {
status,
downloadProgress,
error,
setStatus: this.setStatus,
quitAndInstall: this.quitAndInstall,
}
return <UpdaterContext.Provider value={value}>{this.props.children}</UpdaterContext.Provider>
}
}
export const withUpdaterContext = (ComponentToDecorate: React$ComponentType<*>) => (props: *) => (
<UpdaterContext.Consumer>
{context => <ComponentToDecorate {...props} context={context} />}
</UpdaterContext.Consumer>
)
export const UpdaterProvider = Provider

13
src/components/base/SideBar/SideBarListItem.js

@ -11,7 +11,7 @@ export type Props = {
icon?: any, // TODO: type should be more precise, but, eh ¯\_(ツ)_/¯
disabled?: boolean,
iconActiveColor: ?string,
hasNotif?: boolean,
NotifComponent?: React$ComponentType<*>,
isActive?: boolean,
onClick?: void => void,
isActive?: boolean,
@ -24,7 +24,7 @@ class SideBarListItem extends PureComponent<Props> {
label,
desc,
iconActiveColor,
hasNotif,
NotifComponent,
onClick,
isActive,
disabled,
@ -47,7 +47,7 @@ class SideBarListItem extends PureComponent<Props> {
)}
{!!desc && desc(this.props)}
</Box>
{!!hasNotif && <Bullet />}
{NotifComponent && <NotifComponent />}
</Container>
)
}
@ -85,11 +85,4 @@ const Container = styled(Tabbable).attrs({
}};
`
const Bullet = styled.div`
background: ${p => p.theme.colors.wallet};
width: 8px;
height: 8px;
border-radius: 100%;
`
export default SideBarListItem

1
src/components/base/SideBar/stories.js

@ -33,7 +33,6 @@ const SIDEBAR_ITEMS = [
label: 'Third very very very very long text very long text very long',
icon: IconControls,
iconActiveColor: '#27a2db',
hasNotif: true,
},
{
key: 'fourth',

6
src/components/layout/Default.js

@ -34,6 +34,8 @@ import IsUnlocked from 'components/IsUnlocked'
import SideBar from 'components/MainSideBar'
import TopBar from 'components/TopBar'
import SyncBackground from 'components/SyncBackground'
import DebugUpdater from 'components/Updater/DebugUpdater'
import SyncContinuouslyPendingOperations from '../SyncContinouslyPendingOperations'
import HSMStatusBanner from '../HSMStatusBanner'
@ -41,7 +43,7 @@ const Main = styled(GrowScroll).attrs({
px: 6,
})`
outline: none;
padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[7]}px;
padding-top: ${p => p.theme.sizes.topBarHeight + p.theme.space[4]}px;
`
type Props = {
@ -98,6 +100,8 @@ class Default extends Component<Props> {
<ModalComponent key={name} />
))}
{process.env.DEBUG_UPDATE && <DebugUpdater />}
<SyncContinuouslyPendingOperations priority={20} interval={SYNC_PENDING_INTERVAL} />
<SyncBackground />

57
src/components/modals/ReleaseNotes/index.js

@ -1,17 +1,56 @@
// @flow
import React from 'react'
import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import semver from 'semver'
import type { State } from 'reducers'
import { openModal } from 'reducers/modals'
import { lastUsedVersionSelector } from 'reducers/settings'
import { saveSettings } from 'actions/settings'
import { MODAL_RELEASES_NOTES } from 'config/constants'
import Modal from 'components/base/Modal'
import ReleaseNotesBody from './ReleaseNotesBody'
const ReleaseNotesModal = () => (
<Modal
name={MODAL_RELEASES_NOTES}
centered
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />}
/>
)
type Props = {
openModal: Function,
saveSettings: Function,
lastUsedVersion: string,
}
const mapStateToProps = (state: State) => ({
lastUsedVersion: lastUsedVersionSelector(state),
})
const mapDispatchToProps = {
openModal,
saveSettings,
}
class ReleaseNotesModal extends PureComponent<Props> {
componentDidMount() {
const { lastUsedVersion, saveSettings, openModal } = this.props
const currentVersion = __APP_VERSION__
if (semver.gt(currentVersion, lastUsedVersion)) {
openModal(MODAL_RELEASES_NOTES, currentVersion)
saveSettings({ lastUsedVersion: currentVersion })
}
}
render() {
return (
<Modal
name={MODAL_RELEASES_NOTES}
centered
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />}
/>
)
}
}
export default ReleaseNotesModal
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ReleaseNotesModal)

5
src/config/constants.js

@ -90,6 +90,11 @@ export const EXPERIMENTAL_MARKET_INDICATOR_SETTINGS = boolFromEnv(
)
export const USE_MOCK_DATA = boolFromEnv('USE_MOCK_DATA')
// Auto update
export const UPDATE_CHECK_IGNORE = boolFromEnv('UPDATE_IGNORE_CHECK', true)
export const UPDATE_CHECK_FEED = stringFromEnv('UPDATE_CHECK_FEED', 'https://insert.feed.here')
// Other constants
export const MAX_ACCOUNT_NAME_SIZE = 50

6
src/config/urls.js

@ -1,11 +1,17 @@
// @flow
export const urls = {
liveHome: 'https://www.ledger.com/pages/ledger-live',
// Social
twitter: 'https://twitter.com/LedgerHQ',
github: 'https://github.com/LedgerHQ/ledger-live-desktop',
reddit: 'https://www.reddit.com/r/ledgerwallet/',
// Campaigns
promoNanoX:
'https://www.ledger.com/pages/ledger-nano-x#utm_source=Ledger%20Live%20Desktop%20App&utm_medium=Ledger%20Live&utm_campaign=Ledger%20Live%20Desktop%20-%20Banner%20LNX',
// Ledger support
faq: 'https://support.ledgerwallet.com/hc/en-us',
terms: 'https://www.ledger.com/pages/terms-of-use-and-disclaimer',

39
src/icons/Donjon.js

@ -0,0 +1,39 @@
// @flow
import React, { Fragment } from 'react'
const path = (
<Fragment>
<path
fill="currentColor"
d="m1085.1 267.76v-110.55c0-7.85 3.74-12.45 9-12.21l-144.56-80.23a33.93 33.93 0 0 1 10.11 23.23v110.55z"
/>
<path
fill="currentColor"
d="m1336 406.28v-110.55c0-7.18 3.14-11.6 7.67-12.14l-141.41-78.52a34.34 34.34 0 0 1 8.29 21.4l-0.07 110.53z"
/>
<polygon
fill="currentColor"
transform="translate(206.55 -4.4141e-6)"
points="1254.9 715.08 1167.2 814.98 627.52 515.62 627.52 368.78"
/>
<path
fill="currentColor"
d="m332.03 295.73v110.55l125.52-69.28v-110.55c0-10.18 6.24-21.87 13.94-26.12l97.6-53.87c7.7-4.25 13.94 0.55 13.94 10.73v110.55l125.52-69.24v-110.55c0-10.18 6.24-21.87 13.94-26.12l111.58-61.83-0.11 18.69 0.11 350.09-627.41 346.3-0.11-350.08c0-10.18 6.24-21.87 13.94-26.12l97.59-53.88c7.7-4.25 13.95 0.55 13.95 10.73z"
/>
<path
fill="currentColor"
d="m1175.8 1193.4 2.8-121.54c1.28-55.46-31.41-116.37-72.65-135.34-41.24-19-76 10.88-77.3 66.34l-2.8 121.54z"
/>
<path
fill="currentColor"
d="m294.46 814.91q1.86 205.53 76.34 387.56 67.47 166.41 186.66 290.1 107.94 114.69 236.09 166.43a98.88 98.88 0 0 0 40.48 9v-1152.4z"
/>
</Fragment>
)
export default ({ size, ...p }: { size: number }) => (
<svg viewBox="0 0 1668 1668" width={size} {...p}>
{path}
</svg>
)

13
src/icons/TriangleWarning.js

@ -9,8 +9,17 @@ const path = (
/>
)
export default ({ height, width, ...p }: { height: number, width: number }) => (
<svg viewBox="0 0 17 17" height={height} width={width} {...p}>
export default ({
height,
width,
size,
...p
}: {
height?: number,
width?: number,
size?: number,
}) => (
<svg viewBox="0 0 17 17" height={height || size} width={width || size} {...p}>
{path}
</svg>
)

30
src/icons/device/NanoX.js

@ -0,0 +1,30 @@
// @flow
import React from 'react'
export default ({ size = 30, ...p }: { size: number }) => (
<svg viewBox="0 0 6 16" height={size} width={size} {...p}>
<defs>
<path
id="a"
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z"
/>
</defs>
<g fill="none" fillRule="evenodd">
<path
fill="#000"
fillRule="nonzero"
d="M5.75 6.835a3.509 3.509 0 0 0-1.5-1.105V1.75h-2.5v3.98a3.509 3.509 0 0 0-1.5 1.105V1.666C.25.884.884.25 1.666.25h2.668c.782 0 1.416.634 1.416 1.416v5.169zm-1.5 7.415V9.5a1.25 1.25 0 1 0-2.5 0v4.75h2.5zM3 6.75A2.75 2.75 0 0 1 5.75 9.5v4.834c0 .782-.634 1.416-1.416 1.416H1.666A1.416 1.416 0 0 1 .25 14.334V9.5A2.75 2.75 0 0 1 3 6.75z"
/>
<g>
<mask id="b" fill="#fff">
<use xlinkHref="#a" />
</mask>
<use fill="#FFF" xlinkHref="#a" />
<g fill="#FFF" mask="url(#b)">
<path d="M-5 0h16v16H-5z" />
</g>
</g>
</g>
</svg>
)

1
src/icons/device/index.js

@ -1,2 +1,3 @@
export Blue from './Blue'
export NanoS from './NanoS'
export NanoX from './NanoX'

63
src/internals/index.js

@ -1,16 +1,18 @@
// @flow
import '@babel/polyfill'
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import 'helpers/live-common-setup'
import 'helpers/live-common-setup-internal-hw'
import commands from 'commands'
import logger from 'logger'
import LoggerTransport from 'logger/logger-transport-internal'
import uuid from 'uuid/v4'
import { setImplementation } from 'api/network'
import sentry from 'sentry/node'
import { EXPERIMENTAL_HTTP_ON_RENDERER } from 'config/constants'
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import { executeCommand, unsubscribeCommand } from 'main/commandHandler'
require('../env')
@ -58,64 +60,11 @@ if (EXPERIMENTAL_HTTP_ON_RENDERER) {
})
}
const subscriptions = {}
process.on('message', m => {
if (m.type === 'command') {
const { data, requestId, id } = m.command
const cmd = commands.find(cmd => cmd.id === id)
if (!cmd) {
logger.warn(`command ${id} not found`)
return
}
const startTime = Date.now()
logger.onCmd('cmd.START', id, 0, data)
try {
subscriptions[requestId] = cmd.impl(data).subscribe({
next: data => {
logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data)
process.send({
type: 'cmd.NEXT',
requestId,
data,
})
},
complete: () => {
delete subscriptions[requestId]
logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime)
process.send({
type: 'cmd.COMPLETE',
requestId,
})
},
error: error => {
logger.warn('Command error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
},
})
} catch (error) {
logger.warn('Command impl error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
process.send({
type: 'cmd.ERROR',
requestId,
data: serializeError(error),
})
}
executeCommand(m.command, process.send.bind(process))
} else if (m.type === 'command-unsubscribe') {
const { requestId } = m
const sub = subscriptions[requestId]
if (sub) {
sub.unsubscribe()
delete subscriptions[requestId]
}
unsubscribeCommand(m.requestId)
} else if (m.type === 'executeHttpQueryPayload') {
const { payload } = m
const defer = defers[payload.id]

32
src/main/autoUpdate.js

@ -1,32 +0,0 @@
// @flow
import { app, BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
type SendFunction = (type: string, data: *) => void
export default (notify: SendFunction) => {
autoUpdater.on('checking-for-update', () => notify('checking'))
autoUpdater.on('update-available', info => notify('updateAvailable', info))
autoUpdater.on('update-not-available', () => notify('updateNotAvailable'))
autoUpdater.on('error', err => notify('error', err))
autoUpdater.on('download-progress', progress => notify('downloadProgress', progress))
autoUpdater.on('update-downloaded', () => notify('downloaded'))
autoUpdater.checkForUpdatesAndNotify()
}
export function quitAndInstall() {
setImmediate(() => {
const browserWindows = BrowserWindow.getAllWindows()
// Fixes quitAndInstall not quitting on macOS, as suggested on
// https://github.com/electron-userland/electron-builder/issues/1604#issuecomment-306709572
app.removeAllListeners('window-all-closed')
browserWindows.forEach(browserWindow => {
browserWindow.removeAllListeners('close')
})
autoUpdater.quitAndInstall(false)
})
}

24
src/main/bridge.js

@ -10,10 +10,10 @@ import logger from 'logger'
import LoggerTransport from 'logger/logger-transport-main'
import sentry, { captureException } from 'sentry/node'
import user from 'helpers/user'
import { executeCommand, unsubscribeCommand } from 'main/commandHandler'
import { cleanUpBeforeClosingSync } from 'helpers/log'
import { deserializeError } from '@ledgerhq/errors/lib/helpers'
import setupAutoUpdater, { quitAndInstall } from './autoUpdate'
import { setInternalProcessPID } from './terminator'
import { getMainWindow } from './app'
@ -84,10 +84,18 @@ ipcMain.on('log', (e, { log }) => {
ipcMainListenReceiveCommands({
onUnsubscribe: requestId => {
if (!internalProcess) return
internalProcess.send({ type: 'command-unsubscribe', requestId })
unsubscribeCommand(requestId)
if (internalProcess) {
internalProcess.send({ type: 'command-unsubscribe', requestId })
}
},
onCommand: (command, notifyCommandEvent) => {
// ability to run command from the main process
if (command.id.startsWith('main:')) {
executeCommand(command, notifyCommandEvent)
return
}
if (!internalProcess) bootInternalProcess()
const p = internalProcess
invariant(p, 'internalProcess not started !?')
@ -158,13 +166,3 @@ ipcMain.on('sentryLogsChanged', (event, payload) => {
if (!p) return
p.send({ type: 'sentryLogsChanged', payload })
})
// TODO move this to "command" pattern
ipcMain.on('updater', (event, { type, data }) => {
const handler = {
init: setupAutoUpdater,
quitAndInstall,
}[type]
const send = (type: string, data: *) => event.sender.send('updater', { type, data })
handler(send, data, type)
})

49
src/main/commandHandler.js

@ -0,0 +1,49 @@
import { serializeError } from '@ledgerhq/errors/lib/helpers'
import commands from 'commands'
import logger from 'logger'
const subscriptions = {}
export function executeCommand(command, send) {
const { data, requestId, id } = command
const cmd = commands.find(cmd => cmd.id === id)
if (!cmd) {
logger.warn(`command ${id} not found`)
return
}
const startTime = Date.now()
logger.onCmd('cmd.START', id, 0, data)
try {
subscriptions[requestId] = cmd.impl(data).subscribe({
next: data => {
logger.onCmd('cmd.NEXT', id, Date.now() - startTime, data)
send({ type: 'cmd.NEXT', requestId, data })
},
complete: () => {
delete subscriptions[requestId]
logger.onCmd('cmd.COMPLETE', id, Date.now() - startTime)
send({ type: 'cmd.COMPLETE', requestId })
},
error: error => {
logger.warn('Command error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
send({ type: 'cmd.ERROR', requestId, data: serializeError(error) })
},
})
} catch (error) {
logger.warn('Command impl error:', { error })
delete subscriptions[requestId]
logger.onCmd('cmd.ERROR', id, Date.now() - startTime, error)
send({ type: 'cmd.ERROR', requestId, data: serializeError(error) })
}
}
export function unsubscribeCommand(requestId) {
const sub = subscriptions[requestId]
if (sub) {
sub.unsubscribe()
delete subscriptions[requestId]
}
}

86
src/main/updater/createAppUpdater.js

@ -0,0 +1,86 @@
// @flow
import { UpdateIncorrectHash, UpdateIncorrectSig } from './errors'
import * as pgpHelper from './pgpHelper'
type Opts = {
filename: string,
computeHash: () => Promise<string>,
getNextKey: (?string) => Promise<string>,
getNextKeySignature: string => Promise<string>,
getHashFile: () => Promise<string>,
getHashFileSignature: () => Promise<string>,
}
export default function createAppUpdater(opts: Opts): { verify: () => Promise<void> } {
const {
filename,
computeHash,
getNextKey,
getNextKeySignature,
getHashFile,
getHashFileSignature,
} = opts
// main logic:
// - fetch hashFile + its signature
// - verify signature
// - compare hash with update hash
// throw if any step fail.
async function verify() {
const [hashFile, hashFileSignature] = await Promise.all([getHashFile(), getHashFileSignature()])
await verifyHashFileSignature(hashFile, hashFileSignature, await getNextKey())
await compareHash(hashFile)
}
// compute the update hash and compare it to the hash located in hash file
async function compareHash(hashFile) {
const computedHash = await computeHash()
const hashFromFile = extractHashFromHashFile(hashFile, filename)
if (hashFromFile !== computedHash) {
throw new UpdateIncorrectHash(computedHash)
}
}
// verify hash signature with given pubkey
// if it fails, try to get next key and repeat
// if no more key, throw
async function verifyHashFileSignature(hash, sigContent, pubKey) {
try {
await pgpHelper.verify(hash, sigContent, pubKey)
} catch (err) {
try {
const nextPubKey = await getNextPubKey(pubKey)
await verifyHashFileSignature(hash, sigContent, nextPubKey)
} catch (err) {
throw new UpdateIncorrectSig()
}
}
}
// fetch the next pubkey based on the previous key fingerprint
// also fetch signature, and verify against previous pubkey
async function getNextPubKey(pubKey) {
const fingerprint = await pgpHelper.getFingerprint(pubKey)
const nextPubKey = await getNextKey(fingerprint)
const nextPubKeySignature = await getNextKeySignature(fingerprint)
await pgpHelper.verify(nextPubKey, nextPubKeySignature, pubKey)
return nextPubKey
}
return {
verify,
}
}
// a hash file looks like: "<hash> <filename>\n<hash> <filename>"
// we only need the hash for the given filename
function extractHashFromHashFile(hashFile, filename) {
let hash
hashFile.split('\n').find(r => {
const row = r.split(/\s+/)
hash = row[1] === filename ? row[0] : ''
return !!hash
})
return hash
}

62
src/main/updater/createElectronAppUpdater.js

@ -0,0 +1,62 @@
// @flow
import crypto from 'crypto'
import path from 'path'
import fs from 'fs'
import network from 'api/network'
import { fsReadFile } from 'helpers/fs'
import { UpdateFetchFileFail } from './errors'
import createAppUpdater from './createAppUpdater'
import pubKey from './ledger-pubkey'
export default async ({ feedURL, updateVersion }: { feedURL: string, updateVersion: string }) => {
const { app } = require('electron')
const updateFolder = path.resolve(app.getPath('userData'), '__update__')
const { fileName: filename } = await readUpdateInfos(updateFolder)
const hashFileURL = `${feedURL}/ledger-live-desktop-${updateVersion}.sha512sum`
const hashSigFileURL = `${feedURL}/ledger-live-desktop-${updateVersion}.sha512sum.sig`
const keysURL = `${feedURL}/pubkeys`
return createAppUpdater({
filename,
computeHash: () => sha512sumPath(path.resolve(updateFolder, filename)),
getHashFile: () => getDistantFileContent(hashFileURL),
getHashFileSignature: () => getDistantFileContent(hashSigFileURL),
getNextKey: (fingerprint: ?string) =>
fingerprint ? getDistantFileContent(`${keysURL}/${fingerprint}.asc`) : pubKey,
getNextKeySignature: async (fingerprint: string) =>
getDistantFileContent(`${keysURL}/${fingerprint}.asc.sig`),
})
}
// read the electron-updater file. we basically only need the filename here,
// because the hash file contains hashes for all platforms (better to have
// only 1 file to sign lel)
export async function readUpdateInfos(updateFolder: string) {
const updateInfoPath = path.resolve(updateFolder, 'update-info.json')
const updateInfoContent = await fsReadFile(updateInfoPath)
return JSON.parse(updateInfoContent)
}
// compute hash for given path. i guess we only need that here
export function sha512sumPath(filePath: string) {
return new Promise((resolve, reject) => {
const sum = crypto.createHash('sha512')
const stream = fs.createReadStream(filePath)
stream.on('data', data => sum.update(data))
stream.on('end', () => resolve(sum.digest('hex')))
stream.on('error', reject)
})
}
async function getDistantFileContent(url: string) {
try {
const res = await network({ method: 'GET', url })
return res.data
} catch (err) {
throw new UpdateFetchFileFail(url)
}
}

40
src/main/updater/createMockAppUpdater.js

@ -0,0 +1,40 @@
// @flow
import createAppUpdater from './createAppUpdater'
export default ({
filename,
computedHash,
hashFile,
signature,
pubKey,
pubKeys,
}: {
filename: string,
computedHash: string,
hashFile: string,
signature: string,
pubKey: string,
pubKeys: Array<{ fingerprint: string, content: string, signature: string }>,
}) =>
createAppUpdater({
filename,
computeHash: () => Promise.resolve(computedHash),
getHashFile: () => Promise.resolve(hashFile),
getHashFileSignature: () => Promise.resolve(signature),
getNextKey: async (fingerprint: ?string) => {
if (!fingerprint) return Promise.resolve(pubKey)
const key = pubKeys.find(k => k.fingerprint === fingerprint)
if (!key) {
throw new Error(`Cannot find key for fingerprint ${fingerprint}`)
}
return key.content
},
getNextKeySignature: async (fingerprint: string) => {
const key = pubKeys.find(k => k.fingerprint === fingerprint)
if (!key) {
throw new Error(`Cannot find signature for key ${fingerprint}`)
}
return key.signature
},
})

5
src/main/updater/errors.js

@ -0,0 +1,5 @@
import { createCustomErrorClass } from '@ledgerhq/errors/lib/helpers'
export const UpdateIncorrectHash = createCustomErrorClass('UpdateIncorrectHash')
export const UpdateIncorrectSig = createCustomErrorClass('UpdateIncorrectSig')
export const UpdateFetchFileFail = createCustomErrorClass('UpdateFetchFileFail')

111
src/main/updater/ledger-pubkey.js

@ -0,0 +1,111 @@
// TODO: it's actually my key. replace with qorum key.
export default `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFvQ9SEBEADbDiM5dfhqdZae0QFSd9QVf8k7hmt+7Ybz4vwRN+s3Xfz9feBC
QBu/UePLpc18Jb1nnHUa7bf3FRilvCG0Lwl0SrSYJ7dmOPsWqE4lHnuBF1AAzFh0
MR/I6ydOl9fe6thamzv1a61gV+yp+fSKHS0BUv+jto1XV0YOpKnODxOnVIh0n+pf
KpsGSLug0/S6yLZCQPZucTNegZxe96XTaRE61Lr0PrqiOZMtJMSFQ5qzUuvRirSg
q2uOosfM0JiCIogr5myXk3NPAv1wD236+J/Rvh6fOxH9jvvpbSHJkeCBQbLnp+/6
ubkxg3vgqWwSFNxxOuqbyv0h2dEdnmUUAk5nUOkXt4Pj5f/RqSYe3W4TXEGAvVgP
ow4nfEyMEwM/c6frct3iOHLj6D5a4E1DPHxQ6f8WxjtCUQJPvbBpszArPX2jpjPx
6kEmhCRavdR5veXZldOquWeiaGX2SpV9S6YdgcKe9/XnytpryUT6So3pAQBw18md
vj89wThSTDIVGcD5OPE82VmAAFzgBrUBXHYqP2pxq7wL78zV8sjLYVF0Q8eNplNf
mCtI2ppRZYFv+Epr0N02urSuvS8uim/ZBtaHfV/7vD7h5prNVEVv+kLvXjNSl3p6
ShCm8V0dtbVoQ4Vh3HU06t3jOg0YXu1o+KopezT58v9kl9N4yUgJmXDfUQARAQAB
tCtNZXJpYWRlYyBQaWxsZXQgPG1lcmlhZGVjLnBpbGxldEBnbWFpbC5jb20+iQJU
BBMBCAA+FiEEyrtcOETZkizulGGq/pgOS6k/n0MFAlvQ9SECGwEFCQPCZwAFCwkI
BwIGFQoJCAsCBBYCAwECHgECF4AACgkQ/pgOS6k/n0OHhg//Z6y8dFJnRgVOr8yK
BGIYM58mikMpdbt7/XByVW3t8tZqZlhhLfRVU/u6Jz69R1bggbJ1Feii3NQB/E3W
pBX7IEHy5uh/12ldkNISdVLbMFDsoCQSdZ+zA4lLtkKrVIE/3IQQ378XrkHLeAQL
W6+r8Ft9UsNOY1///pbLG07qPYjO6eLexZaNQzXOYrG3PZCxAWS3crFavm9sEjAh
FRX+XGhi9YaD3MSiafow2PygVa+3v3CNH7Q6yg52BiOI8MtqJvbbGC8SqS6/5zCA
mcwlGJPveFlIjaE40RSYgOBVBGSLDXJwLTnMoi99sG59BwaNpU3dCYJAVOwWO4tI
XV0UUtavEFaIuxu059x+kYzLLhtTOd5i6EB896KOQf5XG79/z/CYUZ3Br0bIDjQG
PqSSPiIKt6Z5KApB2vNNuUWz+J1+X8KF6lc0U4b32b/RTtMWKobzN+ivcKPAOzZn
E5s8zRafqdjo6azTnFuz1axQOuIXpV5ZBfUx/uoJH6T6PFjyCQ9Kw/FPoSY3V+0S
ZSAA2sJzDgFBdRT9TpSBaM9X36N4EswcScLllwy4gDA1A6zt4FOnbmP0utXYowFW
6fVa750SZTog+Pim89ZnW/MFRpgFg0bUd1b+uLLM1c6m1SOgmswLzkFAJIVzo57g
W01BCSJ9Oy/6Cj9FA8+OBkLC8Z+5Ag0EW9D9qAEQALJ8pyYuxt6CLz42gh0saTGn
wgxjFv4tFGx00c89zuvlmHlSwSF7LWfczFZtR5vnDChT70ltl5SR1sBKSSgSRIBC
B1N3JzdWoYuj54cGBcJpgRQQa8SDWRTkiWS1AgQxzDuS/KEpowdwFcL1rTjOMzAM
nDU6UVmVgVMTl4inTogKik+dQj8Jw1GO0b+R2sMaa/ox9dZX3WnIi4ejG0nzzpOm
aQLbnvj0OjKhrN/kaCE5Oxk5TQlLoY6tv5uj84nKKWtsuwQ0ILRR+1kuy8VuLGab
lAYV1O2jUP7VmFMcB1PGTh7+ZioEA/e2vZ98k8l31Uqc2PG4fVgYP+eJ0831a93L
ExrUW+aKEO9XKQOB2ktRgoreVWntREH+L7pYCPTFkhN6tU3qzcT4wFC0UPRKS7zt
o7IBZ9yOtQkPRP/llOMOo1QSLhuauM8U0OVc6TCYFaWyepAf6Fg2yWXnZIuVcyUk
NfrfvpYTboC/5I+xz6lBoAg91FvNVgqK4us7VduuepXD6MrEyt2VskcNN9ZIw/L7
AJUE+IBGxeyBZbHlQt1A750FgjkmZu+k5wd6C0UTpskUobpNZGHJnXjqh+5sCGPC
0fjU1zSHHDnd4ZTcuW5eQyKo4IT5Uy7K9zm3AZmrSIG7wK6MfTlrwoZxMNlOB4b5
OcCPC+ZtFjYFcnnY5SuhABEBAAGJAjYEGAEIACAWIQTKu1w4RNmSLO6UYar+mA5L
qT+fQwUCW9D9qAIbDAAKCRD+mA5LqT+fQ5ifEACIaAqtAzHmjldw8DEnKrVENczy
t/uyvCsh2QWWR/7QtMkOdEEFLx2u9mFCyQdASkW6e2MHTz8aBODaoNgGZKOHLHTB
CYG+RY4FglRk6RBc3ylyprO0NeIutQ7b6tQtcPZcaJhwAJzmF2SsxyjR94RPL+ud
6cuM/uHNAYvHjJEk1oiUQC2Jmph9r4sTO+RVxYsuXqtHm+YMmk5mFTLDf/YxjYMY
3T0OsLd1hHlN+ADaiPodY9SG/FDHBxG3/01lKJnX6hrkUUP+O7X6KopDBL4xOPMh
GNKKxsGXBmapOLREJMA77IWOWtr9+XJO3svwtOzEinMAzj2eY1on9zFQNHAuk3lQ
C4CLD3e7OF4DtXchfeCvzSMSTNSF/ghXH/TrpnSb27xb2EFH1uc1c+dnRxoHp59a
OuzlwaScluQ8U1QV8Okfi0gbGl7jN6pDuixvyTVF9mKdkUajchtcK9oFjmfyeEi+
GzHZ8K8GY74XlZMn8tq/A7DT/24PfAC19x8jGoKZ9g9u4Z6cB93x/cuFo3ld0hO8
8oFO97l+y82TmbXNAWX1BNI5V/ANOgGcMtjK99/3FdPyy+11N7WYjct5obaYKZNF
VmHa7cORClDw83QfZw7tVjMIbvhhPIu2qWmtOlVKeZrI1IDl8fh7bvzv61ywrksF
9Ab3Bblh+XCZC/hcK7kCDQRb0P4uARAA0h4LFXAamHlSC7UbpIh8VBvdgKJOXLMi
5RaACw9TOBTOk718Ft9rBqOFgTtLohT88QVIJ4yET/SQDJ3flKwVQvxZ9hdohbRh
ETfNy3eOCo/TXrEtdx9AnZ0Yr7r2R6JToDQyKVbUsMCV5Sxs1b/dHfI9f/A0Hy+E
AHhxjiEbFL46xD5vzEzR7qViErkvTE4KBBVhbFCfMM0OE1fkGGI/457GSqw39ZgF
GTXbmwhsEPO0pkSY25+DIPi7M4CRQGI4IkbL7p0HWk2nWwH/QAUxVTYISTkUIFLA
coWxbbr2zUgLN8znii8mejHcBgdd6RcnANaNe/P3OKStyIeVfNnDH3x6IgKX/xZC
1fo3n85lbliKuT1hgjBWlBv3xblki40bTNeyOYS0RQHSRTlhCjJZyQGtmPWlJ3c3
+Iebp1r3c3LAlqJ3R/ME/MDEZ9ubGLGzbm3Of1L+67nj1SERlgvgKPHW74kI9sTR
XlkRRokEvQI/I3CGPkfDP1MfjZMupwP+urAQDBePmB+wLJNNzSPe8HoipJsoDjpE
mYfRlUWDxMzI26q65xqHVzu87/QKhCQMlbSewwppVJgpM57zB5SGH9PT8skvQqjt
E6WdmlBa0znvOxyhl/fnCA8gcX6kWKX+6/HzRH3pgoiLpbuJYYIp1etLUrezWJMc
IK/TN35z/McAEQEAAYkEbAQYAQgAIBYhBMq7XDhE2ZIs7pRhqv6YDkupP59DBQJb
0P4uAhsCAkAJEP6YDkupP59DwXQgBBkBCAAdFiEEvLj3asBszhCpOG1upLQHgyCc
iJ0FAlvQ/i4ACgkQpLQHgyCciJ0UKw//VXuIjaEAeVBRDj1HuCm/GBV4DLfqGYqI
vp8tyiEk1iRaAXGrYfGlsQpWm1fCAzAgaJo4mY1NKhmufiLVBCRK3+AMAaMZ3W4c
rRM+Ww2CKWetgufqFxnmLgr/3rdQcI9+JhZ5TN/2Zq9Dx+xtzE3Yf/8+vXQwzL33
KBk2hnh6XOSb3FgSBgtJrIEenlpw7kH5QyAHjI7FCF+7zSw6G84N9T9MCOVp4mh4
ZzGLSPCcQIYzolXkn5rlzrER779JTuG/MzCK6eSWtPuCMB11b9RbeyWMuZ+pSko3
CTz3QWIHzSGmeheOgRPE7a27Omnz3O2F+kh6BvBQmboRJFJfdxyojnm2K5i/THlZ
cOhT1Y63xTw/VOB3gakSCwfyLiwgmgj/jv6Sw98Lc3Y6Cydr1NBDxFv3LWX08vEJ
3f3ceonemJpO1l+W7RhkeRkTvgJFAmRoWTXWff9HEqv15ADNBIZwzRQ0gZ1NSsNS
90+JCCRG6gxxByPrCpfnema/mwcnZsesJhkDSJdyMiS5PVr3KEe3H6K22zVWHYGB
8FwdzprcCYY/oqJaE/w2YPjktUWDNUDd1CoGIheVmQB7q6pEXCWSuEZX20i8BgjT
ektHSi37tpTW9EgD+KMTNqJcJM7ncdno3ss4s/A2Bs8Jgv75kr2pxBwJc1+P1kB2
AZ0ak8BqXa/8DQ//acFEOZd7HU4HZVns1BkezIH9TNKzO2gVMi8MmGY3LohnINFE
Zs1IAb5X5jE6kouLukFBjz0iqPNdpEpTyXXC2MJFLwrAPuU7MXTFw7aYvO7vIYBW
EZjztho6OCHczMOEC9df79V/ThyDFDlY63PrXoIl7tPlRO2cSiIkF1M8Tq0o5FEl
I3Lcu5isu/yfgL1t32SL1UFf0cUAH20hC7K1tUao5QoiR5McpgXvnm7o5mar+bbc
2OtQMdqxQ8InuRPYZVD27CP25ux89FLXFYy6tnwaoLfR/9KLbUcOKf0pxtuNjgmg
yfGqqMSE9ayY55Ge70sBTQ05LdRn6W2VwcM9ko1ysehIer9JscKsCmK8PpXv2xnj
iCzpz7x+PuMQfaeBQspVyAHS8qviT5FBgVHJyFqbEVbYye/jrlRAKxuwlY3h0UDy
ZREwu1wmH5UEiqNlhl5qvOw/27ME6eAD+wTthtMFjx6Pleul7iUD1ZZBWvIb03Cz
rIt1/7hzQ0VMCIqTMWO+djxEK0LsADDdbEd45LGgmeMjJvhtomc5NSOZKReqSaWF
K2GghEcJrMpPq55g5C0gruhdk8LnPOCxYa/8WdomwUOpPVJQVArif4JRlqt3DRL3
3kIpTYIc2r1GYs8aB8vONuvpPVaxYnLYW//Ovt7gkdB0NWhwtZpXVh3bQ2+5Ag0E
W9D+RwEQAM/JS0kM276VusKXdLzRrLyPIM6HK7l+/J3rBWb2z9T2fB14YXSJ/erI
q+KkqGOtGZRdUNjVKzWfAkMVHn1mPD8GWEfBTvZ/rcxld3szBHInqj2+tODuQp/Z
aQQ5mdzifeL7U54jVktWr5x0YV+0WteA/siXnCj4+MUxER4EFu/iJQD71l8dKA8c
Kz5q9r0hOR7ZWOBr/ufBkMTxzAyu2FJJav+lIDGAJo2ie3BL7B/c0fTBiQdRuJYw
YDS8/dhB7TH75IFFm1h7xlYT+9kR8Y0ejCGDsqeusrsCn1OswWD9xrm/QHl9olpw
phNT+LeADS5hNEN1zzPKM5vtXGWHZsnR4v8zKfVKXcd0rL7z2MXuY1fMde3a/plo
a3qzTEPYibclBzt8Htf822BgMzAOLFGHI4VQ/1NbOq1LB+wUmIUZVwiYPaANw08R
7a0XOnIw1Q/clmanoSuTBaroXEsbDUrgo7oBLRQxPlTi1n6EcIP3J5EvwuHMe9RF
De/JmicpCIN+PZmi4TETJqnefXSfs8+NAbabAUhed+iZos1WhM2jTQV1Ac61tSRl
nT5awO142mJE6eVIE0aSmGsdkMh/R3cMjQENzM60I4kdCS8YqmT3CBZBLrM3cjJ8
46ILqFNgNLKo/Tn4UHE9TVMPmqOPDG7Ye/OCf7mxaJAQ06i/316tABEBAAGJAjYE
GAEIACAWIQTKu1w4RNmSLO6UYar+mA5LqT+fQwUCW9D+RwIbIAAKCRD+mA5LqT+f
Q3IsEADOELqrSn85J0NGKW4v7eji7xfT4jCoQupx9D1N6jcnfLbO+obyJm+3qKKZ
KvvY4cwCDHEWuDGFra78vZXxEVGEjpMjrPZtPvCBgmfXY/Pit7lt555oUu+S98Lq
zI1U9ASEwVEFQRE/7RCPf9kGlZ8EQj7/oRzMZGGce+lxo5kX/tcswNow9ikgvfpc
os36RcPRh3ivbQJewaE4gckOc32LLQJjdRPGBhcQA3JNNIiNZkI7qLkICzjbdpL2
SX7Kzyh2AIa1Rh2Rx0CA8D3louS1wDBnwj072wEZCLylQrc5J2NdwVsyHcioH3F5
YLwcKtHLvPjQebqnYcQxNFs49TjHBnR8IvwMWiB8bNZBlTr4CzHr4lIqoTVFVopD
OX1kZkqJ2gaj03hbcin9JdzhK1LAKahfhLxD+zMYPTw4K8mzvYtvuvfOZU2BzCfB
l9yvtrF3GXu94iP29sq/QLehl8IF7TyHBZr7a9LEF1boa7GDBkPcFkuKSymTCQii
VFlYGtM46Q063vXBCFg6NnEl/0Qcvurr6RlDhTxOubxKZaZdvUwDAarDqHWpEeXo
edRfWORPob99dvdcajU0yKWXPzqMtaO8Orsm/P9rXq6fR7LVmJ6o7g+gorJDE8aB
+m3OuSSqq19QL8cmwZ1lVMdGOD8+knAxrr1+bq33QdekBycZ+A==
=97OE
-----END PGP PUBLIC KEY BLOCK-----`

33
src/main/updater/mocks/mock-privkey-1.asc

@ -0,0 +1,33 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQHYBFv8BAcBBADRLTFoRU3IUz37E70wy2jv7jg7QLSoHnJglcUYzq+RCBnQAfxQ
NOcQtJDKOJyo1v3819o5pxnrxWAutM/CNpjanzcWM1yLfHjrKo8BbHlmIbWBmV7w
8te+rm/VlyNi30D1eIIJy276nByW4rMLcfFe4T055xbEvRNvQzR+KfrCBwARAQAB
AAP8Dv3XMart5TKaGZmTkKCzd6ayHwUHLZlpByUUyC9gA9v5xZ+uzqzL9rWKPwQJ
rkwiTfHGcSVZxT0TJKXO0hOJpthgqbMnHokIhrYeNrDbU4l4tgE74Ik+8F3dGGns
C48Q+6lfACbpLMz7wq+60lLT7tzBKgYGA4G35W9+CY9jz8ECAOX3SpqFpOkoYcjh
GsmBSlTs9Kz4JyZDdigiM7xrroe9zkXtwuGUY2X14le/vCoZl3EP/WYV2CY8uODm
cMTYbaECAOjbZZPjudH9Ux234qdv/lMsiYwZ86aQmSUnaR2tNrTmlMiW60yKbsMC
smP0A+wZvpI9U6hMU8cbNtN7SW8WfqcCAOabL3NCCeQbDQBHI/0oKqrSIkMTQzKL
1MdUcvitzHBiKrOGF0PUgiZ0mNVXsSvLH0grULllAwfws3UeRmYGVwWfE7QiRm9v
IEJhciAoTW9jayBLZXkgMSkgPGZvb0BiYXIuY29tPojOBBMBCAA4FiEERZ/aCM7e
heCvgo2lv5b5SATkBG4FAlv8BAcCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA
CgkQv5b5SATkBG5tlwP/V1nUnbcJloznFVZFQJtLdCd9IG15LYeexd0/B3sBZm5d
Jze29tkNWblHc+xBxAp6PuuRPyvBKZd4grPNCfD8tzFLRv3jeVXrrYjOzb4G6ug6
15Zd0jFn6taCykCpgLcuKLDXm9lnCIR9cHD0IHhojdjdLjGbL1Kpu/N8xoOJlcOd
AdgEW/wEBwEEAOUUNwoEzjt/+YReQsPmQsKuBi9KxU1dvbcI0OG5xYdlp4ZgG9Gj
hJwvvH1aHiNU3qyRLF2PjL7dmM6xFIppTF8sakW2H34oE2nY9IS0HYdFNaIag0vk
G0isSrdRuVl38EClkvEEXnb/L7y/J3EDiBwW+4jWTd0SnS9C1IX+pzozABEBAAEA
A/9e21eNIP9K2Qu3uZZ46wSS+5+Y/qOjjSENRSvFhHjcP4Y2mN58GPNE/2lUGLhp
FfAc0gJmfZYgxL7NO7tPMxoJeFs7oo7duWua2rugLHUVH55ApiJXzOMlX4rpDNzR
eYsI4WOUgq1bMTjLoy/Zuz00IrItEuPqKFWot9CI+Rz8uQIA8bCtS1bn/2Tg9Bww
td3WQqERz9AOE3eSDynRrZ/APJrphMhEvL2YC03kS8U5x32cSq0vS8fMlUuGZXvf
Nxfq2wIA8qRjmyFCG+V3wUC6lymWj8DAh+BINCmuDRiXCzHQ1D1q+Nid1bO18dvd
tI81lmn7IIn9esKxU8ym/z9o22YRiQH/SHFD/b/KSZKKQnKOrcKtj2gLT0MW/gL2
mUxenook+hEoZyBwIOEabz2o5rznmXCXnxlgZ2sIX4Ymx67b/0BtuqaSiLYEGAEI
ACAWIQRFn9oIzt6F4K+CjaW/lvlIBOQEbgUCW/wEBwIbDAAKCRC/lvlIBOQEbkeR
BACd1FWQvo1qNTuNC9IwHLLhBQbT8OEHn/TIP2daR0dNjsvcfvpmX4kXEHWFRW7G
CWaoWMNYtWFk8GrGXKDjZ50wLCw9rj/xJdjdz8vDjVZT3Si0MOrDI9WJLbBNrCBO
h52YTF89KXceUoBVqOgVta4ArbiagOK35ezlWjvg6H2slw==
=IP04
-----END PGP PRIVATE KEY BLOCK-----

33
src/main/updater/mocks/mock-privkey-2.asc

@ -0,0 +1,33 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQHYBFv8BCEBBADA0YaD4Sa50kD9g2o/t6qkMMBZtmYt0kh/F+rAEBCYQL1H6dhA
FAUt7k9EYLAzUw03ByAB2WR097te1xEos9eUWz9YIUMJxE6Tk66kys0z+X1uZQcc
Gywxrg9EsMWQPwDSOJjFEKsMV81X7UjInh38+Dx/gy8sLUUClXDpo7HZiQARAQAB
AAP+ITy85ETOaR+uJkUd7ofZeV4R+iTv7hwedRIJXDwosCYQurG7nigVUHqQ3WeR
V/eMAp1OziLMJ0GA4fNOuOZvVY3ZuwcQFaejRMkItOgvuc4lZZ5IvZzoQrJ04NOO
mT6dugZpD3MYtnxoEftNvjEE/ezc1Wch5Nu06jo0fqb5tsUCANhuUemRQkqL+1sZ
HdhQT3vgxRae1WebH8pEjczLflNteydGE8+ND38tAYaQOqSj8Gs6v7uH9YUwHMMZ
vVT6/msCAOQSEFQ40mG9x+pTLWqfHFR6VeBKOuz+AnygYKIjKJ8eTgXay/2bT9Pg
qbeDYYTtfB0XFvUDd1b7xLBPZ1QTnNsCAI/8OTu958QxWoApsH4vEj7fuQXbOyYs
LERh5o7rMUeAacWStv5+i6sBLo63KQWLlfo/vjVJFQI1nxLApg8JKIubkbQmRml6
eiBCYXJyIChNb2NrIEtleSAyKSA8Zml6ekBiYXJyLmNvbT6IzgQTAQgAOBYhBGcP
SZyL0UpLah6eU55NLhDLPtM/BQJb/AQhAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B
AheAAAoJEJ5NLhDLPtM/xMkD/2YwwDlKWS9vAe9Sl9dqU+Rz8OqNTrjhCDUSoGEr
83ltoGkl2unBOWXT8JQVBBokGexUdKhDS/2gEF+WxpAUORE9qVFkARMZgH19bIYf
mNmJY+0t0YttiOvx7GXUnynJ6V3iEX96uMbxfBdIbHF/thzK0yCG/pD9DoLJPB7z
nRA+nQHYBFv8BCEBBACkrayUHg6fK2dMI0gZKdR0WMyxwXXKg+kc1tlsrxqfb/bH
UVtCy+VlpIS2SGcEAfXSIPmeesj5OA4p6e6egPeVFoy6qWOTLR0U9x4GqW/uUsEz
FAiB6IRK1h2HMLsp5wOV8RTf2ENDiObRh3zqEEJ7zANHQm4sDoaRUIHo5RsbuQAR
AQABAAP6A/gywYRC9nfBnu53ybF+L0rHrl47NGU/HISaS5oh8mdsw6u0nzTShAN1
9n3iy9AvSIAmfDk5+HXvM3fx2kzCCwJLv1Jxm7a9S92L6kzPQB/JdQ5c84cBespY
gzK8phGfCa2O0F+/mdBi5hKNuScM5g55UWpr17v0bbznEuhpF60CAMC4JKO32xUi
l2flVjhBR316i5IJmvUvN9Jw0sLtXuHS4tgaTWWPgThCEKc4iIy1Ngyy72mW4XTa
OzFNitInzw0CANrAbpzgKFawx+Kh2DrOUrj8IuXwf1NtajnfQfCk8nn4nzsXglq5
A/z9pSHuTGck5PFJT9PGiNH1ohxRciFfdF0CAJZLcDj09dG1UbgF4JelXhIJFYAE
VJQTKLLZq9H2W2ClA07gKkfvN1r++5lbaIB5fNJkY3BKNzuuEKAICx3hMqajrYi2
BBgBCAAgFiEEZw9JnIvRSktqHp5Tnk0uEMs+0z8FAlv8BCECGwwACgkQnk0uEMs+
0z8XgAP/c75yAyOCLLKUjD2Ov3Xwn/4YR8/mayZNFGkJ28FRWyxrw4zWVE3TCJVK
8AKUpa8QA9FL0maahWV4EQzcOvO4MKwohbUZ6/XLLd9NhMa9L5pZzsGUT2JQQN/i
yk+I22jpK0+ILJwqwB28EBzwkqRJLSgfPwbeRJ9Ni7D9qf8T1VU=
=vt8y
-----END PGP PRIVATE KEY BLOCK-----

19
src/main/updater/mocks/mock-pubkey-1.asc

@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mI0EW/wEBwEEANEtMWhFTchTPfsTvTDLaO/uODtAtKgecmCVxRjOr5EIGdAB/FA0
5xC0kMo4nKjW/fzX2jmnGevFYC60z8I2mNqfNxYzXIt8eOsqjwFseWYhtYGZXvDy
176ub9WXI2LfQPV4ggnLbvqcHJbiswtx8V7hPTnnFsS9E29DNH4p+sIHABEBAAG0
IkZvbyBCYXIgKE1vY2sgS2V5IDEpIDxmb29AYmFyLmNvbT6IzgQTAQgAOBYhBEWf
2gjO3oXgr4KNpb+W+UgE5ARuBQJb/AQHAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4B
AheAAAoJEL+W+UgE5ARubZcD/1dZ1J23CZaM5xVWRUCbS3QnfSBteS2HnsXdPwd7
AWZuXSc3tvbZDVm5R3PsQcQKej7rkT8rwSmXeIKzzQnw/LcxS0b943lV662Izs2+
BuroOteWXdIxZ+rWgspAqYC3Liiw15vZZwiEfXBw9CB4aI3Y3S4xmy9SqbvzfMaD
iZXDuI0EW/wEBwEEAOUUNwoEzjt/+YReQsPmQsKuBi9KxU1dvbcI0OG5xYdlp4Zg
G9GjhJwvvH1aHiNU3qyRLF2PjL7dmM6xFIppTF8sakW2H34oE2nY9IS0HYdFNaIa
g0vkG0isSrdRuVl38EClkvEEXnb/L7y/J3EDiBwW+4jWTd0SnS9C1IX+pzozABEB
AAGItgQYAQgAIBYhBEWf2gjO3oXgr4KNpb+W+UgE5ARuBQJb/AQHAhsMAAoJEL+W
+UgE5ARuR5EEAJ3UVZC+jWo1O40L0jAcsuEFBtPw4Qef9Mg/Z1pHR02Oy9x++mZf
iRcQdYVFbsYJZqhYw1i1YWTwasZcoONnnTAsLD2uP/El2N3Py8ONVlPdKLQw6sMj
1YktsE2sIE6HnZhMXz0pdx5SgFWo6BW1rgCtuJqA4rfl7OVaO+DofayX
=jimW
-----END PGP PUBLIC KEY BLOCK-----

19
src/main/updater/mocks/mock-pubkey-2.asc

@ -0,0 +1,19 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mI0EW/wEIQEEAMDRhoPhJrnSQP2Daj+3qqQwwFm2Zi3SSH8X6sAQEJhAvUfp2EAU
BS3uT0RgsDNTDTcHIAHZZHT3u17XESiz15RbP1ghQwnETpOTrqTKzTP5fW5lBxwb
LDGuD0SwxZA/ANI4mMUQqwxXzVftSMieHfz4PH+DLywtRQKVcOmjsdmJABEBAAG0
JkZpenogQmFyciAoTW9jayBLZXkgMikgPGZpenpAYmFyci5jb20+iM4EEwEIADgW
IQRnD0mci9FKS2oenlOeTS4Qyz7TPwUCW/wEIQIbAwULCQgHAgYVCgkICwIEFgID
AQIeAQIXgAAKCRCeTS4Qyz7TP8TJA/9mMMA5SlkvbwHvUpfXalPkc/DqjU644Qg1
EqBhK/N5baBpJdrpwTll0/CUFQQaJBnsVHSoQ0v9oBBflsaQFDkRPalRZAETGYB9
fWyGH5jZiWPtLdGLbYjr8exl1J8pyeld4hF/erjG8XwXSGxxf7YcytMghv6Q/Q6C
yTwe850QPriNBFv8BCEBBACkrayUHg6fK2dMI0gZKdR0WMyxwXXKg+kc1tlsrxqf
b/bHUVtCy+VlpIS2SGcEAfXSIPmeesj5OA4p6e6egPeVFoy6qWOTLR0U9x4GqW/u
UsEzFAiB6IRK1h2HMLsp5wOV8RTf2ENDiObRh3zqEEJ7zANHQm4sDoaRUIHo5Rsb
uQARAQABiLYEGAEIACAWIQRnD0mci9FKS2oenlOeTS4Qyz7TPwUCW/wEIQIbDAAK
CRCeTS4Qyz7TPxeAA/9zvnIDI4IsspSMPY6/dfCf/hhHz+ZrJk0UaQnbwVFbLGvD
jNZUTdMIlUrwApSlrxAD0UvSZpqFZXgRDNw687gwrCiFtRnr9cst302Exr0vmlnO
wZRPYlBA3+LKT4jbaOkrT4gsnCrAHbwQHPCSpEktKB8/Bt5En02LsP2p/xPVVQ==
=Bk7g
-----END PGP PUBLIC KEY BLOCK-----

52
src/main/updater/mocks/pubkey-1.asc

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
sfB6/eqK4Q==
=UKRx
-----END PGP PUBLIC KEY BLOCK-----

52
src/main/updater/mocks/pubkey-2.asc

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
sEQGv7WFrXcyYM38
=/9tA
-----END PGP PUBLIC KEY BLOCK-----

1
src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage

@ -0,0 +1 @@
sigko-shasumok

1
src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum

@ -0,0 +1 @@
26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage

16
src/main/updater/mocks/sigko-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEhhzv+bPVtMW9pE7JhKxNYN+ul88FAlvKAJIACgkQhKxNYN+u
l8/FJQ/+PvYCbEZB77BGBHPvoEjkTbp4lP9RYzIS1Ial0HUZ6CDsMOfFEVsfM7VQ
MTluvUERshVc5OX2Z4Y+G/eMYHInwZX+ytjWQ7/rXa4M/3na97ND/JwQjNmiIzjT
Fxaiis0BqwulOff9bvgu0AKrA6Bq/DiBKV2EuWwNKVADuUhoAru53YWtAg0mFU87
XRSR6bww1/fDe9pD/PzWNJxonMHjZDz3KYQOh5RXqNE9sgI51hMVpqNW5O1H/N5P
GvqNDB0piyH85CbiZgvYhDVzVBhpuu1+gQLYMYRAsU8Grp5jJmnbTX81kJsqzyrY
1MNwH4v/nr1KIA/H/SwCXLgDX013VPc7U0rD74dZI8IJn2le/72U5jZknPM0B7vl
KBWgEOq3XYSqW5jlY4N4wdHmNvMiTureiLfZmOjV2XLk8aLX+/LpABHLBduGM3hZ
wsj7+av7jFce4w6bBNoew+P3OMS6aHyORTUkMKvGSACI4PchGHSgG6rmGM183o64
opexXsnit5vIYQKbJATp2nuUmW9TD2kK6mDjk2Hio4wAR6KvawUf4prIkF1rR/M5
4DrJ3Oq9ntvmApyE8o5t/uOFE/HSuS/+ZGn1nieSPJTME6WtSYWV9gxfLOgErpXD
GoibMFoInfjh/rPE5JwoCuIaBGYOuFS9ytwlYAPcA7sJQlb9vLE=
=KWhh
-----END PGP SIGNATURE-----

3
src/main/updater/mocks/sigko-shasumok/update-info.json

@ -0,0 +1,3 @@
{
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage"
}

1
src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage

@ -0,0 +1 @@
sigok-shasumok

1
src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum

@ -0,0 +1 @@
this_is_ko_26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage

16
src/main/updater/mocks/sigok-shasumko/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEESKQ3KFYSBLGhLWtAHS/CMF4ss5kFAlvKBL4ACgkQHS/CMF4s
s5lbKxAAndUSWP3tL+aBl/Jn1Fjr48bPlRHFpqUY1bW59uAEB0C1xkHCvExkH7Wh
oc1nT3DsE+khwfhPeruKiNBcgtLtPWBW0PaY7N1tcafzYVldBZYSRfZotv1wEuqF
vt4kgqzCTPLOLfaUxTP7esDv6oIdHY7bah/0QsgvPiOxtT+/Kb/6OIXGT6IyWJxX
6Z/oc2lzkxmIyALFp53YEejUeCayiATVcoR15MCOK2zofU9RBk0BI2R50cNlefpX
WttXOfFP5l2EecZA0S3vA8am66+nG3jKHA8cLIqc//X1pgFCNbZXxCt2kTiE4ri6
8XW0ne9tGvHk+fG2/4ZhNK2dqzwSD5Yitd2g5HU3ZOUxVdmH5K2XyFF5Zhixca+c
ivFlH00LEagF8hhxkf2tZWSG8fLvffNmzPdkQ1HySq4FpT/K33twb+MYHAWtwEP6
/Epyiq7S15aB2SRkdOnBv1VQWf0wGZO/ABKutgF8xrOh13prLLSmXFenIdXGsdnP
2Wlawn6GyvSOcK+s6Z6AuETXXVdGnUQDuwjUyqQXUWp4Fm0xInTTPcgruh7Ip3Ra
vBeQcYr4i/aB37X6fUVogzq4Sy2DvDUnG7uMmaVyDxK6RhQ9/M3Jql/6ffCJPXm5
J7AqoM/QCHfVkXhvgKKwq8hxb6JLSykjB9h//AonCVRcKW/dqY8=
=kaOO
-----END PGP SIGNATURE-----

3
src/main/updater/mocks/sigok-shasumko/update-info.json

@ -0,0 +1,3 @@
{
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage"
}

1
src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage

@ -0,0 +1 @@
sigok-shasumok

1
src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum

@ -0,0 +1 @@
26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage

16
src/main/updater/mocks/sigok-shasumok/ledger-live-desktop-1.2.1-linux-x86_64.AppImage.sha512sum.sig

@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEESKQ3KFYSBLGhLWtAHS/CMF4ss5kFAlvJ+ysACgkQHS/CMF4s
s5mepQ/8DkHSiLEdpMx1GHRX7ElRbhefD7ZAbBCaf/1igpx9K/+SrKvVkgywX2us
D+MU6vmxcFLuMMb5NWpY2s7/0iC+fj36x33fCSvRkmmxxXWf+SzyFozX8sAc/vOo
2x1KTR5jvxspZS0/ERX2KH/88hIWH9kzLsrk/qvppWXpJvEVS5hEHKijLST1aw+c
4L01n9dBwTAsMLo8xm7ybcU86yKUsvbd96XJHuQoqJeg4TOqveUuEmh1J/9jaXnO
3ZcPOtOP42jKuIOFglGMTgtFwR/WN7kxb5eS7wDodvTJE0YzpxeugW4WEfJ2nspZ
3ZCW3G4DD38/uN5MGnPEM77pqKqDZJY7wPHTO2PbXa6w5+WkW9cvE38fBikdQsnX
nKIuvsqQRBBsIOmVe4m+VJ3wrBPpk7IrYSp54+M8XWf8EkkyZYxtMhE+1bqPhRqG
aTIw6qJXJ+yDowe8cFaEybs4xcUl2t4QWHIXM5Y09ZFcwhD5UQwtXz9x6jJogR2j
W8MLeNnAUhRxFY/hySq+wJasKMDKbWXGPk3TmMNn8P+6g6HEllM4KucJty2OH50t
nJbYt0da3Br2W8os1RPA71MmfW6nGhzcFhX2ShKLicyw46Tyc0GGaO29oDwgQkRc
wcbFJmZVMj9ZuZArmfhFL53ZQj9kbAF2qWPNt+L369jSmw5tAko=
=z2+Z
-----END PGP SIGNATURE-----

3
src/main/updater/mocks/sigok-shasumok/update-info.json

@ -0,0 +1,3 @@
{
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage"
}

40
src/main/updater/pgpHelper.js

@ -0,0 +1,40 @@
// @flow
import * as openpgp from 'openpgp'
export async function getFingerprint(pubKey: string) {
const { keys } = await openpgp.key.readArmored(pubKey)
if (!keys.length) {
throw new Error('No key found in pubKey')
}
return keys[0].getFingerprint()
}
export async function verify(msgContent: string, sigContent: string, pubKeyContent: string) {
const signature = await openpgp.signature.readArmored(sigContent)
const message = openpgp.message.fromText(msgContent)
const { keys: publicKeys } = await openpgp.key.readArmored(pubKeyContent)
const pgpOpts = { message, publicKeys, signature }
const verified = await openpgp.verify(pgpOpts)
if (verified.signatures.length === 0) {
throw new Error('No signature found')
}
if (!verified.signatures.every(sig => sig.valid)) {
throw new Error('Signature check failed')
}
}
export async function sign(msgContent: string, privKeyContent: string) {
const privKey = (await openpgp.key.readArmored(privKeyContent)).keys[0]
const options = {
message: openpgp.cleartext.fromText(msgContent),
privateKeys: [privKey],
detached: true,
}
const signed = await openpgp.sign(options)
return signed.signature
}

114
src/main/updater/updater.spec.js

@ -0,0 +1,114 @@
import path from 'path'
import crypto from 'crypto'
import { fsReadFile } from 'helpers/fs'
import { UpdateIncorrectHash, UpdateIncorrectSig } from './errors'
import createMockAppUpdater from './createMockAppUpdater'
import { sha512sumPath, readUpdateInfos } from './createElectronAppUpdater'
import * as pgpHelper from './pgpHelper'
const base = path.resolve(__dirname, 'mocks')
describe('AppUpdater', () => {
describe('simple cases', () => {
test('should not throw if correct hash & correct sig', async () => {
await verifyMockFolder('sigok-shasumok', 'pubkey-1.asc')
})
test('should throw if correct hash but incorrect pubkey', async () => {
const p = verifyMockFolder('sigok-shasumok', 'pubkey-2.asc')
await expectFail(p, UpdateIncorrectSig)
})
test('should throw if incorrect hash but correct sig', async () => {
const p = verifyMockFolder('sigok-shasumko', 'pubkey-1.asc')
await expectFail(p, UpdateIncorrectHash)
})
test('should throw if correct hash but incorrect sig', async () => {
const p = verifyMockFolder('sigko-shasumok', 'pubkey-1.asc')
await expectFail(p, UpdateIncorrectSig)
})
})
describe('key invalidation', () => {
test('should verify successfully by using getNextKey()', async () => {
const filename = 'foo.exe'
const mockPrivKey1 = await fsReadFile(path.resolve(base, 'mock-privkey-1.asc'), 'ascii')
const mockPubKey1 = await fsReadFile(path.resolve(base, 'mock-pubkey-1.asc'), 'ascii')
const mockPrivKey2 = await fsReadFile(path.resolve(base, 'mock-privkey-2.asc'), 'ascii')
const mockPubKey2 = await fsReadFile(path.resolve(base, 'mock-pubkey-2.asc'), 'ascii')
// This is the desired scenario:
//
// Let's simulate a release.
//
// 1) create binary (lol) and its hash. this is basically the app update
const update = '0010011-content-of-the-app-01110001010111'
// 2) create hash of update
const hash = sha512sum(update)
const hashFile = `${hash} ${filename}` // yeah, the hash file contains also file name
// 3) sign the hash with the *KEY 2* (the app will use the *KEY 1* first)
const signature = await pgpHelper.sign(hashFile, mockPrivKey2)
// 4) sign *KEY 2* with *KEY 1* and get the *KEY 1* fingerprint, ye.
const mockPubKey2Signature = await pgpHelper.sign(mockPubKey2, mockPrivKey1)
const mockPubKey1Fingerprint = await pgpHelper.getFingerprint(mockPubKey1)
// 5) app updater, with all thoses infos
const updater = await createMockAppUpdater({
filename,
computedHash: hash,
hashFile,
signature,
pubKey: mockPubKey1,
pubKeys: [
{
fingerprint: mockPubKey1Fingerprint,
content: mockPubKey2,
signature: mockPubKey2Signature,
},
],
})
await updater.verify()
})
})
})
async function expectFail(promise, errType) {
let err
try {
await promise
} catch (e) {
err = e
}
expect(err).toBeDefined()
expect(err).toBeInstanceOf(errType)
}
async function verifyMockFolder(folderName, pubKeyName, pubKeys = []) {
const mock = path.resolve(base, folderName)
const { fileName: filename } = await readUpdateInfos(mock)
const computedHash = await sha512sumPath(path.resolve(mock, filename))
const hashFile = await fsReadFile(path.resolve(mock, `${filename}.sha512sum`), 'ascii')
const signature = await fsReadFile(path.resolve(mock, `${filename}.sha512sum.sig`), 'ascii')
const pubKey = await fsReadFile(path.resolve(base, pubKeyName))
const updater = await createMockAppUpdater({
filename,
computedHash,
hashFile,
signature,
pubKey,
pubKeys,
})
await updater.verify()
}
export function sha512sum(content) {
const sum = crypto.createHash('sha512')
sum.update(content)
return sum.digest('hex')
}

4
src/reducers/index.js

@ -13,7 +13,6 @@ import currenciesStatus from './currenciesStatus'
import devices from './devices'
import modals from './modals'
import settings from './settings'
import update from './update'
import onboarding from './onboarding'
import bridgeSync from './bridgeSync'
@ -22,7 +21,6 @@ import type { ApplicationState } from './application'
import type { DevicesState } from './devices'
import type { ModalsState } from './modals'
import type { SettingsState } from './settings'
import type { UpdateState } from './update'
import type { OnboardingState } from './onboarding'
import type { BridgeSyncState } from './bridgeSync'
import type { CurrenciesStatusState } from './currenciesStatus'
@ -36,7 +34,6 @@ export type State = {
modals: ModalsState,
router: LocationShape,
settings: SettingsState,
update: UpdateState,
onboarding: OnboardingState,
bridgeSync: BridgeSyncState,
}
@ -50,7 +47,6 @@ export default combineReducers({
modals,
router,
settings,
update,
onboarding,
bridgeSync,
})

48
src/reducers/update.js

@ -1,48 +0,0 @@
// @flow
import { handleActions, createAction } from 'redux-actions'
export type UpdateStatus =
| 'idle'
| 'checking'
| 'available'
| 'progress'
| 'unavailable'
| 'error'
| 'downloaded'
export type UpdateState = {
status: UpdateStatus,
data?: Object,
}
const state: UpdateState = {
status: 'idle',
data: {},
}
const handlers = {
UPDATE_SET_STATUS: (state: UpdateState, { payload }: { payload: UpdateState }): UpdateState =>
payload,
}
// Actions
export const setUpdateStatus = createAction(
'UPDATE_SET_STATUS',
(status: UpdateStatus, data?: Object): UpdateState => ({ status, data }),
)
// Selectors
export function getUpdateStatus(state: { update: UpdateState }): UpdateStatus {
return state.update.status
}
export function getUpdateData(state: { update: UpdateState }): Object {
return state.update.data || {}
}
// Default export
export default handleActions(handlers, state)

25
src/renderer/events.js

@ -21,7 +21,6 @@ import { onSetDeviceBusy } from 'components/DeviceBusyIndicator'
import { onSetLibcoreBusy } from 'components/LibcoreBusyIndicator'
import { lock } from 'reducers/application'
import { setUpdateStatus } from 'reducers/update'
import { addDevice, removeDevice, resetDevices } from 'actions/devices'
import listenDevices from 'commands/listenDevices'
@ -32,11 +31,6 @@ const d = {
update: debug('lwd:update'),
}
type MsgPayload = {
type: string,
data: any,
}
// TODO port remaining to command pattern
export function sendEvent(channel: string, msgType: string, data: any) {
ipcRenderer.send(channel, {
@ -109,25 +103,6 @@ export default ({ store }: { store: Object }) => {
onSetDeviceBusy(busy)
})
}
if (__PROD__) {
// TODO move this to "command" pattern
const updaterHandlers = {
checking: () => store.dispatch(setUpdateStatus('checking')),
updateAvailable: info => store.dispatch(setUpdateStatus('available', info)),
updateNotAvailable: () => store.dispatch(setUpdateStatus('unavailable')),
error: err => store.dispatch(setUpdateStatus('error', err)),
downloadProgress: progress => store.dispatch(setUpdateStatus('progress', progress)),
downloaded: () => store.dispatch(setUpdateStatus('downloaded')),
}
ipcRenderer.on('updater', (event: any, payload: MsgPayload) => {
const { type, data } = payload
updaterHandlers[type](data)
})
// Start check of eventual updates
checkUpdates()
}
}
if (module.hot) {

12
static/i18n/en/app.json

@ -111,6 +111,9 @@
}
}
},
"banners": {
"promoteMobile": "Enjoy the Ledger Live experience, now available on mobile with the Ledger Nano X"
},
"dashboard": {
"title": "Portfolio",
"emptyAccountTile": {
@ -494,8 +497,13 @@
}
},
"update": {
"newVersionReady": "A new update is available",
"relaunch": "Update now"
"downloadInProgress": "Downloading update...",
"downloadProgress": "{{progress}}% completed",
"checking": "Checking update...",
"checkSuccess": "Update ready to install",
"quitAndInstall": "Install now",
"error": "Error during update. Please download again",
"reDownload": "Download again"
},
"crash": {
"oops": "Oops, something went wrong",

143
yarn.lock

@ -1756,6 +1756,13 @@
rxjs "^6.3.3"
rxjs-compat "^6.3.3"
"@mattiasbuelens/web-streams-polyfill@0.1.0-alpha.4":
version "0.1.0-alpha.4"
resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.1.0-alpha.4.tgz#fde9688fac83ff40e3be1a01973c867e12a74492"
integrity sha512-WAsiWLWc7ZNS1b0qFAoKSFLeqXesPa60YelVE3pPKc6pZ4iuSW9l6DBxY4hMPQj1dQCBDrUHJj/NDSjE85bTRQ==
dependencies:
"@types/whatwg-streams" "0.0.5"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -2077,6 +2084,11 @@
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.13.6.tgz#128d1685a7c34d31ed17010fc87d6a12c1de6976"
integrity sha512-5Th3OsZ4gTRdr9Mho83BQ23cex4sRhOR4XTG+m+cJc0FhtUBK9Vn62hBJ+pnQYnSxoPOsKoAPOx6FcphxBC8ng==
"@types/whatwg-streams@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@types/whatwg-streams/-/whatwg-streams-0.0.5.tgz#66c3852b8e33f2a2ca225a6d8fd8096e34d17fe6"
integrity sha512-y1UgRuGO64x/v+UIerA2AMquW/qxaIUD95rbf8FYxtVG//D3381+JexnZfcEiZSqXErdxdPmXpz8srY7gs9Grw==
"@types/ws@^3.2.0":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.2.1.tgz#b0c1579e58e686f83ce0a97bb9463d29705827fb"
@ -2316,6 +2328,13 @@ acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.6.2:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
integrity sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==
address-rfc2822@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/address-rfc2822/-/address-rfc2822-2.0.4.tgz#2dbd3b8d6c2de1e957c1a8549dc012d40bbc3431"
integrity sha1-Lb07jWwt4elXwahUncAS1Au8NDE=
dependencies:
email-addresses "^3.0.0"
address@1.0.3, address@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9"
@ -2691,6 +2710,10 @@ asap@~2.0.3:
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asmcrypto.js@github:openpgpjs/asmcrypto":
version "2.3.0"
resolved "https://codeload.github.com/openpgpjs/asmcrypto/tar.gz/6e4e407b9b8ae317925a9e677cc7b4de3e447e83"
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -2700,6 +2723,15 @@ asn1.js@^4.0.0:
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
asn1.js@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.0.1.tgz#7668b56416953f0ce3421adbb3893ace59c96f59"
integrity sha512-aO8EaEgbgqq77IEw+1jfx5c9zTbzvkfuRBuZsSsPnTHMkmd5AI4J6OtITLZFa381jReeaQL67J0GBTUu0+ZTVw==
dependencies:
bn.js "^4.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
@ -4215,7 +4247,7 @@ bn.js@^3.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-3.3.0.tgz#1138e577889fdc97bbdab51844f2190dfc0ae3d7"
integrity sha1-ETjld4if3Je72rUYRPIZDfwK49c=
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.4.0:
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.0, bn.js@^4.11.3, bn.js@^4.11.8, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
@ -4516,6 +4548,14 @@ buffer@^5.0.3:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.0.8:
version "5.2.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
buffer@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.0.tgz#53cf98241100099e9eeae20ee6d51d21b16e541e"
@ -6914,6 +6954,23 @@ elliptic@^6.0.0, elliptic@^6.2.3:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
"elliptic@github:openpgpjs/elliptic":
version "6.4.0"
resolved "https://codeload.github.com/openpgpjs/elliptic/tar.gz/e187e706e11fa51bcd20e46e5119054be4e2a4a6"
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
email-addresses@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.0.2.tgz#a31280d19baf86669840a0aa45be1d7f6e7df315"
integrity sha512-IMn9dnwLMsgZjdUHswB/UZ0S8LQ/u+2/qjnHJ9tCtp3QHZsIYwJCiJOo2FT0i3CwwK/dtSODYtxuvzV4D9MY5g==
emoji-regex@^6.1.0:
version "6.5.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
@ -8690,6 +8747,14 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
hash.js@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812"
integrity sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
@ -11115,7 +11180,7 @@ mini-css-extract-plugin@^0.4.0:
loader-utils "^1.1.0"
webpack-sources "^1.1.0"
minimalistic-assert@^1.0.0:
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
@ -11372,7 +11437,7 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.1.1:
node-fetch@^2.1.1, node-fetch@^2.1.2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5"
integrity sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==
@ -11448,6 +11513,13 @@ node-loader@^0.6.0:
resolved "https://registry.yarnpkg.com/node-loader/-/node-loader-0.6.0.tgz#c797ef51095ed5859902b157f6384f6361e05ae8"
integrity sha1-x5fvUQle1YWZArFX9jhPY2HgWug=
node-localstorage@~1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-1.3.1.tgz#3177ef42837f398aee5dd75e319b281e40704243"
integrity sha512-NMWCSWWc6JbHT5PyWlNT2i8r7PgGYXVntmKawY83k/M0UJScZ5jirb61TLnqKwd815DfBQu+lR3sRw08SPzIaQ==
dependencies:
write-file-atomic "^1.1.4"
node-modules-regexp@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40"
@ -11808,6 +11880,25 @@ opener@^1.4.3:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.4.3.tgz#5c6da2c5d7e5831e8ffa3964950f8d6674ac90b8"
integrity sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=
openpgp@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-4.2.1.tgz#5d693ca9d90f52f0826447a37596b6a9a5a725da"
integrity sha512-spO7D5LVS8H8ktpX55XDNzvIktMQA7CHZVCt3i4REeP3rbFlzvRbg0cR699yC/hwH0dx09/z+6C+fH4lmbyucw==
dependencies:
"@mattiasbuelens/web-streams-polyfill" "0.1.0-alpha.4"
address-rfc2822 "^2.0.3"
asmcrypto.js "github:openpgpjs/asmcrypto"
asn1.js "^5.0.0"
bn.js "^4.11.8"
buffer "^5.0.8"
elliptic "github:openpgpjs/elliptic"
hash.js "^1.1.3"
node-fetch "^2.1.2"
node-localstorage "~1.3.0"
pako "^1.0.6"
seek-bzip "github:openpgpjs/seek-bzip"
web-stream-tools "github:openpgpjs/web-stream-tools"
opn@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
@ -12001,7 +12092,7 @@ package-json@^4.0.0:
registry-url "^3.0.3"
semver "^5.1.0"
pako@~1.0.5:
pako@^1.0.6, pako@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==
@ -13123,15 +13214,15 @@ react-docgen@^3.0.0-beta11:
node-dir "^0.1.10"
recast "^0.12.6"
react-dom@^16.4.1:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
integrity sha512-1Gin+wghF/7gl4Cqcvr1DxFX2Osz7ugxSwl6gBqCMpdrxHjIFUS7GYxrFftZ9Ln44FHw0JxCFD9YtZsrbR5/4A==
react-dom@^16.6.3:
version "16.6.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0"
integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==
dependencies:
fbjs "^0.8.16"
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.0"
prop-types "^15.6.2"
scheduler "^0.11.2"
react-error-overlay@^4.0.0:
version "4.0.0"
@ -13404,7 +13495,7 @@ react-treebeard@^2.1.0:
shallowequal "^0.2.2"
velocity-react "^1.3.1"
react@*, react@^16.2.0, react@^16.6.1:
react@*, react@^16.2.0:
version "16.6.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.6.1.tgz#ee2aef4f0a09e494594882029821049772f915fe"
integrity sha512-OtawJThYlvRgm9BXK+xTL7BIlDx8vv21j+fbQDjRRUyok6y7NyjlweGorielTahLZHYIdKUoK2Dp9ByVWuMqxw==
@ -13414,6 +13505,16 @@ react@*, react@^16.2.0, react@^16.6.1:
prop-types "^15.6.2"
scheduler "^0.11.0"
react@^16.6.3:
version "16.6.3"
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.11.2"
reactcss@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
@ -14331,6 +14432,14 @@ scheduler@^0.11.0:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.11.2:
version "0.11.3"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b"
integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf"
@ -14380,6 +14489,12 @@ secp256k1@^3.0.1:
nan "^2.2.1"
safe-buffer "^5.1.0"
"seek-bzip@github:openpgpjs/seek-bzip":
version "1.0.5-git"
resolved "https://codeload.github.com/openpgpjs/seek-bzip/tar.gz/3aca608ffedc055a1da1d898ecb244804ef32209"
dependencies:
commander "~2.8.1"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@ -16323,6 +16438,10 @@ wdio-dot-reporter@~0.0.8:
resolved "https://registry.yarnpkg.com/wdio-dot-reporter/-/wdio-dot-reporter-0.0.10.tgz#facfb7c9c5984149951f59cbc3cd0752101cf0e0"
integrity sha512-A0TCk2JdZEn3M1DSG9YYbNRcGdx/YRw19lTiRpgwzH4qqWkO/oRDZRmi3Snn4L2j54KKTfPalBhlOtc8fojVgg==
"web-stream-tools@github:openpgpjs/web-stream-tools":
version "0.0.1"
resolved "https://codeload.github.com/openpgpjs/web-stream-tools/tar.gz/9ab800d46add161db496506d67338202ad0114ce"
webdriverio@^4.8.0:
version "4.13.1"
resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.13.1.tgz#624ef4ca569f3c9a5e8e9b11302b4431eda1fb8a"
@ -16752,7 +16871,7 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
write-file-atomic@^1.2.0:
write-file-atomic@^1.1.4, write-file-atomic@^1.2.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
integrity sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=

Loading…
Cancel
Save