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