17 changed files with 432 additions and 260 deletions
@ -0,0 +1,51 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import { createCommand, Command } from 'helpers/ipc' |
||||
|
import { Observable } from 'rxjs' |
||||
|
|
||||
|
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 info => { |
||||
|
try { |
||||
|
const appUpdater = await createElectronAppUpdater({ |
||||
|
feedURL: process.env.LL_UPDATE_FEED || 'https://insert.feed.here', |
||||
|
updateVersion: info.version, |
||||
|
}) |
||||
|
await appUpdater.verify() |
||||
|
sendStatus('check-success') |
||||
|
} catch (err) { |
||||
|
// todo delete update file
|
||||
|
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 |
@ -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,75 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React, { PureComponent } from 'react' |
||||
|
import styled from 'styled-components' |
||||
|
|
||||
|
import { radii } from 'styles/theme' |
||||
|
|
||||
|
import TranslatedError from 'components/TranslatedError' |
||||
|
import Box from 'components/base/Box' |
||||
|
|
||||
|
import { withUpdaterContext } from './UpdaterContext' |
||||
|
import type { UpdaterContextType } from './UpdaterContext' |
||||
|
|
||||
|
type Props = { |
||||
|
context: UpdaterContextType, |
||||
|
} |
||||
|
|
||||
|
export const VISIBLE_STATUS = ['download-progress', 'checking', 'check-success', 'error'] |
||||
|
|
||||
|
class UpdaterTopBanner extends PureComponent<Props> { |
||||
|
render() { |
||||
|
const { context } = this.props |
||||
|
const { status, quitAndInstall, downloadProgress, error } = context |
||||
|
|
||||
|
if (!VISIBLE_STATUS.includes(status)) return null |
||||
|
|
||||
|
return ( |
||||
|
<Container status={status}> |
||||
|
{status === 'download-progress' && `Downloading update... ${Math.round(downloadProgress)}%`} |
||||
|
{status === 'checking' && `Verifying update...`} |
||||
|
{status === 'error' && |
||||
|
error && ( |
||||
|
<div> |
||||
|
{'Error during update. Please download again.'} |
||||
|
<ErrorContainer> |
||||
|
<TranslatedError error={error} /> |
||||
|
</ErrorContainer> |
||||
|
</div> |
||||
|
)} |
||||
|
{status === 'check-success' && ( |
||||
|
<div> |
||||
|
{'Update ready to install. '} |
||||
|
<DownloadLink onClick={quitAndInstall}>{'install now'}</DownloadLink> |
||||
|
</div> |
||||
|
)} |
||||
|
</Container> |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const Container = styled(Box).attrs({ |
||||
|
py: '8px', |
||||
|
px: 3, |
||||
|
bg: p => (p.status === 'error' ? 'alertRed' : 'wallet'), |
||||
|
color: 'white', |
||||
|
mt: -20, |
||||
|
mb: 20, |
||||
|
fontSize: 4, |
||||
|
})` |
||||
|
border-radius: ${radii[1]}px; |
||||
|
` |
||||
|
|
||||
|
const DownloadLink = styled.span` |
||||
|
color: white; |
||||
|
text-decoration: underline; |
||||
|
cursor: pointer; |
||||
|
` |
||||
|
|
||||
|
const ErrorContainer = styled.div` |
||||
|
margin-top: 10px; |
||||
|
font-family: monospace; |
||||
|
font-size: 10px; |
||||
|
` |
||||
|
|
||||
|
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,88 @@ |
|||||
|
// @flow
|
||||
|
|
||||
|
import React from 'react' |
||||
|
import styled, { keyframes } 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 rotate = keyframes` |
||||
|
0% { |
||||
|
transform: rotate(0deg); |
||||
|
} |
||||
|
100% { |
||||
|
transform: rotate(360deg); |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
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%; |
||||
|
` |
||||
|
|
||||
|
const Spinner = styled.div` |
||||
|
opacity: 0.5; |
||||
|
position: absolute; |
||||
|
top: -3px; |
||||
|
left: -3px; |
||||
|
animation: ${rotate} 1.5s linear infinite; |
||||
|
width: 14px; |
||||
|
height: 14px; |
||||
|
|
||||
|
&:before { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
width: 4px; |
||||
|
height: 4px; |
||||
|
background-color: ${colors.wallet}; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
&:after { |
||||
|
content: ''; |
||||
|
position: absolute; |
||||
|
width: 4px; |
||||
|
height: 4px; |
||||
|
background-color: ${colors.wallet}; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
function UpdateDot(props: Props) { |
||||
|
const { context } = props |
||||
|
const { status } = context |
||||
|
if (!VISIBLE_STATUS.includes(status)) return null |
||||
|
const showSpinner = status === 'download-progress' || status === 'checking' |
||||
|
return ( |
||||
|
<div style={styles.container}> |
||||
|
{showSpinner && <Spinner />} |
||||
|
<Dot status={status} /> |
||||
|
</div> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const styles = { |
||||
|
container: { |
||||
|
position: 'relative', |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
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,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