mattermost-mobile/app/mattermost.js
Miguel Alatzar cbe0c719ac [MM-15874] Add native code to support RNN v2 on Android (#2855)
* Remove fix for MM-9233

* MM-15774 Add native code to support RNN v2 on iOS

* Android changes for RNN v2 upgrade

The activity visibility handling of NotificationsLifecycleFacade
is no longer needed in RNN v2. I've moved the lifecycle callbacks
we use for handling managed config into
ManagedActivityLifecycleCallbacks.java and registered them in
MainApplication's onCreate. Also, I've moved and updated the
loading and getting of the managed config into MainApplication

* Update moduleNames and modulePaths

* Use TAG in restrictionsReceiver

* Set launch screen onCreate

* Comment out registerActivityLifecycleCallbacks for now

* Remove setSoftInputMode call as it's handled in the manifest

* Remove clearHostOnActivityDestroy as that fix is no longer needed

* Rename to canLaunchEntry

* Remove replacement of super.onBackPressed()

* Remove react-navigation from packager files
2019-06-12 15:26:03 -04:00

495 lines
15 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {
Alert,
AppState,
Dimensions,
InteractionManager,
NativeModules,
Platform,
YellowBox,
} from 'react-native';
const {StatusBarManager, MattermostShare, Initialization} = NativeModules;
import DeviceInfo from 'react-native-device-info';
import {Navigation, NativeEventsReceiver} from 'react-native-navigation';
import {Provider} from 'react-redux';
import semver from 'semver';
import {Client4} from 'mattermost-redux/client';
import {setAppState, setServerVersion} from 'mattermost-redux/actions/general';
import {loadMe, logout} from 'mattermost-redux/actions/users';
import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {resetToChannel, resetToSelectServer} from 'app/actions/navigation';
import {selectDefaultChannel} from 'app/actions/views/channel';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device';
import {handleLoginIdChanged} from 'app/actions/views/login';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import {loadConfigAndLicense, startDataCleanup} from 'app/actions/views/root';
import initialState from 'app/initial_state';
import configureStore from 'app/store';
import {NavigationTypes} from 'app/constants';
import mattermostBucket from 'app/mattermost_bucket';
import mattermostManaged from 'app/mattermost_managed';
import {configurePushNotifications} from 'app/utils/push_notifications';
import PushNotifications from 'app/push_notifications';
import {registerScreens} from 'app/screens';
import {deleteFileCache} from 'app/utils/file';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import {t} from 'app/utils/i18n';
import LocalConfig from 'assets/config';
import telemetry from 'app/telemetry';
import App from './app';
import './fetch_preconfig';
const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000;
// Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841)
YellowBox.ignoreWarnings(['Require cycle: node_modules/react-native/Libraries/Network/fetch.js']);
export const app = new App();
export const store = configureStore(initialState);
registerScreens(store, Provider);
const lazyLoadExternalModules = () => {
const StatusBarSizeIOS = require('react-native-status-bar-size');
const initializeErrorHandling = require('app/utils/error_handling').initializeErrorHandling;
return {
StatusBarSizeIOS,
initializeErrorHandling,
};
};
const lazyLoadAnalytics = () => {
const initAnalytics = require('app/utils/segment').init;
return {
initAnalytics,
};
};
export const initializeModules = () => {
const {
StatusBarSizeIOS,
initializeErrorHandling,
} = lazyLoadExternalModules();
const {
config,
} = store.getState().entities.general;
const window = Dimensions.get('window');
initializeErrorHandling();
EventEmitter.on(NavigationTypes.NAVIGATION_RESET, handleLogout);
EventEmitter.on(NavigationTypes.RESTART_APP, restartApp);
EventEmitter.on(General.SERVER_VERSION_CHANGED, handleServerVersionChanged);
EventEmitter.on(General.CONFIG_CHANGED, handleConfigChanged);
EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, handleSwitchToDefaultChannel);
Dimensions.addEventListener('change', handleOrientationChange);
mattermostManaged.addEventListener('managedConfigDidChange', () => {
handleManagedConfig(true);
});
if (config) {
configureAnalytics(config);
}
handleOrientationChange({window});
if (Platform.OS === 'ios') {
StatusBarSizeIOS.addEventListener('willChange', handleStatusBarHeightChange);
StatusBarManager.getHeight(
(data) => {
handleStatusBarHeightChange(data.height);
}
);
}
};
const configureAnalytics = (config) => {
const {
initAnalytics,
} = lazyLoadAnalytics();
if (!__DEV__ && config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) {
initAnalytics(config);
} else {
global.analytics = null;
}
};
const resetBadgeAndVersion = () => {
Client4.serverVersion = '';
Client4.setUserId('');
PushNotifications.setApplicationIconBadgeNumber(0);
PushNotifications.cancelAllLocalNotifications();
store.dispatch(setServerVersion(''));
};
const handleLogout = () => {
Client4.setCSRF(null);
store.dispatch(closeWebSocket(false));
app.setAppStarted(true);
app.clearNativeCache();
deleteFileCache();
resetBadgeAndVersion();
store.dispatch(resetToSelectServer(app.allowOtherServers));
};
const restartApp = async () => {
Navigation.dismissModal({animationType: 'none'});
try {
const window = Dimensions.get('window');
handleOrientationChange({window});
await store.dispatch(loadConfigAndLicense());
await store.dispatch(loadMe());
if (Platform.OS === 'ios') {
StatusBarManager.getHeight(
(data) => {
handleStatusBarHeightChange(data.height);
}
);
}
} catch (e) {
console.warn('Failed to load initial data while restarting', e); // eslint-disable-line no-console
}
store.dispatch(resetToChannel());
};
const handleServerVersionChanged = async (serverVersion) => {
const {dispatch, getState} = store;
const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0];
const translations = app.getTranslations();
const state = getState();
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: handleServerVersionUpgradeNeeded,
}],
{cancelable: false}
);
} else if (state.entities.users && state.entities.users.currentUserId) {
dispatch(setServerVersion(serverVersion));
const data = await dispatch(loadConfigAndLicense());
configureAnalytics(data.config);
}
}
};
const handleConfigChanged = (config) => {
configureAnalytics(config);
};
const handleServerVersionUpgradeNeeded = async () => {
const {dispatch, getState} = store;
resetBadgeAndVersion();
if (getState().entities.general.credentials.token) {
InteractionManager.runAfterInteractions(() => {
dispatch(logout());
});
}
};
const handleStatusBarHeightChange = (nextStatusBarHeight) => {
store.dispatch(setStatusBarHeight(nextStatusBarHeight));
};
const handleOrientationChange = (dimensions) => {
const {dispatch} = store;
if (DeviceInfo.isTablet()) {
dispatch(setDeviceAsTablet());
}
const {height, width} = dimensions.window;
const orientation = height > width ? 'PORTRAIT' : 'LANDSCAPE';
dispatch(setDeviceOrientation(orientation));
dispatch(setDeviceDimensions(height, width));
};
export const handleManagedConfig = async (eventFromEmmServer = false) => {
if (app.performingEMMAuthentication) {
return true;
}
const {dispatch, getState} = store;
const state = getState();
let authNeeded = false;
let blurApplicationScreen = false;
let jailbreakProtection = false;
let vendor = null;
let serverUrl = null;
let username = null;
if (LocalConfig.AutoSelectServerUrl) {
dispatch(handleServerUrlChanged(LocalConfig.DefaultServerUrl));
app.setAllowOtherServers(false);
}
try {
const config = await avoidNativeBridge(
() => {
return true;
},
() => {
return Initialization.managedConfig;
},
() => {
return mattermostManaged.getConfig();
}
);
if (config && Object.keys(config).length) {
app.setEMMEnabled(true);
authNeeded = config.inAppPinCode === 'true';
blurApplicationScreen = config.blurApplicationScreen === 'true';
jailbreakProtection = config.jailbreakProtection === 'true';
vendor = config.vendor || 'Mattermost';
if (!state.entities.general.credentials.token) {
serverUrl = config.serverUrl;
username = config.username;
if (config.allowOtherServers && config.allowOtherServers === 'false') {
app.setAllowOtherServers(false);
}
}
if (jailbreakProtection) {
const isTrusted = mattermostManaged.isTrustedDevice();
if (!isTrusted) {
const translations = app.getTranslations();
Alert.alert(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', vendor),
translations[t('mobile.managed.jailbreak')].replace('{vendor}', vendor),
[{
text: translations[t('mobile.managed.exit')],
style: 'destructive',
onPress: () => {
mattermostManaged.quitApp();
},
}],
{cancelable: false}
);
return false;
}
}
if (authNeeded && !eventFromEmmServer) {
const authenticated = await handleAuthentication(vendor);
if (!authenticated) {
return false;
}
}
if (blurApplicationScreen) {
mattermostManaged.blurAppScreen(true);
}
if (serverUrl) {
dispatch(handleServerUrlChanged(serverUrl));
}
if (username) {
dispatch(handleLoginIdChanged(username));
}
}
} catch (error) {
return true;
}
return true;
};
const handleAuthentication = async (vendor) => {
app.setPerformingEMMAuthentication(true);
const isSecured = await mattermostManaged.isDeviceSecure();
const translations = app.getTranslations();
if (isSecured) {
try {
mattermostBucket.setPreference('emm', vendor);
await mattermostManaged.authenticate({
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', vendor),
fallbackToPasscode: true,
suppressEnterPassword: true,
});
} catch (err) {
mattermostManaged.quitApp();
return false;
}
} else {
await showNotSecuredAlert(vendor);
mattermostManaged.quitApp();
return false;
}
app.setPerformingEMMAuthentication(false);
return true;
};
function showNotSecuredAlert(vendor) {
const translations = app.getTranslations();
return new Promise((resolve) => {
const options = [];
if (Platform.OS === 'android') {
options.push({
text: translations[t('mobile.managed.settings')],
onPress: () => {
mattermostManaged.goToSecuritySettings();
},
});
}
options.push({
text: translations[t('mobile.managed.exit')],
onPress: resolve,
style: 'cancel',
});
Alert.alert(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', vendor),
Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')],
options,
{cancelable: false, onDismiss: resolve},
);
});
}
const handleSwitchToDefaultChannel = (teamId) => {
store.dispatch(selectDefaultChannel(teamId));
};
const handleAppStateChange = (appState) => {
const isActive = appState === 'active';
store.dispatch(setAppState(isActive));
if (isActive) {
handleAppActive();
return;
}
handleAppInActive();
};
const handleAppActive = async () => {
const authExpired = (Date.now() - app.inBackgroundSince) >= AUTHENTICATION_TIMEOUT;
// This handles when the app was started in the background
// cause of an iOS push notification reply
if (Platform.OS === 'ios' && app.shouldRelaunchWhenActive) {
app.launchApp();
app.setShouldRelaunchWhenActive(false);
}
// Once the app becomes active after more than 5 minutes in the background and is controlled by an EMM Provider
if (app.emmEnabled && app.inBackgroundSince && authExpired) {
try {
const config = await mattermostManaged.getConfig();
const authNeeded = config.inAppPinCode === 'true';
if (authNeeded) {
await handleAuthentication(config.vendor);
}
} catch (error) {
// do nothing
}
}
app.setInBackgroundSince(null);
};
const handleAppInActive = () => {
const {dispatch, getState} = store;
const theme = getTheme(getState());
// When the app is sent to the background we set the time when that happens
// and perform a data clean up to improve on performance
app.setInBackgroundSince(Date.now());
app.setStartupThemes(
theme.sidebarHeaderBg,
theme.sidebarHeaderTextColor,
theme.centerChannelBg,
);
dispatch(startDataCleanup());
};
AppState.addEventListener('change', handleAppStateChange);
const launchEntry = () => {
Navigation.events().registerAppLaunchedListener(() => {
telemetry.start([
'start:select_server_screen',
'start:channel_screen',
]);
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Entry',
},
}],
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
},
},
},
},
});
telemetry.startSinceLaunch(['start:splash_screen']);
});
};
configurePushNotifications();
const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened;
const fromPushNotification = Platform.OS === 'android' && Initialization.replyFromPushNotification;
if (startedSharedExtension || fromPushNotification) {
// Hold on launching Entry screen
app.canLaunchEntry(false);
// Listen for when the user opens the app
new NativeEventsReceiver().appLaunched(() => {
app.canLaunchEntry(true);
launchEntry();
});
}
if (app.canLaunchEntry) {
launchEntry();
}