From c04c1f26f70789decb4b22173da125ed99dfa3f1 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 19 May 2020 12:15:45 -0400 Subject: [PATCH] Do not schedule session expiry notification for 5.24+ (#4291) --- app/actions/views/session.js | 68 ++++++++++++++++ app/init/global_event_handler.js | 79 ++++++++++--------- app/screens/login/index.js | 8 +- app/screens/login/login.js | 13 ++- app/screens/login/login.test.js | 2 +- app/screens/mfa/index.js | 2 +- app/screens/mfa/mfa.js | 14 ++-- app/screens/select_server/index.js | 12 ++- app/screens/select_server/select_server.js | 2 - app/screens/sso/index.js | 6 +- app/utils/security.js | 4 +- .../react-native-notifications+2.1.7.patch | 16 +++- 12 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 app/actions/views/session.js diff --git a/app/actions/views/session.js b/app/actions/views/session.js new file mode 100644 index 000000000..f79cd4e49 --- /dev/null +++ b/app/actions/views/session.js @@ -0,0 +1,68 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import moment from 'moment-timezone'; + +import {getSessions} from '@mm-redux/actions/users'; +import {Client4} from '@mm-redux/client'; +import {getConfig} from '@mm-redux/selectors/entities/general'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; + +import PushNotifications from 'app/push_notifications'; + +const sortByNewest = (a, b) => { + if (a.create_at > b.create_at) { + return -1; + } + + return 1; +}; + +export function scheduleExpiredNotification(intl) { + return async (dispatch, getState) => { + const state = getState(); + const {currentUserId} = state.entities.users; + const config = getConfig(state); + + if (isMinimumServerVersion(Client4.serverVersion, 5, 24) && config.ExtendSessionLengthWithActivity === 'true') { + PushNotifications.cancelAllLocalNotifications(); + return; + } + + let sessions; + try { + sessions = await dispatch(getSessions(currentUserId)); + } catch (e) { + console.warn('Failed to get current session', e); // eslint-disable-line no-console + return; + } + + if (!Array.isArray(sessions.data)) { + return; + } + + const session = sessions.data.sort(sortByNewest)[0]; + const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase + const expiresInDays = parseInt(Math.ceil(Math.abs(moment.duration(moment().diff(expiresAt)).asDays())), 10); + + const message = intl.formatMessage({ + id: 'mobile.session_expired', + defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.', + }, { + siteName: config.SiteName, + daysCount: expiresInDays, + }); + + if (expiresAt) { + // eslint-disable-next-line no-console + console.log('Schedule Session Expiry Local Push Notification', expiresAt); + PushNotifications.localNotificationSchedule({ + date: new Date(expiresAt), + message, + userInfo: { + localNotification: true, + }, + }); + } + }; +} diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index 88988fdae..e956de705 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -9,6 +9,14 @@ import {getLocales} from 'react-native-localize'; import RNFetchBlob from 'rn-fetch-blob'; import semver from 'semver/preload'; +import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from '@actions/device'; +import {selectDefaultChannel} from '@actions/views/channel'; +import {showOverlay} from '@actions/navigation'; +import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from '@actions/views/root'; +import {loadMe, logout} from '@actions/views/user'; +import LocalConfig from '@assets/config'; +import {NavigationTypes, ViewTypes} from '@constants'; +import {getTranslations, resetMomentLocale} from '@i18n'; import {setAppState, setServerVersion} from '@mm-redux/actions/general'; import {autoUpdateTimezone} from '@mm-redux/actions/timezone'; import {close as closeWebSocket} from '@actions/websocket'; @@ -19,15 +27,7 @@ import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels'; import {getCurrentUser, getUser} from '@mm-redux/selectors/entities/users'; import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone'; import EventEmitter from '@mm-redux/utils/event_emitter'; - -import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from '@actions/device'; -import {selectDefaultChannel} from '@actions/views/channel'; -import {showOverlay} from '@actions/navigation'; -import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from '@actions/views/root'; -import {loadMe, logout} from '@actions/views/user'; -import LocalConfig from '@assets/config'; -import {NavigationTypes, ViewTypes} from '@constants'; -import {getTranslations, resetMomentLocale} from '@i18n'; +import {isMinimumServerVersion} from '@mm-redux/utils/helpers'; import {getCurrentLocale} from '@selectors/i18n'; import initialState from '@store/initial_state'; import Store from '@store/store'; @@ -156,10 +156,6 @@ class GlobalEventHandler { emmProvider.handleManagedConfig(true); }; - onServerConfigChanged = (config) => { - this.configureAnalytics(config); - }; - onLogout = async () => { Store.redux.dispatch(closeWebSocket(false)); Store.redux.dispatch(setServerVersion('')); @@ -198,6 +194,12 @@ class GlobalEventHandler { if (this.launchApp) { this.launchApp(); } + + // Reset the state after sending + // the user to the Select server URL screen + // To avoid unavailable data crashes while components are + // still mounted. + this.resetState(); }; onOrientationChange = (dimensions) => { @@ -246,30 +248,35 @@ class GlobalEventHandler { } }; - onServerVersionChanged = async (serverVersion) => { - if (Store?.redux?.dispatch) { - const {dispatch, getState} = Store.redux; - const state = getState(); - const match = serverVersion && serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g); - const version = match && match[0]; - const locale = getCurrentLocale(state); - const translations = getTranslations(locale); + onServerConfigChanged = (config) => { + this.configureAnalytics(config); - if (serverVersion) { - if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { - Alert.alert( - translations[t('mobile.server_upgrade.title')], - translations[t('mobile.server_upgrade.description')], - [{ - text: translations[t('mobile.server_upgrade.button')], - onPress: this.serverUpgradeNeeded, - }], - {cancelable: false}, - ); - } else if (state.entities.users && state.entities.users.currentUserId) { - dispatch(setServerVersion(serverVersion)); - dispatch(loadConfigAndLicense()); - } + if (isMinimumServerVersion(Client4.serverVersion, 5, 24) && config.ExtendSessionLengthWithActivity === 'true') { + PushNotifications.cancelAllLocalNotifications(); + } + }; + + onServerVersionChanged = async (serverVersion) => { + const {dispatch, getState} = Store.redux; + const state = getState(); + const match = serverVersion && serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g); + const version = match && match[0]; + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + if (serverVersion) { + if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) { + Alert.alert( + translations[t('mobile.server_upgrade.title')], + translations[t('mobile.server_upgrade.description')], + [{ + text: translations[t('mobile.server_upgrade.button')], + onPress: this.serverUpgradeNeeded, + }], + {cancelable: false}, + ); + } else if (state.entities.users && state.entities.users.currentUserId) { + dispatch(setServerVersion(serverVersion)); + dispatch(loadConfigAndLicense()); } } }; diff --git a/app/screens/login/index.js b/app/screens/login/index.js index 490fe6f6c..5f876cdc9 100644 --- a/app/screens/login/index.js +++ b/app/screens/login/index.js @@ -4,11 +4,11 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {login} from 'app/actions/views/user'; +import {login} from '@actions/views/user'; +import {scheduleExpiredNotification} from '@actions/views/session'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; -import {isLandscape} from 'app/selectors/device'; -import LoginActions from 'app/actions/views/login'; +import {isLandscape} from '@selectors/device'; import Login from './login.js'; @@ -27,7 +27,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - ...LoginActions, + scheduleExpiredNotification, login, }, dispatch), }; diff --git a/app/screens/login/login.js b/app/screens/login/login.js index dfba4fb77..a09589941 100644 --- a/app/screens/login/login.js +++ b/app/screens/login/login.js @@ -39,7 +39,6 @@ export const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', ' export default class Login extends PureComponent { static propTypes = { actions: PropTypes.shape({ - handleSuccessfulLogin: PropTypes.func.isRequired, scheduleExpiredNotification: PropTypes.func.isRequired, login: PropTypes.func.isRequired, }).isRequired, @@ -81,7 +80,6 @@ export default class Login extends PureComponent { telemetry.remove(['start:overall']); tracker.initialLoad = Date.now(); - this.scheduleSessionExpiredNotification(); resetToChannel(); @@ -94,7 +92,7 @@ export default class Login extends PureComponent { const loginId = this.loginId; const password = this.password; - goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse, loginId, password}); + goToScreen(screen, title, {onMfaComplete: this.checkLoginResponse, goToChannel: this.goToChannel, loginId, password}); }; blur = () => { @@ -125,7 +123,6 @@ export default class Login extends PureComponent { } this.setState({isLoading: false}); - resetToChannel(); return true; }; @@ -309,12 +306,14 @@ export default class Login extends PureComponent { } } - signIn = () => { + signIn = async () => { const {actions} = this.props; const {isLoading} = this.state; if (isLoading) { - actions.login(this.loginId.toLowerCase(), this.password). - then(this.checkLoginResponse); + const result = await actions.login(this.loginId.toLowerCase(), this.password); + if (this.checkLoginResponse(result)) { + this.goToChannel(); + } } }; diff --git a/app/screens/login/login.test.js b/app/screens/login/login.test.js index 112e52bde..53eb3bd26 100644 --- a/app/screens/login/login.test.js +++ b/app/screens/login/login.test.js @@ -23,7 +23,6 @@ describe('Login', () => { }, loginRequest: {}, actions: { - handleSuccessfulLogin: jest.fn(), scheduleExpiredNotification: jest.fn(), login: jest.fn(), }, @@ -94,6 +93,7 @@ describe('Login', () => { 'MFA', 'Multi-factor Authentication', { + goToChannel: wrapper.instance().goToChannel, onMfaComplete: wrapper.instance().checkLoginResponse, loginId, password, diff --git a/app/screens/mfa/index.js b/app/screens/mfa/index.js index 3f2c548a3..369533398 100644 --- a/app/screens/mfa/index.js +++ b/app/screens/mfa/index.js @@ -4,7 +4,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {login} from 'app/actions/views/user'; +import {login} from '@actions/views/user'; import Mfa from './mfa'; diff --git a/app/screens/mfa/mfa.js b/app/screens/mfa/mfa.js index 0f14c1379..48c67b688 100644 --- a/app/screens/mfa/mfa.js +++ b/app/screens/mfa/mfa.js @@ -31,6 +31,7 @@ export default class Mfa extends PureComponent { actions: PropTypes.shape({ login: PropTypes.func.isRequired, }).isRequired, + goToChannel: PropTypes.func.isRequired, loginId: PropTypes.string.isRequired, password: PropTypes.string.isRequired, onMfaComplete: PropTypes.func.isRequired, @@ -78,7 +79,7 @@ export default class Mfa extends PureComponent { }; submit = preventDoubleTap(() => { - const {actions, loginId, password, onMfaComplete} = this.props; + const {actions, goToChannel, loginId, password, onMfaComplete} = this.props; const {token} = this.state; Keyboard.dismiss(); @@ -95,11 +96,14 @@ export default class Mfa extends PureComponent { } setMfaPreflightDone(true); this.setState({isLoading: true}); - actions.login(loginId, password, token).then(() => { - if (!onMfaComplete()) { - popTopScreen(); - } + actions.login(loginId, password, token).then((result) => { this.setState({isLoading: false}); + if (onMfaComplete(result)) { + goToChannel(); + return; + } + + popTopScreen(); }); }); diff --git a/app/screens/select_server/index.js b/app/screens/select_server/index.js index f42f242fd..a8300577c 100644 --- a/app/screens/select_server/index.js +++ b/app/screens/select_server/index.js @@ -4,15 +4,14 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; +import {setLastUpgradeCheck} from '@actions/views/client_upgrade'; +import {loadConfigAndLicense} from '@actions/views/root'; +import {handleServerUrlChanged} from '@actions/views/select_server'; +import {scheduleExpiredNotification} from '@actions/views/session'; import {getPing, resetPing, setServerVersion} from '@mm-redux/actions/general'; import {login} from '@mm-redux/actions/users'; import {getConfig, getLicense} from '@mm-redux/selectors/entities/general'; - -import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade'; -import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login'; -import {loadConfigAndLicense} from 'app/actions/views/root'; -import {handleServerUrlChanged} from 'app/actions/views/select_server'; -import getClientUpgrade from 'app/selectors/client_upgrade'; +import getClientUpgrade from '@selectors/client_upgrade'; import SelectServer from './select_server'; @@ -35,7 +34,6 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - handleSuccessfulLogin, getPing, scheduleExpiredNotification, handleServerUrlChanged, diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index 40c63396e..7cb7a12e6 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -48,7 +48,6 @@ export default class SelectServer extends PureComponent { actions: PropTypes.shape({ getPing: PropTypes.func.isRequired, handleServerUrlChanged: PropTypes.func.isRequired, - handleSuccessfulLogin: PropTypes.func.isRequired, scheduleExpiredNotification: PropTypes.func.isRequired, loadConfigAndLicense: PropTypes.func.isRequired, login: PropTypes.func.isRequired, @@ -297,7 +296,6 @@ export default class SelectServer extends PureComponent { tracker.initialLoad = Date.now(); await this.props.actions.login('credential', 'password'); - await this.props.actions.handleSuccessfulLogin(); this.scheduleSessionExpiredNotification(); resetToChannel(); diff --git a/app/screens/sso/index.js b/app/screens/sso/index.js index 724380852..70d34963c 100644 --- a/app/screens/sso/index.js +++ b/app/screens/sso/index.js @@ -4,10 +4,10 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {scheduleExpiredNotification} from 'app/actions/views/login'; -import {ssoLogin} from 'app/actions/views/user'; +import {ssoLogin} from '@actions/views/user'; +import {scheduleExpiredNotification} from '@actions/views/session'; import {getTheme} from '@mm-redux/selectors/entities/preferences'; -import {isLandscape} from 'app/selectors/device'; +import {isLandscape} from '@selectors/device'; import SSO from './sso'; diff --git a/app/utils/security.js b/app/utils/security.js index dfaaf60a1..b5ec49c96 100644 --- a/app/utils/security.js +++ b/app/utils/security.js @@ -16,8 +16,8 @@ export function getMfaPreflightDone() { export function setCSRFFromCookie(url) { return new Promise((resolve) => { - CookieManager.get(url, false).then((res) => { - const token = res.MMCSRF; + CookieManager.get(url, false).then((cookies) => { + const token = cookies.MMCSRF; if (token) { let value = null; if (typeof token === 'object' && Object.prototype.hasOwnProperty.call(token, 'value')) { diff --git a/patches/react-native-notifications+2.1.7.patch b/patches/react-native-notifications+2.1.7.patch index d3b21d0f7..48213c4dd 100644 --- a/patches/react-native-notifications+2.1.7.patch +++ b/patches/react-native-notifications+2.1.7.patch @@ -1,15 +1,27 @@ diff --git a/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml b/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml -index 7053040..2a97b04 100644 +index 7053040..ad63eff 100644 --- a/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml +++ b/node_modules/react-native-notifications/android/app/src/main/AndroidManifest.xml -@@ -29,6 +29,7 @@ +@@ -11,6 +11,7 @@ + android:protectionLevel="signature" /> + + ++ + + + +@@ -29,7 +30,11 @@ ++ + diff --git a/node_modules/react-native-notifications/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java b/node_modules/react-native-notifications/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java index 7b47aed..200cca8 100644 --- a/node_modules/react-native-notifications/android/app/src/main/java/com/wix/reactnativenotifications/RNNotificationsModule.java