Fix push notification and emm race conditions (#1691)

* Fix push notification and emm race conditions

* feedback review

* Removed unnecesary code
This commit is contained in:
Elias Nahum 2018-05-22 11:02:14 -04:00 committed by Harrison Healey
parent 04e9c64638
commit 8e0d167f95
12 changed files with 89 additions and 123 deletions

View file

@ -57,8 +57,6 @@
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:noHistory="true"
android:excludeFromRecents="true"
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"

View file

@ -21,7 +21,6 @@ public class InitializationModule extends ReactContextBaseJavaModule {
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
static final String APP_BACKGROUND = "APP_BACKGROUND";
static final String DEVICE_SECURE_CACHE_KEY = "DEVICE_SECURE_CACHE_KEY";
private final Application mApplication;
@ -55,7 +54,6 @@ public class InitializationModule extends ReactContextBaseJavaModule {
* toolbarBackground
* toolbarTextColor
* appBackground
* isDeviceSecure
*
* Miscellaneous:
* MattermostManaged.Config
@ -100,7 +98,6 @@ public class InitializationModule extends ReactContextBaseJavaModule {
keys.add(TOOLBAR_BACKGROUND);
keys.add(TOOLBAR_TEXT_COLOR);
keys.add(APP_BACKGROUND);
keys.add(DEVICE_SECURE_CACHE_KEY);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
@ -119,7 +116,6 @@ public class InitializationModule extends ReactContextBaseJavaModule {
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
String managedConfigResult = asyncStorageResults.get(DEVICE_SECURE_CACHE_KEY);
if (toolbarBackground != null
&& toolbarTextColor != null
@ -133,10 +129,6 @@ public class InitializationModule extends ReactContextBaseJavaModule {
constants.put("themesExist", false);
}
if (managedConfigResult != null) {
constants.put("managedConfigResult", managedConfigResult);
}
if (credentialsExist[0]) {
constants.put("credentialsExist", true);

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* eslint-disable global-require*/
@ -6,10 +6,10 @@ import {AsyncStorage, NativeModules} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {ViewTypes} from 'app/constants';
import mattermostManaged from 'app/mattermost_managed';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
@ -19,7 +19,6 @@ import avoidNativeBridge from 'app/utils/avoid_native_bridge';
const {Initialization} = NativeModules;
const DEVICE_SECURE_CACHE_KEY = 'DEVICE_SECURE_CACHE_KEY';
const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND';
const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR';
const APP_BACKGROUND = 'APP_BACKGROUND';
@ -36,6 +35,7 @@ export default class App {
this.allowOtherServers = true;
this.appStarted = false;
this.emmEnabled = false;
this.performingEMMAuthentication = false;
this.translations = null;
this.toolbarBackground = null;
this.toolbarTextColor = null;
@ -66,28 +66,6 @@ export default class App {
return this.translations;
};
getManagedConfig = async () => {
try {
const shouldStart = await avoidNativeBridge(
() => {
return Initialization.managedConfigResult;
},
() => {
return Initialization.managedConfigResult;
},
() => {
return AsyncStorage.getItem(DEVICE_SECURE_CACHE_KEY);
}
);
if (shouldStart !== null) {
return shouldStart === 'true';
}
} catch (error) {
return false;
}
return false;
};
getAppCredentials = async () => {
try {
const credentials = await avoidNativeBridge(
@ -116,6 +94,8 @@ export default class App {
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
}
}
} catch (error) {
@ -163,8 +143,8 @@ export default class App {
return null;
};
setManagedConfig = (shouldStart) => {
AsyncStorage.setItem(DEVICE_SECURE_CACHE_KEY, shouldStart.toString());
setPerformingEMMAuthentication = (authenticating) => {
this.performingEMMAuthentication = authenticating;
};
setAppCredentials = (deviceToken, currentUserId, token, url) => {
@ -221,37 +201,15 @@ export default class App {
clearNativeCache = () => {
resetGenericPassword();
AsyncStorage.multiRemove([
DEVICE_SECURE_CACHE_KEY,
TOOLBAR_BACKGROUND,
TOOLBAR_TEXT_COLOR,
APP_BACKGROUND,
]);
};
verifyManagedConfigCache = async (shouldStartCache) => {
// since we are caching managed device results
// we should verify after using the cache
const shouldStart = await handleManagedConfig();
if (shouldStartCache && !shouldStart) {
this.setManagedConfig(false);
mattermostManaged.quitApp();
return;
}
this.setManagedConfig(true);
};
launchApp = async () => {
const shouldStartCache = await this.getManagedConfig();
if (shouldStartCache) {
this.startApp();
this.verifyManagedConfigCache(shouldStartCache);
return;
}
const shouldStart = await handleManagedConfig();
if (shouldStart) {
this.setManagedConfig(shouldStart);
this.startApp();
}
};
@ -261,18 +219,13 @@ export default class App {
return;
}
const {dispatch, getState} = store;
const {entities} = getState();
const {dispatch} = store;
let screen = 'SelectServer';
if (entities) {
const {credentials} = entities.general;
if (credentials.token && credentials.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
loadMe()(dispatch, getState);
}
if (this.token && this.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
dispatch(loadMe());
}
switch (screen) {

View file

@ -1,4 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* eslint-disable global-require*/
@ -94,7 +94,9 @@ const initializeModules = () => {
EventEmitter.on(General.CONFIG_CHANGED, handleConfigChanged);
EventEmitter.on(General.DEFAULT_CHANNEL, handleResetChannelDisplayName);
Orientation.addOrientationListener(handleOrientationChange);
mattermostManaged.addEventListener('managedConfigDidChange', handleManagedConfig);
mattermostManaged.addEventListener('managedConfigDidChange', () => {
handleManagedConfig(true);
});
if (config) {
configureAnalytics(config);
@ -135,7 +137,7 @@ const resetBadgeAndVersion = () => {
};
const handleLogout = () => {
app.setAppStarted(false);
app.setAppStarted(true);
app.clearNativeCache();
deleteFileCache();
resetBadgeAndVersion();
@ -207,7 +209,11 @@ const handleOrientationChange = (orientation) => {
}, 100);
};
export const handleManagedConfig = async (serverConfig) => {
export const handleManagedConfig = async (eventFromEmmServer = false) => {
if (app.performingEMMAuthentication) {
return true;
}
const {dispatch, getState} = store;
const state = getState();
@ -235,6 +241,7 @@ export const handleManagedConfig = async (serverConfig) => {
return mattermostManaged.getConfig();
}
);
if (config && Object.keys(config).length) {
app.setEMMEnabled(true);
authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
@ -272,7 +279,7 @@ export const handleManagedConfig = async (serverConfig) => {
}
}
if (authNeeded && !serverConfig) {
if (authNeeded && !eventFromEmmServer) {
const authenticated = await handleAuthentication(vendor);
if (!authenticated) {
return false;
@ -299,6 +306,7 @@ export const handleManagedConfig = async (serverConfig) => {
};
const handleAuthentication = async (vendor) => {
app.setPerformingEMMAuthentication(true);
const isSecured = await mattermostManaged.isDeviceSecure();
const translations = app.getTranslations();
@ -316,6 +324,7 @@ const handleAuthentication = async (vendor) => {
}
}
app.setPerformingEMMAuthentication(false);
return true;
};
@ -391,10 +400,7 @@ const handleAppActive = async () => {
const config = await mattermostManaged.getConfig();
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
if (authNeeded) {
const authenticated = await handleAuthentication(config.vendor);
if (!authenticated) {
mattermostManaged.quitApp();
}
await handleAuthentication(config.vendor);
}
} catch (error) {
// do nothing

View file

@ -34,7 +34,7 @@ export default {
listeners.splice(index, 1);
}
},
authenticate: LocalAuth.authenticate,
authenticate: LocalAuth.auth,
blurAppScreen: MattermostManaged.blurAppScreen,
getConfig: MattermostManaged.getConfig,
getLocalConfig: async () => {

View file

@ -35,7 +35,17 @@ export default {
},
authenticate: LocalAuth.authenticate,
blurAppScreen: BlurAppScreen.enabled,
getConfig: MattermostManaged.getConfig,
getConfig: async () => {
return {
inAppPinCode: 'true',
blurApplicationScreen: 'true',
jailbreakDetection: 'true',
allowOtherServers: 'true',
vendor: 'Local Test',
serverUrl: 'http://192.168.0.18:8065',
username: 'elias',
};
},
getLocalConfig: async () => {
if (!localConfig) {
try {

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {AppRegistry, AppState} from 'react-native';
@ -15,6 +15,9 @@ class PushNotification {
NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => {
this.deviceToken = deviceToken;
if (this.onRegister) {
this.onRegister({token: this.deviceToken});
}
});
NotificationsAndroid.setNotificationReceivedListener((notification) => {
@ -42,13 +45,14 @@ class PushNotification {
data,
text: data.text,
badge: parseInt(data.badge, 10) - parseInt(data.msg_count, 10),
completed: true, // used to identify that the notification belongs to a reply
};
}
});
}
handleNotification = (data, userInteraction) => {
const deviceNotification = {
this.deviceNotification = {
data,
foreground: !userInteraction && AppState.currentState === 'active',
message: data.message,
@ -57,7 +61,7 @@ class PushNotification {
};
if (this.onNotification) {
this.onNotification(deviceNotification);
this.onNotification(this.deviceNotification);
}
};

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {AppState} from 'react-native';
@ -12,6 +12,7 @@ const replies = new Set();
class PushNotification {
constructor() {
this.deviceNotification = null;
this.onRegister = null;
this.onNotification = null;
this.onReply = null;
@ -56,7 +57,7 @@ class PushNotification {
}
handleNotification = (data, foreground, userInteraction) => {
const deviceNotification = {
this.deviceNotification = {
data,
foreground: foreground || (!userInteraction && AppState.currentState === 'active'),
message: data.message,
@ -65,7 +66,7 @@ class PushNotification {
};
if (this.onNotification) {
this.onNotification(deviceNotification);
this.onNotification(this.deviceNotification);
}
};
@ -130,7 +131,7 @@ class PushNotification {
}
getNotification() {
return null;
return this.deviceNotification;
}
resetNotification() {

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* eslint-disable global-require*/
@ -19,10 +19,12 @@ import {
app,
store,
} from 'app/mattermost';
import {loadFromPushNotification} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import PushNotifications from 'app/push_notifications';
import {stripTrailingSlashes} from 'app/utils/url';
import ChannelLoader from 'app/components/channel_loader';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import Loading from 'app/components/loading';
import SafeAreaView from 'app/components/safe_area_view';
@ -116,7 +118,7 @@ export default class Entry extends PureComponent {
this.setAppCredentials();
this.setStartupThemes();
this.setReplyNotifications();
this.handleNotification();
if (Platform.OS === 'android') {
this.launchForAndroid();
@ -173,7 +175,7 @@ export default class Entry extends PureComponent {
);
};
setReplyNotifications = () => {
handleNotification = async () => {
const notification = PushNotifications.getNotification();
// If notification exists, it means that the app was started through a reply
@ -183,16 +185,18 @@ export default class Entry extends PureComponent {
const notificationData = notification || app.replyNotificationData;
const {data, text, badge, completed} = notificationData;
onPushNotificationReply(data, text, badge, completed);
// if the notification has a completed property it means that we are replying to a notification
// and in case it doesn't it means we just opened the notification
if (completed) {
onPushNotificationReply(data, text, badge, completed);
} else {
await store.dispatch(loadFromPushNotification(notification));
}
PushNotifications.resetNotification();
}
};
launchForAndroid = () => {
if (app.startAppFromPushNotification) {
return;
}
app.launchApp();
};
@ -242,6 +246,7 @@ export default class Entry extends PureComponent {
}
let toolbar = null;
let loading = null;
const backgroundColor = app.appBackground ? app.appBackground : '#ffff';
if (app.token && app.toolbarBackground) {
const toolbarTheme = {
@ -258,6 +263,14 @@ export default class Entry extends PureComponent {
/>
</View>
);
loading = (
<View>
<ChannelLoader channelIsLoading={true}/>
</View>
);
} else {
loading = <Loading/>;
}
return (
@ -267,7 +280,7 @@ export default class Entry extends PureComponent {
navigator={navigator}
>
{toolbar}
<Loading/>
{loading}
</SafeAreaView>
);
}

View file

@ -1,7 +1,7 @@
import {
Platform,
InteractionManager,
} from 'react-native';
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Platform} from 'react-native';
import PushNotifications from 'app/push_notifications';
import DeviceInfo from 'react-native-device-info';
@ -18,8 +18,6 @@ import {
loadFromPushNotification,
} from 'app/actions/views/root';
import {stripTrailingSlashes} from 'app/utils/url';
import {
app,
store,
@ -42,16 +40,14 @@ const onRegisterDevice = (data) => {
const token = `${prefix}:${data.token}`;
if (state.views.root.hydrationComplete) {
app.setDeviceToken(token);
setDeviceToken(token)(store.dispatch, store.getState);
store.dispatch(setDeviceToken(token));
} else {
app.setDeviceToken(token);
}
};
const onPushNotification = (deviceNotification) => {
const onPushNotification = async (deviceNotification) => {
const {dispatch, getState} = store;
const state = getState();
const {token, url} = state.entities.general.credentials;
// mark the app as started as soon as possible
if (Platform.OS === 'android' && !app.appStarted) {
@ -69,25 +65,18 @@ const onPushNotification = (deviceNotification) => {
}
if (data.type === 'clear') {
markChannelAsRead(data.channel_id, null, false)(dispatch, getState);
dispatch(markChannelAsRead(data.channel_id, null, false));
} else if (foreground) {
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
} else if (userInteraction && !notification.localNotification) {
EventEmitter.emit('close_channel_drawer');
if (app.startAppFromPushNotification) {
Client4.setToken(token);
Client4.setUrl(stripTrailingSlashes(url));
}
InteractionManager.runAfterInteractions(async () => {
await loadFromPushNotification(notification)(dispatch, getState);
if (app.startAppFromPushNotification) {
app.launchApp();
} else {
if (getState().views.root.hydrationComplete) {
await dispatch(loadFromPushNotification(notification));
if (!app.startAppFromPushNotification) {
EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED);
PushNotifications.resetNotification();
}
});
}
}
};

View file

@ -1,10 +1,9 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* eslint-disable no-unused-vars */
import {AppRegistry, Platform} from 'react-native';
import Mattermost from 'app/mattermost';
import 'app/mattermost';
import ShareExtension from 'share_extension/android';
if (Platform.OS === 'android') {

View file

@ -153,6 +153,7 @@ export default class ExtensionPost extends PureComponent {
if (config) {
const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true';
const vendor = config.vendor || 'Mattermost';
if (authNeeded) {
const isSecured = await mattermostManaged.isDeviceSecure();
if (isSecured) {
@ -166,7 +167,7 @@ export default class ExtensionPost extends PureComponent {
suppressEnterPassword: false,
});
} catch (err) {
return this.onClose();
return this.onClose({nativeEvent: true});
}
}
}