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