[MM-24426] [MM-24451] Set previous app version in redux store (#4197)

* Set previous app version in redux store

* Handle setting previousVersion on login and clearing data

* Set previous version on logout instead

* Remove action arg
This commit is contained in:
Miguel Alatzar 2020-04-23 02:42:35 -07:00 committed by GitHub
parent b5ef54527f
commit 135dc8c9bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 28 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -101,8 +101,7 @@ export function resetStateForNewVersion(payload) {
rehydrated: true,
},
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
...payload.app,
},
entities: {
channels,

View file

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

View file

@ -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', () => {

View file

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

View file

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

View file

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