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",