diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js index de262cf7d..9bc18c0f7 100644 --- a/app/init/global_event_handler.js +++ b/app/init/global_event_handler.js @@ -276,6 +276,11 @@ class GlobalEventHandler { const state = Store.redux.getState(); const newState = { ...initialState, + app: { + build: DeviceInfo.getBuildNumber(), + version: DeviceInfo.getVersion(), + previousVersion: state.app?.previousVersion || DeviceInfo.getVersion(), + }, entities: { ...initialState.entities, general: { diff --git a/app/mattermost.js b/app/mattermost.js index 939d4cc7d..4c2d4f34d 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -25,6 +25,7 @@ import Store from '@store/store'; import {waitForHydration} from '@store/utils'; import {validatePreviousVersion} from '@utils/general'; import pushNotificationsUtils from '@utils/push_notifications'; +import {captureJSException} from '@utils/sentry'; const init = async () => { const credentials = await getAppCredentials(); @@ -55,11 +56,18 @@ const launchApp = (credentials) => { 'start:channel_screen', ]); + const store = Store.redux; if (credentials) { - waitForHydration(Store.redux, async () => { - if (validatePreviousVersion(Store.redux, EphemeralStore.prevAppVersion)) { - Store.redux.dispatch(loadMe()); + waitForHydration(store, async () => { + const {previousVersion} = store.getState().app; + const valid = validatePreviousVersion(previousVersion); + if (valid) { + store.dispatch(loadMe()); resetToChannel({skipMetrics: true}); + } else { + const error = new Error(`Previous app version "${previousVersion}" is invalid.`); + captureJSException(error, false, store); + store.dispatch(logout()); } }); } else { @@ -71,7 +79,7 @@ const launchApp = (credentials) => { Linking.getInitialURL().then((url) => { if (url) { - Store.redux.dispatch(setDeepLinkURL(url)); + store.dispatch(setDeepLinkURL(url)); } }); }; diff --git a/app/reducers/app/index.js b/app/reducers/app/index.js index 814ba1094..86b019788 100644 --- a/app/reducers/app/index.js +++ b/app/reducers/app/index.js @@ -5,8 +5,10 @@ import {combineReducers} from 'redux'; import build from './build'; import version from './version'; +import previousVersion from './previousVersion'; export default combineReducers({ build, version, + previousVersion, }); diff --git a/app/reducers/app/previousVersion.js b/app/reducers/app/previousVersion.js new file mode 100644 index 000000000..c7ea49b44 --- /dev/null +++ b/app/reducers/app/previousVersion.js @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export default function previousVersion(state = '') { + return state; +} diff --git a/app/store/middlewares/helpers.js b/app/store/middlewares/helpers.js index 5ecab892e..04425bbb6 100644 --- a/app/store/middlewares/helpers.js +++ b/app/store/middlewares/helpers.js @@ -101,8 +101,7 @@ export function resetStateForNewVersion(payload) { rehydrated: true, }, app: { - build: DeviceInfo.getBuildNumber(), - version: DeviceInfo.getVersion(), + ...payload.app, }, entities: { channels, diff --git a/app/store/middlewares/message_retention.js b/app/store/middlewares/message_retention.js index 567c55583..5c5f0566a 100644 --- a/app/store/middlewares/message_retention.js +++ b/app/store/middlewares/message_retention.js @@ -3,11 +3,8 @@ import DeviceInfo from 'react-native-device-info'; import {REHYDRATE} from 'redux-persist'; -import semver from 'semver/preload'; import {ViewTypes} from '@constants'; -import {General} from '@mm-redux/constants'; -import EphemeralStore from '@store/ephemeral_store'; import { captureException, LOGGER_JAVASCRIPT_WARNING, @@ -20,17 +17,7 @@ export default function messageRetention(store) { if (action.type === REHYDRATE) { if (!action.payload) { // On first run payload is not set (when installed) - const version = DeviceInfo.getVersion(); - const major = semver.major(version); - const minor = semver.minor(version); - const patch = semver.patch(version); - const prevAppVersion = `${major}.${parseInt(minor, 10) - 1}.${patch}`; - EphemeralStore.prevAppVersion = prevAppVersion; action.payload = { - app: { - build: DeviceInfo.getBuildNumber(), - version, - }, _persist: { rehydrated: true, }, @@ -40,16 +27,26 @@ export default function messageRetention(store) { const {app} = action.payload; const {entities, views} = action.payload; - if (!EphemeralStore.prevAppVersion) { - EphemeralStore.prevAppVersion = app?.version; - } + const build = DeviceInfo.getBuildNumber(); + const version = DeviceInfo.getVersion(); + const previousVersion = app?.version; + const previousBuild = app?.build; + + action.payload = { + ...action.payload, + app: { + build, + version, + previousVersion, + }, + }; if (!entities || !views) { return next(action); } // When a new version of the app has been detected - if (!app || !app.version || app.version !== DeviceInfo.getVersion() || app.build !== DeviceInfo.getBuildNumber()) { + if (previousVersion !== version || previousBuild !== build) { action.payload = resetStateForNewVersion(action.payload); return next(action); } diff --git a/app/store/middlewares/middleware.test.js b/app/store/middlewares/middleware.test.js index 39057c83b..d6a2bdded 100644 --- a/app/store/middlewares/middleware.test.js +++ b/app/store/middlewares/middleware.test.js @@ -3,7 +3,10 @@ /* eslint-disable max-nested-callbacks */ +import DeviceInfo from 'react-native-device-info'; + import assert from 'assert'; +import {REHYDRATE} from 'redux-persist'; import {ViewTypes} from '@constants'; import { @@ -55,6 +58,122 @@ describe('messageRetention', () => { }); }); }); + + describe('should add build, version, and previousVersion to payload.app on persist/REHYDRATE', () => { + const next = (a) => a; + const store = {}; + const build = 'build'; + const version = 'version'; + const previousBuild = 'previous-build'; + const previousVersion = 'previous-version'; + DeviceInfo.getBuildNumber = jest.fn().mockReturnValue('build'); + DeviceInfo.getVersion = jest.fn().mockReturnValue('version'); + const rehydrateAction = { + type: REHYDRATE, + payload: { + app: { + build: previousBuild, + version: previousVersion, + }, + }, + }; + const expectedPayloadApp = { + build, + version, + previousVersion, + }; + const entities = { + channels: {}, + posts: {}, + }; + const views = { + team: { + lastChannelForTeam: {}, + }, + }; + + test('when entities is missing', () => { + const action = {...rehydrateAction}; + + const nextAction = messageRetention(store)(next)(action); + expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp); + }); + + test('when views is missing', () => { + const action = { + ...rehydrateAction, + payload: { + ...rehydrateAction.payload, + entities, + }, + }; + + const nextAction = messageRetention(store)(next)(action); + expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp); + }); + + test('when previousVersion !== version', () => { + const action = { + ...rehydrateAction, + payload: { + ...rehydrateAction.payload, + entities, + views, + }, + }; + expect(action.payload.app.version).not.toEqual(DeviceInfo.getVersion()); + + const nextAction = messageRetention(store)(next)(action); + expect(nextAction.payload.app).toStrictEqual(expectedPayloadApp); + }); + + test('when previousBuild !== build', () => { + const action = { + ...rehydrateAction, + payload: { + ...rehydrateAction.payload, + app: { + ...rehydrateAction.payload.app, + version: DeviceInfo.getVersion(), + }, + entities, + views, + }, + }; + expect(action.payload.app.version).toEqual(DeviceInfo.getVersion()); + expect(action.payload.app.build).not.toEqual(DeviceInfo.getBuildNumber()); + + const nextAction = messageRetention(store)(next)(action); + expect(nextAction.payload.app).toStrictEqual({ + ...expectedPayloadApp, + previousVersion: DeviceInfo.getVersion(), + }); + }); + + test('when cleanUpState', () => { + const action = { + ...rehydrateAction, + payload: { + ...rehydrateAction.payload, + app: { + ...rehydrateAction.payload.app, + version: DeviceInfo.getVersion(), + build: DeviceInfo.getBuildNumber(), + }, + entities, + views, + }, + }; + expect(action.payload.app.version).toEqual(DeviceInfo.getVersion()); + expect(action.payload.app.build).toEqual(DeviceInfo.getBuildNumber()); + + const nextAction = messageRetention(store)(next)(action); + expect(nextAction.payload.app).toStrictEqual({ + ...expectedPayloadApp, + previousVersion: DeviceInfo.getVersion(), + }); + }); + }); }); describe('cleanUpState', () => { diff --git a/app/store/utils.js b/app/store/utils.js index 416c959ee..b6e08f14c 100644 --- a/app/store/utils.js +++ b/app/store/utils.js @@ -73,12 +73,14 @@ export function waitForHydration(store, callback) { } export function getStateForReset(initialState, currentState) { + const {app} = currentState; const {currentUserId} = currentState.entities.users; const currentUserProfile = currentState.entities.users.profiles[currentUserId]; const {currentTeamId} = currentState.entities.teams; const preferences = currentState.entities.preferences; const resetState = merge(initialState, { + app, entities: { general: currentState.entities.general, users: { diff --git a/app/store/utils.test.js b/app/store/utils.test.js index 3afd94b70..4dd00a630 100644 --- a/app/store/utils.test.js +++ b/app/store/utils.test.js @@ -20,6 +20,11 @@ describe('getStateForReset', () => { const otherUserId = 'other-user-id'; const currentTeamId = 'current-team-id'; const currentState = { + app: { + build: 'build', + version: 'version', + previousVersion: 'previousVersion', + }, entities: { users: { currentUserId, @@ -80,4 +85,10 @@ describe('getStateForReset', () => { const themeKeys = preferenceKeys.filter((key) => key.startsWith('theme--')); expect(themeKeys.length).toEqual(2); }); + + it('should keep app', () => { + const resetState = getStateForReset(initialState, currentState); + const {app} = resetState; + expect(app).toStrictEqual(currentState.app); + }); }); diff --git a/app/utils/general.js b/app/utils/general.js index b84325c15..76070c21d 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -6,8 +6,6 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import {Posts} from '@mm-redux/constants'; -import {logout} from 'app/actions/views/user'; - const INVALID_VERSIONS = ['1.29.0']; export function fromAutoResponder(post) { @@ -87,9 +85,8 @@ export function isPendingPost(postId, userId) { return postId.startsWith(userId); } -export function validatePreviousVersion(store, version) { - if (INVALID_VERSIONS.includes(version)) { - store.dispatch(logout()); +export function validatePreviousVersion(previousVersion) { + if (!previousVersion || INVALID_VERSIONS.includes(previousVersion)) { return false; }