From 54ddec3d463921507df1d39f1044485b073244a2 Mon Sep 17 00:00:00 2001 From: Chris Duarte Date: Thu, 26 Oct 2017 07:45:34 -0700 Subject: [PATCH] Prompt/require user to upgrade to new version (#948) * Prompt/require user to upgrade to new version * Update en.json * Update app message text * Fix typo and add copyright --- app/actions/views/client_upgrade.js | 10 + .../client_upgrade_listener.js | 231 +++++++++++++ .../client_upgrade_listener/index.js | 35 ++ app/constants/view.js | 10 +- app/reducers/views/client_upgrade.js | 16 + app/reducers/views/index.js | 2 + app/screens/channel/channel.js | 13 +- app/screens/channel/index.js | 4 +- app/screens/client_upgrade/client_upgrade.js | 322 ++++++++++++++++++ app/screens/client_upgrade/index.js | 34 ++ app/screens/index.js | 2 + app/screens/select_server/index.js | 21 +- app/screens/select_server/select_server.js | 71 +++- app/screens/settings/general/settings.js | 33 ++ app/selectors/client_upgrade.js | 33 ++ app/utils/client_upgrade.js | 25 ++ assets/base/config.json | 8 +- assets/base/i18n/en.json | 14 + 18 files changed, 853 insertions(+), 31 deletions(-) create mode 100644 app/actions/views/client_upgrade.js create mode 100644 app/components/client_upgrade_listener/client_upgrade_listener.js create mode 100644 app/components/client_upgrade_listener/index.js create mode 100644 app/reducers/views/client_upgrade.js create mode 100644 app/screens/client_upgrade/client_upgrade.js create mode 100644 app/screens/client_upgrade/index.js create mode 100644 app/selectors/client_upgrade.js create mode 100644 app/utils/client_upgrade.js diff --git a/app/actions/views/client_upgrade.js b/app/actions/views/client_upgrade.js new file mode 100644 index 000000000..908a77d8c --- /dev/null +++ b/app/actions/views/client_upgrade.js @@ -0,0 +1,10 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {ViewTypes} from 'app/constants'; + +export function setLastUpgradeCheck() { + return { + type: ViewTypes.SET_LAST_UPGRADE_CHECK + }; +} diff --git a/app/components/client_upgrade_listener/client_upgrade_listener.js b/app/components/client_upgrade_listener/client_upgrade_listener.js new file mode 100644 index 000000000..9073a3f61 --- /dev/null +++ b/app/components/client_upgrade_listener/client_upgrade_listener.js @@ -0,0 +1,231 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Alert, + Animated, + Linking, + TouchableOpacity, + View +} from 'react-native'; +import {injectIntl, intlShape} from 'react-intl'; + +import FormattedText from 'app/components/formatted_text'; +import {UpgradeTypes} from 'app/constants/view'; +import checkUpgradeType from 'app/utils/client_upgrade'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +const {View: AnimatedView} = Animated; + +const UPDATE_TIMEOUT = 60000; + +class ClientUpgradeListener extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + logError: PropTypes.func.isRequired, + setLastUpgradeCheck: PropTypes.func.isRequired + }).isRequired, + currentVersion: PropTypes.string, + downloadLink: PropTypes.string, + forceUpgrade: PropTypes.bool, + intl: intlShape.isRequired, + lastUpgradeCheck: PropTypes.number, + latestVersion: PropTypes.string, + minVersion: PropTypes.string, + navigator: PropTypes.object, + theme: PropTypes.object.isRequired + }; + + state = { + top: new Animated.Value(-100) + } + + componentDidMount() { + const {forceUpgrade, lastUpgradeCheck, latestVersion, minVersion} = this.props; + if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) { + this.checkUpgrade(minVersion, latestVersion); + } + } + + componentWillReceiveProps(nextProps) { + const {forceUpgrade, latestVersion, minVersion} = this.props; + const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps; + + const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion; + if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) { + this.checkUpgrade(minVersion, latestVersion); + } + } + + checkUpgrade = (minVersion, latestVersion) => { + const {actions, currentVersion} = this.props; + + const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError); + + if (upgradeType === UpgradeTypes.NO_UPGRADE) { + return; + } + + this.setState({upgradeType}); + + setTimeout(this.toggleUpgradeMessage, 500); + + actions.setLastUpgradeCheck(); + } + + toggleUpgradeMessage = (show = true) => { + const toValue = show ? 75 : -100; + Animated.timing(this.state.top, { + toValue, + duration: 300 + }).start(); + } + + handleDismiss = () => { + this.toggleUpgradeMessage(false); + } + + handleDownload = () => { + const {downloadLink, intl} = this.props; + + Linking.canOpenURL(downloadLink).then((supported) => { + if (supported) { + return Linking.openURL(downloadLink); + } + + Alert.alert( + intl.formatMessage({ + id: 'mobile.client_upgrade.download_error.title', + defaultMessage: 'Upgrade Error' + }), + intl.formatMessage({ + id: 'mobile.client_upgrade.download_error.message', + defaultMessage: 'An error occurred while trying to open the download link.' + }) + ); + + return false; + }); + + this.toggleUpgradeMessage(false); + } + + handleLearnMore = () => { + this.props.navigator.dismissAllModals({animationType: 'none'}); + + this.props.navigator.showModal({ + screen: 'ClientUpgrade', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: true, + statusBarHideWithNavBar: true + }, + passProps: { + upgradeType: this.state.upgradeType + } + }); + + this.toggleUpgradeMessage(false); + } + + render() { + const {forceUpgrade, theme} = this.props; + const styles = getStyleSheet(theme); + + return ( + + + + + + + + + + + + + {!forceUpgrade && + + + + } + + + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + bottom: { + flexDirection: 'row', + justifyContent: 'space-around', + borderTopColor: changeOpacity(theme.centerChannelColor, 0.2), + backgroundColor: changeOpacity(theme.centerChannelColor, 0.06), + borderTopWidth: 1 + }, + button: { + color: theme.linkColor, + fontSize: 13, + paddingHorizontal: 5, + paddingVertical: 5 + }, + container: { + flex: 1, + backgroundColor: changeOpacity(theme.centerChannelBg, 0.8), + borderRadius: 5 + }, + message: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + messageText: { + fontSize: 16, + color: changeOpacity(theme.centerChannelColor, 0.8), + fontWeight: '600' + }, + wrapper: { + position: 'absolute', + elevation: 5, + left: 30, + right: 30, + height: 75, + backgroundColor: 'white', + borderColor: changeOpacity(theme.centerChannelColor, 0.2), + borderWidth: 2, + borderRadius: 5, + shadowColor: theme.centerChannelColor, + shadowOffset: { + width: 0, + height: 3 + }, + shadowOpacity: 0.2, + shadowRadius: 2 + } + }; +}); + +export default injectIntl(ClientUpgradeListener); diff --git a/app/components/client_upgrade_listener/index.js b/app/components/client_upgrade_listener/index.js new file mode 100644 index 000000000..38b66afce --- /dev/null +++ b/app/components/client_upgrade_listener/index.js @@ -0,0 +1,35 @@ +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {logError} from 'mattermost-redux/actions/errors'; + +import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; +import getClientUpgrade from 'app/selectors/client_upgrade'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import ClientUpgradeListener from './client_upgrade_listener'; + +function mapStateToProps(state) { + const {currentVersion, downloadLink, forceUpgrade, latestVersion, minVersion} = getClientUpgrade(state); + + return { + currentVersion, + downloadLink, + forceUpgrade, + lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck, + latestVersion, + minVersion, + theme: getTheme(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + logError, + setLastUpgradeCheck + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ClientUpgradeListener); diff --git a/app/constants/view.js b/app/constants/view.js index 0e2e8c8a6..c07add7ea 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -3,6 +3,12 @@ import keyMirror from 'mattermost-redux/utils/key_mirror'; +export const UpgradeTypes = { + CAN_UPGRADE: 'can_upgrade', + MUST_UPGRADE: 'must_upgrade', + NO_UPGRADE: 'no_upgrade' +}; + const ViewTypes = keyMirror({ SERVER_URL_CHANGED: null, @@ -44,7 +50,9 @@ const ViewTypes = keyMirror({ RECEIVED_FOCUSED_POST: null, LOADING_POSTS: null, - RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null + RECEIVED_POSTS_FOR_CHANNEL_AT_TIME: null, + + SET_LAST_UPGRADE_CHECK: null }); export default { diff --git a/app/reducers/views/client_upgrade.js b/app/reducers/views/client_upgrade.js new file mode 100644 index 000000000..c1759b3c9 --- /dev/null +++ b/app/reducers/views/client_upgrade.js @@ -0,0 +1,16 @@ +const initialState = { + lastUpdateCheck: 0 +}; + +import {ViewTypes} from 'app/constants'; + +export default function clientUpgrade(state = initialState, action) { + switch (action.type) { + case ViewTypes.SET_LAST_UPGRADE_CHECK: + return { + lastUpdateCheck: Date.now() + }; + default: + return state; + } +} diff --git a/app/reducers/views/index.js b/app/reducers/views/index.js index 642fa47e3..dd158d526 100644 --- a/app/reducers/views/index.js +++ b/app/reducers/views/index.js @@ -4,6 +4,7 @@ import {combineReducers} from 'redux'; import channel from './channel'; +import clientUpgrade from './client_upgrade'; import fetchCache from './fetch_cache'; import i18n from './i18n'; import login from './login'; @@ -15,6 +16,7 @@ import thread from './thread'; export default combineReducers({ channel, + clientUpgrade, fetchCache, i18n, login, diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index c4463b0a3..3da4afc82 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -11,6 +11,7 @@ import { import EventEmitter from 'mattermost-redux/utils/event_emitter'; +import ClientUpgradeListener from 'app/components/client_upgrade_listener'; import ChannelDrawer from 'app/components/channel_drawer'; import ChannelLoader from 'app/components/channel_loader'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; @@ -23,6 +24,8 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; import PostTextbox from 'app/components/post_textbox'; import networkConnectionListener from 'app/utils/network'; +import LocalConfig from 'assets/config'; + import ChannelDrawerButton from './channel_drawer_button'; import ChannelPostList from './channel_post_list'; import ChannelSearchButton from './channel_search_button'; @@ -43,12 +46,12 @@ class Channel extends PureComponent { stopPeriodicStatusUpdates: PropTypes.func.isRequired, viewChannel: PropTypes.func.isRequired }).isRequired, + currentChannelId: PropTypes.string, + channelsRequestFailed: PropTypes.bool, + currentTeamId: PropTypes.string, intl: intlShape.isRequired, navigator: PropTypes.object, - currentTeamId: PropTypes.string, - currentChannelId: PropTypes.string, - theme: PropTypes.object.isRequired, - channelsRequestFailed: PropTypes.bool + theme: PropTypes.object.isRequired }; componentWillMount() { @@ -215,7 +218,9 @@ class Channel extends PureComponent { ref={this.attachPostTextbox} navigator={navigator} /> + + {LocalConfig.EnableMobileClientUpgrade && } ); } diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 3893fd5cb..8717e0104 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -26,10 +26,10 @@ function mapStateToProps(state) { const {myChannels: channelsRequest} = state.requests.channels; return { + channelsRequestFailed: channelsRequest.status === RequestStatus.FAILURE, currentTeamId: getCurrentTeamId(state), currentChannelId: getCurrentChannelId(state), - theme: getTheme(state), - channelsRequestFailed: channelsRequest.status === RequestStatus.FAILURE + theme: getTheme(state) }; } diff --git a/app/screens/client_upgrade/client_upgrade.js b/app/screens/client_upgrade/client_upgrade.js new file mode 100644 index 000000000..148ed27d2 --- /dev/null +++ b/app/screens/client_upgrade/client_upgrade.js @@ -0,0 +1,322 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Alert, + Image, + Linking, + ScrollView, + TouchableOpacity, + View +} from 'react-native'; +import {injectIntl, intlShape} from 'react-intl'; + +import FormattedText from 'app/components/formatted_text'; +import StatusBar from 'app/components/status_bar'; +import {UpgradeTypes} from 'app/constants/view'; +import logo from 'assets/images/logo.png'; +import checkUpgradeType from 'app/utils/client_upgrade'; +import {makeStyleSheetFromTheme} from 'app/utils/theme'; + +class ClientUpgrade extends PureComponent { + static propTypes = { + actions: PropTypes.shape({ + logError: PropTypes.func.isRequired, + setLastUpgradeCheck: PropTypes.func.isRequired + }).isRequired, + currentVersion: PropTypes.string, + closeAction: PropTypes.func, + userCheckedForUpgrade: PropTypes.bool, + downloadLink: PropTypes.string.isRequired, + forceUpgrade: PropTypes.bool, + intl: intlShape.isRequired, + latestVersion: PropTypes.string, + navigator: PropTypes.object, + theme: PropTypes.object.isRequired, + upgradeType: PropTypes.string + }; + + state = { + upgradeType: UpgradeTypes.NO_UPGRADE + } + + componentDidMount() { + if (this.props.userCheckedForUpgrade) { + this.checkUpgrade(this.props); + } + } + + checkUpgrade = ({minVersion, latestVersion}) => { + const {actions, currentVersion, downloadLink} = this.props; + + // We need at least minVersion or latestVersion and the app downloadlink + if (!(latestVersion || minVersion) || !downloadLink) { + return; + } + + const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError); + + if (upgradeType === UpgradeTypes.NO_UPGRADE) { + return; + } + + this.setState({ + upgradeType + }); + + actions.setLastUpgradeCheck(); + } + + handleClose = () => { + if (this.props.closeAction) { + this.props.closeAction(); + } else if (this.props.userCheckedForUpgrade) { + this.props.navigator.pop(); + } else { + this.props.navigator.dismissModal(); + } + } + + handleDownload = () => { + const {downloadLink, intl} = this.props; + + Linking.canOpenURL(downloadLink).then((supported) => { + if (supported) { + return Linking.openURL(downloadLink); + } + + Alert.alert( + intl.formatMessage({ + id: 'mobile.client_upgrade.download_error.title', + defaultMessage: 'Upgrade Error' + }), + intl.formatMessage({ + id: 'mobile.client_upgrade.download_error.message', + defaultMessage: 'An error occurred while trying to open the download link.' + }) + ); + + return false; + }); + } + + handleUpgrade = () => { + const {actions, downloadLink} = this.props; + + try { + Linking.openURL(downloadLink); + } catch (error) { + actions.logError(error); + } + } + + renderMustUpgrade() { + const {theme} = this.props; + const styles = getStyleFromTheme(theme); + + return ( + + + + + ); + } + + renderCanUpgrade() { + const {theme} = this.props; + const styles = getStyleFromTheme(theme); + + return ( + + + + + ); + } + + renderNoUpgrade() { + const {theme} = this.props; + const styles = getStyleFromTheme(theme); + + return ( + + + + + ); + } + + renderMessageContent = () => { + const {currentVersion, forceUpgrade, latestVersion, upgradeType: passedUpgradeType, userCheckedForUpgrade, theme} = this.props; + const styles = getStyleFromTheme(theme); + const upgradeType = userCheckedForUpgrade ? this.state.upgradeType : passedUpgradeType; + + let messageComponent; + + switch (upgradeType) { + case UpgradeTypes.CAN_UPGRADE: + messageComponent = this.renderCanUpgrade(); + break; + case UpgradeTypes.MUST_UPGRADE: + messageComponent = this.renderMustUpgrade(); + break; + default: + case UpgradeTypes.NO_UPGRADE: + messageComponent = this.renderNoUpgrade(); + break; + } + + return ( + + {messageComponent} + + + {upgradeType !== UpgradeTypes.NO_UPGRADE && + + + + + {!forceUpgrade && + + + + } + + } + + ); + } + + render() { + const {theme} = this.props; + + const styles = getStyleFromTheme(theme); + + return ( + + + + {this.renderMessageContent()} + + ); + } +} + +const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { + return { + image: { + marginTop: 75, + width: 76, + height: 75 + }, + messageButton: { + marginBottom: 15, + borderWidth: 2, + borderRadius: 2, + alignItems: 'center', + justifyContent: 'center', + borderColor: theme.buttonBg + }, + messageButtonText: { + paddingVertical: 10, + paddingHorizontal: 20, + fontWeight: '600', + color: theme.buttonBg + }, + messageContainer: { + marginTop: 25, + flex: 1, + alignSelf: 'stretch', + alignItems: 'center' + }, + messageCloseButton: { + marginBottom: 15, + alignItems: 'center', + justifyContent: 'center' + }, + messageSubtitle: { + fontSize: 16, + marginBottom: 20, + color: theme.centerChannelColor, + textAlign: 'center', + paddingHorizontal: 30 + }, + messageTitle: { + fontSize: 24, + marginBottom: 20, + fontWeight: '600', + color: theme.centerChannelColor, + textAlign: 'center', + paddingHorizontal: 30 + }, + scrollView: { + flex: 1 + }, + scrollViewContent: { + paddingBottom: 20, + alignItems: 'center' + } + }; +}); + +export default injectIntl(ClientUpgrade); diff --git a/app/screens/client_upgrade/index.js b/app/screens/client_upgrade/index.js new file mode 100644 index 000000000..34ce5ef86 --- /dev/null +++ b/app/screens/client_upgrade/index.js @@ -0,0 +1,34 @@ +import {bindActionCreators} from 'redux'; +import {connect} from 'react-redux'; + +import {logError} from 'mattermost-redux/actions/errors'; + +import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; +import getClientUpgrade from 'app/selectors/client_upgrade'; +import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; + +import ClientUpgrade from './client_upgrade'; + +function mapStateToProps(state) { + const {currentVersion, downloadLink, forceUpgrade, latestVersion, minVersion} = getClientUpgrade(state); + + return { + currentVersion, + downloadLink, + forceUpgrade, + latestVersion, + minVersion, + theme: getTheme(state) + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + logError, + setLastUpgradeCheck + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(ClientUpgrade); diff --git a/app/screens/index.js b/app/screens/index.js index 67b8d7909..10d3ee475 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -12,6 +12,7 @@ import Channel from 'app/screens/channel'; import ChannelAddMembers from 'app/screens/channel_add_members'; import ChannelInfo from 'app/screens/channel_info'; import ChannelMembers from 'app/screens/channel_members'; +import ClientUpgrade from 'app/screens/client_upgrade'; import Code from 'app/screens/code'; import CreateChannel from 'app/screens/create_channel'; import EditPost from 'app/screens/edit_post'; @@ -62,6 +63,7 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider); Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider); Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider); + Navigation.registerComponent('ClientUpgrade', () => wrapWithContextProvider(ClientUpgrade), store, Provider); Navigation.registerComponent('Code', () => wrapWithContextProvider(Code), store, Provider); Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider); Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider); diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index 5e9ea74d8..be7db4ab8 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -7,7 +7,9 @@ import {connect} from 'react-redux'; import {getPing, resetPing} from 'mattermost-redux/actions/general'; import {RequestStatus} from 'mattermost-redux/constants'; +import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; import {handleServerUrlChanged} from 'app/actions/views/select_server'; +import getClientUpgrade from 'app/selectors/client_upgrade'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import SelectServer from './select_server'; @@ -15,28 +17,33 @@ import SelectServer from './select_server'; function mapStateToProps(state) { const {config: configRequest, license: licenseRequest, server: pingRequest} = state.requests.general; const {config, license} = state.entities.general; + const {currentVersion, latestVersion, minVersion} = getClientUpgrade(state); const success = RequestStatus.SUCCESS; const transition = (pingRequest.status === success && configRequest.status === success && licenseRequest.status === success); return { ...state.views.selectServer, - pingRequest, - configRequest, - licenseRequest, config, + configRequest, + currentVersion, + pingRequest, + latestVersion, license, - transition, - theme: getTheme(state) + licenseRequest, + minVersion, + theme: getTheme(state), + transition }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - handleServerUrlChanged, getPing, - resetPing + handleServerUrlChanged, + resetPing, + setLastUpgradeCheck }, dispatch) }; } diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index a9bff1c98..18fa3d34d 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -15,6 +15,7 @@ import { View } from 'react-native'; import Button from 'react-native-button'; +import urlParse from 'url-parse'; import {RequestStatus} from 'mattermost-redux/constants'; import {Client, Client4} from 'mattermost-redux/client'; @@ -24,29 +25,34 @@ import FormattedText from 'app/components/formatted_text'; import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder'; import {GlobalStyles} from 'app/styles'; import {isValidUrl, stripTrailingSlashes} from 'app/utils/url'; +import {UpgradeTypes} from 'app/constants/view'; +import checkUpgradeType from 'app/utils/client_upgrade'; -import urlParse from 'url-parse'; - +import LocalConfig from 'assets/config'; import logo from 'assets/images/logo.png'; class SelectServer extends PureComponent { static propTypes = { - allowOtherServers: PropTypes.bool, - navigator: PropTypes.object, - intl: intlShape.isRequired, - config: PropTypes.object, - license: PropTypes.object, - theme: PropTypes.object, - transition: PropTypes.bool.isRequired, - serverUrl: PropTypes.string.isRequired, - pingRequest: PropTypes.object.isRequired, - configRequest: PropTypes.object.isRequired, - licenseRequest: PropTypes.object.isRequired, actions: PropTypes.shape({ getPing: PropTypes.func.isRequired, + handleServerUrlChanged: PropTypes.func.isRequired, resetPing: PropTypes.func.isRequired, - handleServerUrlChanged: PropTypes.func.isRequired - }).isRequired + setLastUpgradeCheck: PropTypes.func.isRequired + }).isRequired, + allowOtherServers: PropTypes.bool, + config: PropTypes.object, + configRequest: PropTypes.object.isRequired, + currentVersion: PropTypes.string, + intl: intlShape.isRequired, + latestVersion: PropTypes.string, + license: PropTypes.object, + licenseRequest: PropTypes.object.isRequired, + minVersion: PropTypes.string, + navigator: PropTypes.object, + pingRequest: PropTypes.object.isRequired, + serverUrl: PropTypes.string.isRequired, + theme: PropTypes.object, + transition: PropTypes.bool.isRequired }; constructor(props) { @@ -72,7 +78,18 @@ class SelectServer extends PureComponent { componentWillReceiveProps(nextProps) { if (!this.props.transition && nextProps.transition) { - this.handleLoginOptions(); + if (LocalConfig.EnableMobileClientUpgrade) { + this.props.actions.setLastUpgradeCheck(); + const {currentVersion, minVersion, latestVersion} = nextProps; + const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion); + if (upgradeType === UpgradeTypes.NO_UPGRADE) { + this.handleLoginOptions(); + } else { + this.handleShowClientUpgrade(upgradeType); + } + } else { + this.handleLoginOptions(); + } } } @@ -82,6 +99,28 @@ class SelectServer extends PureComponent { } } + handleShowClientUpgrade = (upgradeType) => { + const {intl, theme} = this.props; + + this.props.navigator.push({ + screen: 'ClientUpgrade', + title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}), + backButtonTitle: '', + navigatorStyle: { + navBarHidden: false, + statusBarHidden: true, + statusBarHideWithNavBar: true, + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor + }, + passProps: { + closeAction: this.handleLoginOptions, + upgradeType + } + }); + } + handleLoginOptions = () => { const {config, intl, license, theme} = this.props; const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true'; diff --git a/app/screens/settings/general/settings.js b/app/screens/settings/general/settings.js index bb3d10da9..50a8d2734 100644 --- a/app/screens/settings/general/settings.js +++ b/app/screens/settings/general/settings.js @@ -18,6 +18,8 @@ import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import {isValidUrl} from 'app/utils/url'; +import LocalConfig from 'assets/config'; + class Settings extends PureComponent { static propTypes = { actions: PropTypes.shape({ @@ -134,6 +136,26 @@ class Settings extends PureComponent { }); }; + goToClientUpgrade = () => { + const {intl, theme} = this.props; + + this.props.navigator.push({ + screen: 'ClientUpgrade', + title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}), + navigatorStyle: { + navBarHidden: false, + statusBarHidden: true, + statusBarHideWithNavBar: true, + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor + }, + passProps: { + userCheckedForUpgrade: true + } + }); + } + handlePress = (action) => { preventDoubleTap(action, this); }; @@ -229,6 +251,17 @@ class Settings extends PureComponent { showArrow={showArrow} theme={theme} /> + {LocalConfig.EnableMobileClientUpgrade && LocalConfig.EnableMobileClientUpgradeUserSetting && + this.handlePress(this.goToClientUpgrade)} + showArrow={showArrow} + theme={theme} + /> + } { + const {AndroidMinVersion, AndroidLatestVersion, IosMinVersion, IosLatestVersion} = config; + + let minVersion = IosMinVersion; + let latestVersion = IosLatestVersion; + let downloadLink = LocalConfig.MobileClientUpgradeIosIpaLink; + if (Platform.OS === 'android') { + minVersion = AndroidMinVersion; + latestVersion = AndroidLatestVersion; + downloadLink = LocalConfig.MobileClientUpgradeAndroidApkLink; + } + + return { + currentVersion: DeviceInfo.getVersion(), + downloadLink, + forceUpgrade: LocalConfig.EnableForceMobileClientUpgrade, + latestVersion, + minVersion + }; + } +); + +export default getClientUpgrade; diff --git a/app/utils/client_upgrade.js b/app/utils/client_upgrade.js new file mode 100644 index 000000000..a1ee991f0 --- /dev/null +++ b/app/utils/client_upgrade.js @@ -0,0 +1,25 @@ +import semver from 'semver'; + +import {UpgradeTypes} from 'app/constants/view'; + +import LocalConfig from 'assets/config'; + +export default function checkUpgradeType(currentVersion, minVersion, latestVersion, logError) { + let upgradeType = UpgradeTypes.NO_UPGRADE; + + try { + if (minVersion && semver.lt(currentVersion, minVersion)) { + upgradeType = UpgradeTypes.MUST_UPGRADE; + } else if (latestVersion && semver.lt(currentVersion, latestVersion)) { + if (LocalConfig.EnableForceMobileClientUpgrade) { + upgradeType = UpgradeTypes.MUST_UPGRADE; + } else { + upgradeType = UpgradeTypes.CAN_UPGRADE; + } + } + } catch (error) { + logError(error.message); + } + + return upgradeType; +} diff --git a/assets/base/config.json b/assets/base/config.json index c9ac6addc..0bcd6b67b 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -21,5 +21,11 @@ "xhr": false, "console": true } - } + }, + + "EnableMobileClientUpgrade": false, + "EnableMobileClientUpgradeUserSetting": false, + "EnableForceMobileClientUpgrade": false, + "MobileClientUpgradeAndroidApkLink": "", + "MobileClientUpgradeIosIpaLink": "" } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 879d8cea8..5cfe6cb9f 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -1913,6 +1913,19 @@ "mobile.channel_list.privateChannel": "Private Channel", "mobile.channel_list.publicChannel": "Public Channel", "mobile.channel_list.unreads": "UNREADS", + "mobile.client_upgrade": "Update App", + "mobile.client_upgrade.can_upgrade_title": "Update Available", + "mobile.client_upgrade.can_upgrade_subtitle": "A new version is available for download.", + "mobile.client_upgrade.close": "Update Later", + "mobile.client_upgrade.current_version": "Latest Version: {version}", + "mobile.client_upgrade.download_error.title": "Unable to Install Update", + "mobile.client_upgrade.download_error.message": "An error occurred downloading the new version.", + "mobile.client_upgrade.latest_version": "Your Version: {version}", + "mobile.client_upgrade.must_upgrade_title": "Update Required", + "mobile.client_upgrade.must_upgrade_subtitle": "Please update the app to continue.", + "mobile.client_upgrade.no_upgrade_title": "Your App Is Up to Date", + "mobile.client_upgrade.no_upgrade_subtitle": "You already have the latest version.", + "mobile.client_upgrade.upgrade": "Update", "mobile.components.channels_list_view.yourChannels": "Your channels:", "mobile.components.error_list.dismiss_all": "Dismiss All", "mobile.components.select_server_view.continue": "Continue", @@ -2044,6 +2057,7 @@ "mobile.server_upgrade.title": "Server upgrade required", "mobile.server_url.invalid_format": "URL must start with http:// or https://", "mobile.session_expired": "Session Expired: Please log in to continue receiving notifications.", + "mobile.settings.modal.check_for_upgrade": "Check for Updates", "mobile.settings.clear": "Clear Offline Store", "mobile.settings.clear_button": "Clear", "mobile.settings.clear_message": "\nThis will clear all offline data and restart the app. You will be automatically logged back in once the app restarts.\n",