diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/packages/desktop/src/assets/images/icon-alert.svg | 4 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/app.js | 18 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/Settings.js | 39 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/SettingsStyles.js | 17 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/containers/SettingsPage.js | 4 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 43 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/store.js | 8 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/version/actions.js | 33 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/version/reducers.js | 53 |
10 files changed, 206 insertions, 14 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd3f7dddf..8b1c42239a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Line wrap the file at 100 chars. Th ### Added - Add option to enable or disable IPv6 on the tunnel interface. - Log panics in the daemon to the log file. +- Warn in the Settings screen if a new version is available. ### Changed - The "Buy more credit" button is changed to open a dedicated account login page instead of one diff --git a/gui/packages/desktop/src/assets/images/icon-alert.svg b/gui/packages/desktop/src/assets/images/icon-alert.svg new file mode 100644 index 0000000000..c11507a4a5 --- /dev/null +++ b/gui/packages/desktop/src/assets/images/icon-alert.svg @@ -0,0 +1,4 @@ +<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>alert</title> + <path d="m12 24c-6.627417 0-12-5.372583-12-12s5.372583-12 12-12 12 5.372583 12 12-5.372583 12-12 12zm0-19.5c-.8284271 0-1.5.67157288-1.5 1.5v7.5c0 .8284271.6715729 1.5 1.5 1.5s1.5-.6715729 1.5-1.5v-7.5c0-.82842712-.6715729-1.5-1.5-1.5zm0 12c-.8284271 0-1.5.6715729-1.5 1.5s.6715729 1.5 1.5 1.5 1.5-.6715729 1.5-1.5-.6715729-1.5-1.5-1.5z" fill="currentColor" /> +</svg> diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index 82a717941d..310b1f84ce 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -10,7 +10,7 @@ import { replace as replaceHistory, } from 'connected-react-router'; import { createMemoryHistory } from 'history'; -import { webFrame, ipcRenderer } from 'electron'; +import { remote, webFrame, ipcRenderer } from 'electron'; import makeRoutes from './routes'; import ReconnectionBackoff from './lib/reconnection-backoff'; @@ -23,6 +23,7 @@ import configureStore from './redux/store'; import accountActions from './redux/account/actions'; import connectionActions from './redux/connection/actions'; import settingsActions from './redux/settings/actions'; +import versionActions from './redux/version/actions'; import daemonActions from './redux/daemon/actions'; import type { RpcCredentials } from '../common/types'; @@ -57,6 +58,7 @@ export default class AppRenderer { account: bindActionCreators(accountActions, dispatch), connection: bindActionCreators(connectionActions, dispatch), settings: bindActionCreators(settingsActions, dispatch), + version: bindActionCreators(versionActions, dispatch), daemon: bindActionCreators(daemonActions, dispatch), history: bindActionCreators( { @@ -405,6 +407,19 @@ export default class AppRenderer { actions.settings.updateEnableIpv6(tunnelOptions.openvpn.enableIpv6); } + async _fetchVersionInfo() { + const actions = this._reduxActions; + const latestVersionInfo = await this._daemonRpc.getVersionInfo(); + const versionFromDaemon = await this._daemonRpc.getCurrentVersion(); + const versionFromGui = remote.app + .getVersion() + .replace('.0-', '-') // remove the .0 in yyyy.x.0-zzz + .replace(/\.0$/, ''); // remove the .0 in yyyy.x.0 + + actions.version.updateVersion(versionFromDaemon, versionFromDaemon === versionFromGui); + actions.version.updateLatest(latestVersionInfo); + } + async _connectToDaemon(): Promise<void> { let credentials; try { @@ -536,6 +551,7 @@ export default class AppRenderer { this._fetchLocation(), this._fetchAccountHistory(), this._fetchTunnelOptions(), + this._fetchVersionInfo(), ]); } diff --git a/gui/packages/desktop/src/renderer/components/Settings.js b/gui/packages/desktop/src/renderer/components/Settings.js index 40c4c2e2de..cb8071a9ce 100644 --- a/gui/packages/desktop/src/renderer/components/Settings.js +++ b/gui/packages/desktop/src/renderer/components/Settings.js @@ -2,15 +2,17 @@ import moment from 'moment'; import * as React from 'react'; -import { Component, View } from 'reactxp'; +import { Component, Text, View } from 'reactxp'; import * as AppButton from './AppButton'; import * as Cell from './Cell'; +import Img from './Img'; import { Layout, Container } from './Layout'; import NavigationBar, { CloseBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; import CustomScrollbars from './CustomScrollbars'; import styles from './SettingsStyles'; import WindowStateObserver from '../lib/window-state-observer'; +import { colors } from '../../config'; import type { LoginState } from '../redux/account/reducers'; @@ -18,6 +20,8 @@ type Props = { loginState: LoginState, accountExpiry: ?string, appVersion: string, + consistentVersion: boolean, + upToDateVersion: boolean, onQuit: () => void, onClose: () => void, onViewAccount: () => void, @@ -133,29 +137,40 @@ export default class Settings extends Component<Props> { } _renderMiddleButtons() { + let icon; + let footer; + if (!this.props.consistentVersion || !this.props.upToDateVersion) { + const message = !this.props.consistentVersion + ? 'Inconsistent internal version information, please restart the app.' + : 'This is not the latest version, download the update to remain safe.'; + + icon = ( + <Img source="icon-alert" tintColor={colors.red} style={styles.settings__version_warning} /> + ); + footer = ( + <View style={styles.settings__cell_footer}> + <Text style={styles.settings__cell_footer_label}>{message}</Text> + </View> + ); + } else { + footer = <View style={styles.settings__cell_spacer} />; + } + return ( <View> <Cell.CellButton onPress={this.props.onExternalLink.bind(this, 'download')} testName="settings__version"> + {icon} <Cell.Label>App version</Cell.Label> - <Cell.SubText>{this._formattedVersion()}</Cell.SubText> + <Cell.SubText>{this.props.appVersion}</Cell.SubText> <Cell.Img height={16} width={16} source="icon-extLink" /> </Cell.CellButton> - <View style={styles.settings__cell_spacer} /> + {footer} </View> ); } - _formattedVersion() { - // the version in package.json has to be semver, but we use a YEAR.release-channel - // version scheme. in package.json we thus have to write YEAR.release.X-channel and - // this function is responsible for removing .X part. - return this.props.appVersion - .replace('.0-', '-') // remove the .0 in 2018.1.0-beta9 - .replace(/\.0$/, ''); // remove the .0 in 2018.1.0 - } - _renderBottomButtons() { return ( <View> diff --git a/gui/packages/desktop/src/renderer/components/SettingsStyles.js b/gui/packages/desktop/src/renderer/components/SettingsStyles.js index 3f63d96b0f..56ec6b3752 100644 --- a/gui/packages/desktop/src/renderer/components/SettingsStyles.js +++ b/gui/packages/desktop/src/renderer/components/SettingsStyles.js @@ -27,14 +27,31 @@ export default { height: 24, flex: 0, }), + settings__cell_footer: Styles.createViewStyle({ + paddingTop: 8, + paddingRight: 24, + paddingBottom: 24, + paddingLeft: 24, + }), settings__footer: Styles.createViewStyle({ paddingTop: 24, paddingBottom: 24, paddingLeft: 24, paddingRight: 24, }), + settings__version_warning: Styles.createViewStyle({ + marginLeft: 8, + }), settings__account_paid_until_label__error: Styles.createTextStyle({ color: colors.red, }), + settings__cell_footer_label: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 13, + fontWeight: '600', + lineHeight: 20, + letterSpacing: -0.2, + color: colors.white60, + }), }; diff --git a/gui/packages/desktop/src/renderer/containers/SettingsPage.js b/gui/packages/desktop/src/renderer/containers/SettingsPage.js index 68fc929342..98b0fcd061 100644 --- a/gui/packages/desktop/src/renderer/containers/SettingsPage.js +++ b/gui/packages/desktop/src/renderer/containers/SettingsPage.js @@ -13,7 +13,9 @@ import type { SharedRouteProps } from '../routes'; const mapStateToProps = (state: ReduxState) => ({ loginState: state.account.status, accountExpiry: state.account.expiry, - appVersion: remote.app.getVersion(), + appVersion: state.version.current, + consistentVersion: state.version.consistent, + upToDateVersion: state.version.upToDate, }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: SharedRouteProps) => { const history = bindActionCreators({ push, goBack }, dispatch); diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index 8e2925f83e..3f3d3af9c1 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -205,6 +205,22 @@ const BackendStateSchema = object({ target_state: enumeration(...allSecurityStates), }); +export type AppVersionInfo = { + currentIsSupported: boolean, + latest: { + latestStable: string, + latest: string, + }, +}; + +const AppVersionInfoSchema = object({ + current_is_supported: boolean, + latest: object({ + latest_stable: string, + latest: string, + }), +}); + export interface DaemonRpcProtocol { connect(string): void; disconnect(): void; @@ -230,6 +246,8 @@ export interface DaemonRpcProtocol { authenticate(sharedSecret: string): Promise<void>; getAccountHistory(): Promise<Array<AccountToken>>; removeAccountFromHistory(accountToken: AccountToken): Promise<void>; + getCurrentVersion(): Promise<string>; + getVersionInfo(): Promise<AppVersionInfo>; } export class ResponseParseError extends Error { @@ -455,4 +473,29 @@ export class DaemonRpc implements DaemonRpcProtocol { async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { await this._transport.send('remove_account_from_history', accountToken); } + + async getCurrentVersion(): Promise<string> { + const response = await this._transport.send('get_current_version'); + try { + return validate(string, response); + } catch (error) { + throw new ResponseParseError('Invalid response from get_current_version', null); + } + } + + async getVersionInfo(): Promise<AppVersionInfo> { + const response = await this._transport.send('get_version_info'); + try { + const versionInfo = validate(AppVersionInfoSchema, response); + return { + currentIsSupported: versionInfo.current_is_supported, + latest: { + latestStable: versionInfo.latest.latest_stable, + latest: versionInfo.latest.latest, + }, + }; + } catch (error) { + throw new ResponseParseError('Invalid response from get_version_info', null); + } + } } diff --git a/gui/packages/desktop/src/renderer/redux/store.js b/gui/packages/desktop/src/renderer/redux/store.js index af2ebc1d5a..58c6645a45 100644 --- a/gui/packages/desktop/src/renderer/redux/store.js +++ b/gui/packages/desktop/src/renderer/redux/store.js @@ -11,6 +11,8 @@ import settings from './settings/reducers'; import settingsActions from './settings/actions'; import support from './support/reducers'; import supportActions from './support/actions'; +import version from './version/reducers'; +import versionActions from './version/actions'; import daemon from './daemon/reducers'; import daemonActions from './daemon/actions'; @@ -20,12 +22,14 @@ import type { AccountReduxState } from './account/reducers'; import type { ConnectionReduxState } from './connection/reducers'; import type { SettingsReduxState } from './settings/reducers'; import type { SupportReduxState } from './support/reducers'; +import type { VersionReduxState } from './version/reducers'; import type { DaemonReduxState } from './daemon/reducers'; import type { AccountAction } from './account/actions'; import type { ConnectionAction } from './connection/actions'; import type { SettingsAction } from './settings/actions'; import type { SupportAction } from './support/actions'; +import type { VersionAction } from './version/actions'; import type { DaemonAction } from './daemon/actions'; export type ReduxState = { @@ -33,6 +37,7 @@ export type ReduxState = { connection: ConnectionReduxState, settings: SettingsReduxState, support: SupportReduxState, + version: VersionReduxState, daemon: DaemonReduxState, }; @@ -41,6 +46,7 @@ export type ReduxAction = | ConnectionAction | SettingsAction | SupportAction + | VersionAction | DaemonAction; export type ReduxStore = Store<ReduxState, ReduxAction, ReduxDispatch>; export type ReduxGetState = () => ReduxState; @@ -57,6 +63,7 @@ export default function configureStore( ...connectionActions, ...settingsActions, ...supportActions, + ...versionActions, ...daemonActions, pushRoute: (route) => push(route), replaceRoute: (route) => replace(route), @@ -67,6 +74,7 @@ export default function configureStore( connection, settings, support, + version, daemon, }; diff --git a/gui/packages/desktop/src/renderer/redux/version/actions.js b/gui/packages/desktop/src/renderer/redux/version/actions.js new file mode 100644 index 0000000000..0277a7f069 --- /dev/null +++ b/gui/packages/desktop/src/renderer/redux/version/actions.js @@ -0,0 +1,33 @@ +// @flow + +import type { AppVersionInfo } from '../../lib/daemon-rpc'; + +export type UpdateLatestAction = { + type: 'UPDATE_LATEST', + latestInfo: AppVersionInfo, +}; + +export type UpdateVersionAction = { + type: 'UPDATE_VERSION', + version: string, + consistent: boolean, +}; + +export type VersionAction = UpdateLatestAction | UpdateVersionAction; + +function updateLatest(latestInfo: AppVersionInfo): UpdateLatestAction { + return { + type: 'UPDATE_LATEST', + latestInfo, + }; +} + +function updateVersion(version: string, consistent: boolean): UpdateVersionAction { + return { + type: 'UPDATE_VERSION', + version, + consistent, + }; +} + +export default { updateLatest, updateVersion }; diff --git a/gui/packages/desktop/src/renderer/redux/version/reducers.js b/gui/packages/desktop/src/renderer/redux/version/reducers.js new file mode 100644 index 0000000000..97e99f0c3e --- /dev/null +++ b/gui/packages/desktop/src/renderer/redux/version/reducers.js @@ -0,0 +1,53 @@ +// @flow + +import type { ReduxAction } from '../store'; + +export type VersionReduxState = { + current: string, + latest: string, + latestStable: string, + upToDate: boolean, + consistent: boolean, +}; + +const initialState: VersionReduxState = { + current: '', + latest: '', + latestStable: '', + upToDate: false, + consistent: true, +}; + +const checkIfLatest = (current: string, latest: string, latestStable: string): boolean => { + return current === latest || current === latestStable; +}; + +export default function( + state: VersionReduxState = initialState, + action: ReduxAction, +): VersionReduxState { + switch (action.type) { + case 'UPDATE_LATEST': { + const latest = action.latestInfo.latest.latest; + const latestStable = action.latestInfo.latest.latestStable; + + return { + ...state, + latest, + latestStable, + upToDate: checkIfLatest(state.current, latest, latestStable), + }; + } + + case 'UPDATE_VERSION': + return { + ...state, + current: action.version, + consistent: action.consistent, + upToDate: checkIfLatest(action.version, state.latest, state.latestStable), + }; + + default: + return state; + } +} |
