Browse Source
LL-931 Add a banner to promote the mobile version of the Live + Banner refactoringdevelop
Gaëtan Renaudeau
6 years ago
committed by
GitHub
62 changed files with 1689 additions and 404 deletions
@ -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 |
@ -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 |
@ -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 |
@ -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; |
||||
|
} |
||||
|
` |
@ -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) |
|
@ -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) |
|
@ -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> |
|
||||
) |
|
||||
} |
|
||||
} |
|
@ -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) |
@ -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) |
@ -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) |
@ -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 |
@ -1,17 +1,56 @@ |
|||||
// @flow
|
// @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_RELEASES_NOTES } from 'config/constants' |
||||
import Modal from 'components/base/Modal' |
import Modal from 'components/base/Modal' |
||||
|
|
||||
import ReleaseNotesBody from './ReleaseNotesBody' |
import ReleaseNotesBody from './ReleaseNotesBody' |
||||
|
|
||||
const ReleaseNotesModal = () => ( |
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 |
<Modal |
||||
name={MODAL_RELEASES_NOTES} |
name={MODAL_RELEASES_NOTES} |
||||
centered |
centered |
||||
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} |
render={({ data, onClose }) => <ReleaseNotesBody version={data} onClose={onClose} />} |
||||
/> |
/> |
||||
) |
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
export default ReleaseNotesModal |
export default connect( |
||||
|
mapStateToProps, |
||||
|
mapDispatchToProps, |
||||
|
)(ReleaseNotesModal) |
||||
|
@ -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> |
||||
|
) |
@ -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,2 +1,3 @@ |
|||||
export Blue from './Blue' |
export Blue from './Blue' |
||||
export NanoS from './NanoS' |
export NanoS from './NanoS' |
||||
|
export NanoX from './NanoX' |
||||
|
@ -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) |
|
||||
}) |
|
||||
} |
|
@ -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] |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
} |
@ -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) |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
}, |
||||
|
}) |
@ -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') |
@ -0,0 +1,111 @@ |
|||||
|
// TODO: it's actually my key. replace with qorum key.
|
||||
|
|
||||
|
export default `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
|
||||
|
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 |
||||
|
=97OE |
||||
|
-----END PGP PUBLIC KEY BLOCK-----` |
@ -0,0 +1,33 @@ |
|||||
|
-----BEGIN PGP PRIVATE KEY BLOCK----- |
||||
|
|
||||
|
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 |
||||
|
=IP04 |
||||
|
-----END PGP PRIVATE KEY BLOCK----- |
@ -0,0 +1,33 @@ |
|||||
|
-----BEGIN PGP PRIVATE KEY BLOCK----- |
||||
|
|
||||
|
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 |
||||
|
=vt8y |
||||
|
-----END PGP PRIVATE KEY BLOCK----- |
@ -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----- |
@ -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----- |
@ -0,0 +1,52 @@ |
|||||
|
-----BEGIN PGP PUBLIC KEY BLOCK----- |
||||
|
|
||||
|
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 |
||||
|
sfB6/eqK4Q== |
||||
|
=UKRx |
||||
|
-----END PGP PUBLIC KEY BLOCK----- |
@ -0,0 +1,52 @@ |
|||||
|
-----BEGIN PGP PUBLIC KEY BLOCK----- |
||||
|
|
||||
|
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 |
||||
|
sEQGv7WFrXcyYM38 |
||||
|
=/9tA |
||||
|
-----END PGP PUBLIC KEY BLOCK----- |
@ -0,0 +1 @@ |
|||||
|
sigko-shasumok |
@ -0,0 +1 @@ |
|||||
|
26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage |
@ -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----- |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
sigok-shasumok |
@ -0,0 +1 @@ |
|||||
|
this_is_ko_26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage |
@ -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----- |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
sigok-shasumok |
@ -0,0 +1 @@ |
|||||
|
26ca4f4aa377a8cbe434463debe94d9ec923b930a3053d4fe7129440b48667ac66db03e0f6c417671e16ea9445148d2124df6d34184514f05b26aa14a0790f4b ledger-live-desktop-1.2.1-linux-x86_64.AppImage |
@ -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----- |
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"fileName": "ledger-live-desktop-1.2.1-linux-x86_64.AppImage" |
||||
|
} |
@ -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 |
||||
|
} |
@ -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') |
||||
|
} |
@ -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) |
|
Loading…
Reference in new issue