From 27411cb119fe86260e340828fa4204d6f830fb51 Mon Sep 17 00:00:00 2001 From: Miguel Alatzar Date: Wed, 22 Apr 2020 08:23:45 -0700 Subject: [PATCH] [MM-24426] Set previous app version in redux store (1.30) (#4196) * Set previous app version in redux store * Update state * Log error --- app/mattermost.js | 10 ++- app/reducers/app/index.js | 2 + app/reducers/app/previousVersion.js | 6 ++ app/store/middleware.js | 31 ++++---- app/store/middleware.test.js | 118 ++++++++++++++++++++++++++++ app/utils/general.js | 12 +-- 6 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 app/reducers/app/previousVersion.js diff --git a/app/mattermost.js b/app/mattermost.js index db48feab4..490538164 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -7,7 +7,7 @@ import {Provider} from 'react-redux'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {loadMe} from 'app/actions/views/user'; +import {loadMe, logout} from 'app/actions/views/user'; import {resetToChannel, resetToSelectServer} from 'app/actions/navigation'; import {setDeepLinkURL} from 'app/actions/views/root'; @@ -24,6 +24,7 @@ import EphemeralStore from 'app/store/ephemeral_store'; import telemetry from 'app/telemetry'; import {validatePreviousVersion} from 'app/utils/general'; import pushNotificationsUtils from 'app/utils/push_notifications'; +import {captureJSException} from 'app/utils/sentry'; const init = async () => { const credentials = await getAppCredentials(); @@ -53,10 +54,15 @@ const launchApp = (credentials) => { if (credentials) { waitForHydration(store, () => { - const valid = validatePreviousVersion(store); + 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 { 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/middleware.js b/app/store/middleware.js index 88381d304..a5762d8d9 100644 --- a/app/store/middleware.js +++ b/app/store/middleware.js @@ -6,8 +6,6 @@ import DeviceInfo from 'react-native-device-info'; import {ViewTypes} from 'app/constants'; import initialState from 'app/initial_state'; -import EphemeralStore from './ephemeral_store'; - import { captureException, LOGGER_JAVASCRIPT_WARNING, @@ -19,21 +17,25 @@ export function messageRetention(store) { const {app} = action.payload; const {entities, views} = action.payload; + 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) { - const version = DeviceInfo.getVersion(); - EphemeralStore.prevAppVersion = version; - action.payload = { - ...action.payload, - app: { - build: DeviceInfo.getBuildNumber(), - version, - }, - }; return next(action); } - EphemeralStore.prevAppVersion = app?.version; - if (app?.version !== DeviceInfo.getVersion() || app?.build !== DeviceInfo.getBuildNumber()) { + if (previousVersion !== version || previousBuild !== build) { // When a new version of the app has been detected return next(resetStateForNewVersion(action)); } @@ -154,8 +156,7 @@ function resetStateForNewVersion(action) { const nextState = { app: { - build: DeviceInfo.getBuildNumber(), - version: DeviceInfo.getVersion(), + ...payload.app, }, entities: { channels, diff --git a/app/store/middleware.test.js b/app/store/middleware.test.js index 55d793097..c00062475 100644 --- a/app/store/middleware.test.js +++ b/app/store/middleware.test.js @@ -3,6 +3,8 @@ /* eslint-disable max-nested-callbacks */ +import DeviceInfo from 'react-native-device-info'; + import assert from 'assert'; import {ViewTypes} from 'app/constants'; @@ -55,6 +57,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: 'persist/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/utils/general.js b/app/utils/general.js index 8982dbb34..a19a180b4 100644 --- a/app/utils/general.js +++ b/app/utils/general.js @@ -6,9 +6,6 @@ import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; import {Posts} from 'mattermost-redux/constants'; -import {logout} from 'app/actions/views/user'; -import EphemeralStore from 'app/store/ephemeral_store'; - const INVALID_VERSIONS = ['1.29.0']; export function fromAutoResponder(post) { @@ -88,12 +85,9 @@ export function isPendingPost(postId, userId) { return postId.startsWith(userId); } -export function validatePreviousVersion(store) { - const version = EphemeralStore.prevAppVersion; - - if (!version || INVALID_VERSIONS.includes(version)) { - console.log('Previous version is no longer valid'); //eslint-disable-line no-console - store.dispatch(logout()); +export function validatePreviousVersion(previousVersion) { + if (!previousVersion || INVALID_VERSIONS.includes(previousVersion)) { + console.log(`Previous version "${previousVersion}" is no longer valid`); //eslint-disable-line no-console return false; }