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
This commit is contained in:
Chris Duarte 2017-10-26 07:45:34 -07:00 committed by Harrison Healey
parent 59ce8571c3
commit 54ddec3d46
18 changed files with 853 additions and 31 deletions

View file

@ -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
};
}

View file

@ -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 (
<AnimatedView
style={[styles.wrapper, {top: this.state.top}]}
>
<View style={styles.container}>
<View style={styles.message}>
<FormattedText
id='mobile.client_upgrade.listener.message'
defaultMessage='A client upgrade is available!'
style={styles.messageText}
/>
</View>
<View style={styles.bottom}>
<TouchableOpacity onPress={this.handleDownload}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.upgrade_button'
defaultMessage='Upgrade'
/>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleLearnMore}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.learn_more_button'
defaultMessage='Learn More'
/>
</TouchableOpacity>
{!forceUpgrade &&
<TouchableOpacity onPress={this.handleDismiss}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.dismiss_button'
defaultMessage='Dismiss'
/>
</TouchableOpacity>
}
</View>
</View>
</AnimatedView>
);
}
}
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);

View file

@ -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);

View file

@ -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 {

View file

@ -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;
}
}

View file

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

View file

@ -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}
/>
<ChannelLoader theme={theme}/>
</KeyboardLayout>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</ChannelDrawer>
);
}

View file

@ -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)
};
}

View file

@ -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 (
<View style={styles.messageContent}>
<FormattedText
id='mobile.client_upgrade.must_upgrade_title'
defaultMessage='Upgrade Required'
style={styles.messageTitle}
/>
<FormattedText
id='mobile.client_upgrade.must_upgrade_subtitle'
defaultMessage='Please update the app to continue.'
style={styles.messageSubtitle}
/>
</View>
);
}
renderCanUpgrade() {
const {theme} = this.props;
const styles = getStyleFromTheme(theme);
return (
<View style={styles.messageContent}>
<FormattedText
id='mobile.client_upgrade.can_upgrade_title'
defaultMessage='Upgrade Available!'
style={styles.messageTitle}
/>
<FormattedText
id='mobile.client_upgrade.can_upgrade_subtitle'
defaultMessage={'There\'s a new upgrade ready for download.'}
style={styles.messageSubtitle}
/>
</View>
);
}
renderNoUpgrade() {
const {theme} = this.props;
const styles = getStyleFromTheme(theme);
return (
<View style={styles.messageContent}>
<FormattedText
id='mobile.client_upgrade.no_upgrade_title'
defaultMessage='Your App Is Up to Date'
style={styles.messageTitle}
/>
<FormattedText
id='mobile.client_upgrade.no_upgrade_subtitle'
defaultMessage={'You\'re already using the latest version.'}
style={styles.messageSubtitle}
/>
</View>
);
}
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 (
<View style={styles.messageContainer}>
{messageComponent}
<FormattedText
id='mobile.client_upgrade.current_version'
defaultMessage='Lastest Version: {version}'
style={styles.messageSubtitle}
values={{
version: latestVersion
}}
/>
<FormattedText
id='mobile.client_upgrade.latest_version'
defaultMessage='Your Version: {version}'
style={styles.messageSubtitle}
values={{
version: currentVersion
}}
/>
{upgradeType !== UpgradeTypes.NO_UPGRADE &&
<View>
<TouchableOpacity
onPress={this.handleDownload}
style={styles.messageButton}
>
<FormattedText
id='mobile.client_upgrade.upgrade'
defaultMessage='Upgrade'
style={styles.messageButtonText}
/>
</TouchableOpacity>
{!forceUpgrade &&
<TouchableOpacity
onPress={this.handleClose}
style={styles.messageCloseButton}
>
<FormattedText
id='mobile.client_upgrade.close'
defaultMessage='Upgrade Later'
style={styles.messageCloseButtonText}
/>
</TouchableOpacity>
}
</View>
}
</View>
);
}
render() {
const {theme} = this.props;
const styles = getStyleFromTheme(theme);
return (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
>
<StatusBar/>
<Image
source={logo}
style={styles.image}
/>
{this.renderMessageContent()}
</ScrollView>
);
}
}
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);

View file

@ -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);

View file

@ -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);

View file

@ -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)
};
}

View file

@ -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';

View file

@ -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 &&
<SettingsItem
defaultMessage='Check for Upgrade'
i18nId='mobile.settings.modal.check_for_upgrade'
iconName='update'
iconType='material'
onPress={() => this.handlePress(this.goToClientUpgrade)}
showArrow={showArrow}
theme={theme}
/>
}
<SettingsItem
defaultMessage='About Mattermost'
i18nId='about.title'

View file

@ -0,0 +1,33 @@
import {createSelector} from 'reselect';
import {Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import LocalConfig from 'assets/config';
const getClientUpgrade = createSelector(
getConfig,
(config) => {
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;

View file

@ -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;
}

View file

@ -21,5 +21,11 @@
"xhr": false,
"console": true
}
}
},
"EnableMobileClientUpgrade": false,
"EnableMobileClientUpgradeUserSetting": false,
"EnableForceMobileClientUpgrade": false,
"MobileClientUpgradeAndroidApkLink": "",
"MobileClientUpgradeIosIpaLink": ""
}

View file

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