From 89b96d51db1c20cecd31049c2357351488e4db92 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 26 Jun 2019 15:21:20 -0400 Subject: [PATCH] MM-16430 refactor app entry point (#2924) * MM-16430 refactor app entry point * iOS Extensions to use session token from KeyChain * Remove token from general credentials entity * Fix mattermost-managed.ios to return the cachedConfig * Migrate server based keychain for android push notifications * remove unneeded async * Remove unneeded android InitializationModule --- .../rnbeta/InitializationModule.java | 143 ----- .../rnbeta/InitializationPackage.java | 36 -- .../mattermost/rnbeta/MainApplication.java | 1 - .../rnbeta/MattermostCredentialsHelper.java | 41 ++ .../NotificationReplyBroadcastReceiver.java | 17 +- .../mattermost/rnbeta/ReceiptDelivery.java | 19 +- app/actions/views/channel.js | 12 +- app/actions/views/login.js | 5 +- app/actions/views/login.test.js | 6 +- app/app.js | 316 ----------- app/init/credentials.js | 128 +++++ app/init/emm_provider.js | 165 ++++++ app/{fetch_preconfig.js => init/fetch.js} | 3 + app/init/global_event_handler.js | 233 ++++++++ app/mattermost.js | 516 ++---------------- .../mattermost-managed.android.js | 2 + .../mattermost-managed.ios.js | 1 + app/screens/channel/channel_base.js | 7 +- app/screens/entry/entry.js | 321 ----------- app/screens/entry/index.js | 39 -- app/screens/index.js | 2 - app/screens/select_server/select_server.js | 22 +- app/store/ephemeral_store.js | 12 + app/store/index.js | 1 - app/utils/error_handling.js | 85 +-- app/utils/push_notifications.js | 297 +++++----- app/utils/url.js | 3 +- index.js | 44 +- ios/Mattermost/MattermostManaged.m | 1 + .../project.pbxproj | 28 +- .../UploadAttachments/MMKeychainManager.h | 3 + .../UploadAttachments/MMKeychainManager.m | 73 +++ .../UploadAttachments/StoreManager.h | 2 + .../UploadAttachments/StoreManager.m | 13 +- .../UploadAttachments-Bridging-Header.h | 1 + packager/moduleNames.js | 181 +----- packager/modulePaths.js | 165 +----- .../android/extension_post/extension_post.js | 36 +- 38 files changed, 1094 insertions(+), 1886 deletions(-) delete mode 100644 android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java delete mode 100644 android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java delete mode 100644 app/app.js create mode 100644 app/init/credentials.js create mode 100644 app/init/emm_provider.js rename app/{fetch_preconfig.js => init/fetch.js} (97%) create mode 100644 app/init/global_event_handler.js delete mode 100644 app/screens/entry/entry.js delete mode 100644 app/screens/entry/index.js create mode 100644 app/store/ephemeral_store.js create mode 100644 ios/UploadAttachments/UploadAttachments/MMKeychainManager.h create mode 100644 ios/UploadAttachments/UploadAttachments/MMKeychainManager.m diff --git a/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java b/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java deleted file mode 100644 index cf8a489dc..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/InitializationModule.java +++ /dev/null @@ -1,143 +0,0 @@ -package com.mattermost.rnbeta; - -import android.app.Application; -import android.support.annotation.Nullable; - -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeMap; -import com.mattermost.react_native_interface.AsyncStorageHelper; -import com.mattermost.react_native_interface.KeysReadableArray; -import com.mattermost.react_native_interface.ResolvePromise; -import com.oblador.keychain.KeychainModule; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -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"; - - private final Application mApplication; - - public InitializationModule(Application application, ReactApplicationContext reactContext) { - super(reactContext); - mApplication = application; - } - - @Override - public String getName() { - return "Initialization"; - } - - @Nullable - @Override - public Map getConstants() { - Map constants = new HashMap<>(); - - /** - * Package all native module variables in constants - * in order to avoid the native bridge - * - * KeyStore: - * credentialsExist - * deviceToken - * currentUserId - * token - * url - * - * AsyncStorage: - * toolbarBackground - * toolbarTextColor - * appBackground - * - * Miscellaneous: - * MattermostManaged.Config - */ - - MainApplication app = (MainApplication) mApplication; - final Boolean[] credentialsExist = {false}; - final WritableMap[] credentials = {null}; - final Object[] config = {null}; - - // Get KeyStore credentials - KeychainModule module = new KeychainModule(this.getReactApplicationContext()); - module.getGenericPasswordForOptions(null, new ResolvePromise() { - @Override - public void resolve(@Nullable Object value) { - if (value instanceof Boolean && !(Boolean)value) { - credentialsExist[0] = false; - return; - } - - WritableMap map = (WritableMap) value; - if (map != null) { - credentialsExist[0] = true; - credentials[0] = map; - } - } - }); - - // Get managedConfig from MattermostManagedModule - MattermostManagedModule.getInstance().getConfig(new ResolvePromise() { - @Override - public void resolve(@Nullable Object value) { - WritableNativeMap nativeMap = (WritableNativeMap) value; - config[0] = value; - } - }); - - - // Get AsyncStorage key/values - final ArrayList keys = new ArrayList(5); - keys.add(TOOLBAR_BACKGROUND); - keys.add(TOOLBAR_TEXT_COLOR); - keys.add(APP_BACKGROUND); - KeysReadableArray asyncStorageKeys = new KeysReadableArray() { - @Override - public int size() { - return keys.size(); - } - - @Override - public String getString(int index) { - return keys.get(index); - } - }; - - AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext()); - HashMap asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys); - - String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND); - String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR); - String appBackground = asyncStorageResults.get(APP_BACKGROUND); - - if (toolbarBackground != null - && toolbarTextColor != null - && appBackground != null) { - - constants.put("themesExist", true); - constants.put("toolbarBackground", toolbarBackground); - constants.put("toolbarTextColor", toolbarTextColor); - constants.put("appBackground", appBackground); - } else { - constants.put("themesExist", false); - } - - - if (credentialsExist[0]) { - constants.put("credentialsExist", true); - constants.put("credentials", credentials[0]); - } else { - constants.put("credentialsExist", false); - } - - constants.put("managedConfig", config[0]); - - return constants; - } -} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java b/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java deleted file mode 100644 index 79e7e12e7..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/InitializationPackage.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.mattermost.rnbeta; - -import android.app.Application; - -import com.facebook.react.ReactPackage; -import com.facebook.react.bridge.JavaScriptModule; -import com.facebook.react.bridge.NativeModule; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.uimanager.ViewManager; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public class InitializationPackage implements ReactPackage { - - private final Application mApplication; - - public InitializationPackage(Application application) { - mApplication = application; - } - - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new InitializationModule(mApplication, reactContext)); - } - - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); - } -} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index 967d9cd3b..4ac35a02e 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -100,7 +100,6 @@ public class MainApplication extends NavigationApplication implements INotificat new ReactNativeDocumentPicker(), new SharePackage(this), new KeychainPackage(), - new InitializationPackage(this), new AsyncStoragePackage(), new NetInfoPackage(), new RNCWebViewPackage(), diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java new file mode 100644 index 000000000..1c1d331ee --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostCredentialsHelper.java @@ -0,0 +1,41 @@ +package com.mattermost.rnbeta; + +import android.content.Context; + +import java.util.ArrayList; +import java.util.HashMap; + +import com.facebook.react.bridge.ReactApplicationContext; + +import com.oblador.keychain.KeychainModule; + +import com.mattermost.react_native_interface.ResolvePromise; +import com.mattermost.react_native_interface.AsyncStorageHelper; +import com.mattermost.react_native_interface.KeysReadableArray; + +public class MattermostCredentialsHelper { + static final String CURRENT_SERVER_URL = "@currentServerUrl"; + + public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) { + final KeychainModule keychainModule = new KeychainModule(context); + final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context); + final ArrayList keys = new ArrayList(1); + keys.add(CURRENT_SERVER_URL); + KeysReadableArray asyncStorageKeys = new KeysReadableArray() { + @Override + public int size() { + return keys.size(); + } + + @Override + public String getString(int index) { + return keys.get(index); + } + }; + + HashMap asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys); + String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL); + + keychainModule.getGenericPasswordForOptions(serverUrl, promise); + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java index cc99e0316..dd02b935e 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyBroadcastReceiver.java @@ -24,7 +24,6 @@ import okhttp3.Response; import com.mattermost.react_native_interface.ResolvePromise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.KeychainModule; import com.wix.reactnativenotifications.core.NotificationIntentAdapter; @@ -47,27 +46,19 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver { final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); - final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); - keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + + MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() { @Override public void resolve(@Nullable Object value) { if (value instanceof Boolean && !(Boolean)value) { - String channelId = bundle.getString("channel_id"); - onReplyFailed(notificationManager, notificationId, channelId); return; } WritableMap map = (WritableMap) value; if (map != null) { - String[] credentials = map.getString("password").split(",[ ]*"); - String token = null; - String serverUrl = null; - if (credentials.length == 2) { - token = credentials[0]; - serverUrl = credentials[1]; - - } + String token = map.getString("password"); + String serverUrl = map.getString("service"); Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token)); replyToMessage(serverUrl, token, notificationId, message); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java index 64ec0d96f..a276fec0a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/ReceiptDelivery.java @@ -15,17 +15,18 @@ import okhttp3.Response; import org.json.JSONObject; import org.json.JSONException; -import com.mattermost.react_native_interface.ResolvePromise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; -import com.oblador.keychain.KeychainModule; + +import com.mattermost.react_native_interface.ResolvePromise; public class ReceiptDelivery { + static final String CURRENT_SERVER_URL = "@currentServerUrl"; + public static void send (Context context, final String ackId, final String type) { final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context); - final KeychainModule keychainModule = new KeychainModule(reactApplicationContext); - keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() { + MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() { @Override public void resolve(@Nullable Object value) { if (value instanceof Boolean && !(Boolean)value) { @@ -34,14 +35,8 @@ public class ReceiptDelivery { WritableMap map = (WritableMap) value; if (map != null) { - String[] credentials = map.getString("password").split(",[ ]*"); - String token = null; - String serverUrl = null; - if (credentials.length == 2) { - token = credentials[0]; - serverUrl = credentials[1]; - - } + String token = map.getString("password"); + String serverUrl = map.getString("service"); Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token)); execute(serverUrl, token, ackId, type); diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 511f5a8b6..32418fd2e 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -21,7 +21,7 @@ import { } from 'mattermost-redux/actions/posts'; import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences} from 'mattermost-redux/actions/preferences'; -import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; +import {getTeamMembersByIds, selectTeam} from 'mattermost-redux/actions/teams'; import {getProfilesInChannel} from 'mattermost-redux/actions/users'; import {General, Preferences} from 'mattermost-redux/constants'; import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts'; @@ -54,8 +54,8 @@ import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels' const MAX_POST_TRIES = 3; export function loadChannelsIfNecessary(teamId) { - return async (dispatch, getState) => { - await fetchMyChannelsAndMembers(teamId)(dispatch, getState); + return async (dispatch) => { + await dispatch(fetchMyChannelsAndMembers(teamId)); }; } @@ -416,6 +416,12 @@ export function handleSelectChannelByName(channelName, teamName) { const currentTeamName = currentTeam?.name; const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName)); const currentChannelId = getCurrentChannelId(state); + + if (teamName && teamName !== currentTeamName) { + const team = getTeamByName(state, teamName); + dispatch(selectTeam(team)); + } + if (channel && currentChannelId !== channel.id) { dispatch(handleSelectChannel(channel.id)); } diff --git a/app/actions/views/login.js b/app/actions/views/login.js index 740659e4d..f84d97b9d 100644 --- a/app/actions/views/login.js +++ b/app/actions/views/login.js @@ -11,7 +11,7 @@ import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {ViewTypes} from 'app/constants'; -import {app} from 'app/mattermost'; +import {setAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; import {getDeviceTimezone} from 'app/utils/timezone'; import {setCSRFFromCookie} from 'app/utils/security'; @@ -45,7 +45,7 @@ export function handleSuccessfulLogin() { const currentUserId = getCurrentUserId(state); await setCSRFFromCookie(url); - app.setAppCredentials(deviceToken, currentUserId, token, url); + setAppCredentials(deviceToken, currentUserId, token, url); const enableTimezone = isTimezoneEnabled(state); if (enableTimezone) { @@ -56,7 +56,6 @@ export function handleSuccessfulLogin() { type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: { url, - token, }, }); diff --git a/app/actions/views/login.test.js b/app/actions/views/login.test.js index 66020d79c..f75dee2f2 100644 --- a/app/actions/views/login.test.js +++ b/app/actions/views/login.test.js @@ -11,10 +11,8 @@ import { handlePasswordChanged, } from 'app/actions/views/login'; -jest.mock('app/mattermost', () => ({ - app: { - setAppCredentials: () => jest.fn(), - }, +jest.mock('app/init/credentials', () => ({ + setAppCredentials: () => jest.fn(), })); jest.mock('react-native-cookies', () => ({ diff --git a/app/app.js b/app/app.js deleted file mode 100644 index 1011b164d..000000000 --- a/app/app.js +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/* eslint-disable global-require*/ -import {Linking, NativeModules, Platform, Text} from 'react-native'; -import AsyncStorage from '@react-native-community/async-storage'; -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 {setDeepLinkURL} from 'app/actions/views/root'; -import {ViewTypes} from 'app/constants'; -import tracker from 'app/utils/time_tracker'; -import {getCurrentLocale} from 'app/selectors/i18n'; - -import {getTranslations as getLocalTranslations} from 'app/i18n'; -import {store, handleManagedConfig} from 'app/mattermost'; -import avoidNativeBridge from 'app/utils/avoid_native_bridge'; -import {setCSRFFromCookie} from 'app/utils/security'; - -const {Initialization} = NativeModules; - -const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND'; -const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR'; -const APP_BACKGROUND = 'APP_BACKGROUND'; - -export default class App { - constructor() { - // Usage: app.js - this.shouldRelaunchWhenActive = false; - this.inBackgroundSince = null; - this.previousAppState = null; - - // Usage: screen/entry.js - this.startAppFromPushNotification = false; - this.isNotificationsConfigured = false; - this.allowOtherServers = true; - this.appStarted = false; - this.emmEnabled = false; - this.performingEMMAuthentication = false; - this.translations = null; - this.toolbarBackground = null; - this.toolbarTextColor = null; - this.appBackground = null; - - // Usage utils/push_notifications.js - this.replyNotificationData = null; - this.deviceToken = null; - - // Usage credentials - this.currentUserId = null; - this.token = null; - this.url = null; - - // Usage deeplinking - Linking.addEventListener('url', this.handleDeepLink); - - this.setFontFamily(); - this.getStartupThemes(); - this.getAppCredentials(); - } - - setFontFamily = () => { - // Set a global font for Android - if (Platform.OS === 'android') { - const defaultFontFamily = { - style: { - fontFamily: 'Roboto', - }, - }; - const TextRender = Text.render; - const initialDefaultProps = Text.defaultProps; - Text.defaultProps = { - ...initialDefaultProps, - ...defaultFontFamily, - }; - Text.render = function render(props, ...args) { - const oldProps = props; - let newProps = {...props, style: [defaultFontFamily.style, props.style]}; - try { - return Reflect.apply(TextRender, this, [newProps, ...args]); - } finally { - newProps = oldProps; - } - }; - } - }; - - getTranslations = () => { - if (this.translations) { - return this.translations; - } - - const state = store.getState(); - const locale = getCurrentLocale(state); - - this.translations = getLocalTranslations(locale); - return this.translations; - }; - - getAppCredentials = async () => { - try { - const credentials = await avoidNativeBridge( - () => { - return Initialization.credentialsExist; - }, - () => { - return Initialization.credentials; - }, - () => { - this.waitForRehydration = true; - return getGenericPassword(); - } - ); - - if (credentials) { - const usernameParsed = credentials.username.split(','); - const passwordParsed = credentials.password.split(','); - - // username == deviceToken, currentUserId - // password == token, url - if (usernameParsed.length === 2 && passwordParsed.length === 2) { - const [deviceToken, currentUserId] = usernameParsed; - const [token, url] = passwordParsed; - - // if for any case the url and the token aren't valid proceed with re-hydration - if (url && url !== 'undefined' && token && token !== 'undefined') { - this.deviceToken = deviceToken; - this.currentUserId = currentUserId; - this.token = token; - this.url = url; - Client4.setUrl(url); - Client4.setToken(token); - await setCSRFFromCookie(url); - } else { - this.waitForRehydration = true; - } - } - } else { - this.waitForRehydration = false; - } - } catch (error) { - return null; - } - - return null; - }; - - getStartupThemes = async () => { - try { - const [ - toolbarBackground, - toolbarTextColor, - appBackground, - ] = await avoidNativeBridge( - () => { - return Initialization.themesExist; - }, - () => { - return [ - Initialization.toolbarBackground, - Initialization.toolbarTextColor, - Initialization.appBackground, - ]; - }, - () => { - return Promise.all([ - AsyncStorage.getItem(TOOLBAR_BACKGROUND), - AsyncStorage.getItem(TOOLBAR_TEXT_COLOR), - AsyncStorage.getItem(APP_BACKGROUND), - ]); - } - ); - - if (toolbarBackground) { - this.toolbarBackground = toolbarBackground; - this.toolbarTextColor = toolbarTextColor; - this.appBackground = appBackground; - } - } catch (error) { - return null; - } - - return null; - }; - - setPerformingEMMAuthentication = (authenticating) => { - this.performingEMMAuthentication = authenticating; - }; - - setAppCredentials = (deviceToken, currentUserId, token, url) => { - if (!currentUserId) { - return; - } - - const username = `${deviceToken}, ${currentUserId}`; - const password = `${token},${url}`; - - this.token = token; - this.url = url; - - if (this.waitForRehydration) { - this.waitForRehydration = false; - } - - // Only save to keychain if the url and token are set - if (url && token) { - try { - setGenericPassword(username, password); - } catch (e) { - console.warn('could not set credentials', e); //eslint-disable-line no-console - } - } - }; - - setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => { - AsyncStorage.setItem(TOOLBAR_BACKGROUND, toolbarBackground); - AsyncStorage.setItem(TOOLBAR_TEXT_COLOR, toolbarTextColor); - AsyncStorage.setItem(APP_BACKGROUND, appBackground); - }; - - setStartAppFromPushNotification = (startAppFromPushNotification) => { - this.startAppFromPushNotification = startAppFromPushNotification; - }; - - setIsNotificationsConfigured = (isNotificationsConfigured) => { - this.isNotificationsConfigured = isNotificationsConfigured; - }; - - setAllowOtherServers = (allowOtherServers) => { - this.allowOtherServers = allowOtherServers; - }; - - setAppStarted = (appStarted) => { - this.appStarted = appStarted; - }; - - setEMMEnabled = (emmEnabled) => { - this.emmEnabled = emmEnabled; - }; - - setDeviceToken = (deviceToken) => { - this.deviceToken = deviceToken; - }; - - setReplyNotificationData = (replyNotificationData) => { - this.replyNotificationData = replyNotificationData; - }; - - setInBackgroundSince = (inBackgroundSince) => { - this.inBackgroundSince = inBackgroundSince; - }; - - setShouldRelaunchWhenActive = (shouldRelaunchWhenActive) => { - this.shouldRelaunchWhenActive = shouldRelaunchWhenActive; - }; - - clearNativeCache = () => { - resetGenericPassword(); - AsyncStorage.multiRemove([ - TOOLBAR_BACKGROUND, - TOOLBAR_TEXT_COLOR, - APP_BACKGROUND, - ]); - }; - - handleDeepLink = (event) => { - const {url} = event; - store.dispatch(setDeepLinkURL(url)); - } - - launchApp = async () => { - const shouldStart = await handleManagedConfig(); - if (shouldStart) { - this.startApp(); - } - }; - - startApp = () => { - if (this.appStarted || this.waitForRehydration) { - return; - } - - const {dispatch} = store; - - Linking.getInitialURL().then((url) => { - dispatch(setDeepLinkURL(url)); - }); - - let screen = 'SelectServer'; - if (this.token && this.url) { - screen = 'Channel'; - tracker.initialLoad = Date.now(); - - try { - dispatch(loadMe()); - } catch (e) { - // Fall through since we should have a previous version of the current user because we have a token - console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console - } - } - - switch (screen) { - case 'SelectServer': - EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true); - break; - case 'Channel': - EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true); - break; - } - - this.setAppStarted(true); - } -} diff --git a/app/init/credentials.js b/app/init/credentials.js new file mode 100644 index 000000000..0f4ee7753 --- /dev/null +++ b/app/init/credentials.js @@ -0,0 +1,128 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import AsyncStorage from '@react-native-community/async-storage'; +import * as KeyChain from 'react-native-keychain'; + +import {Client4} from 'mattermost-redux/client'; + +import mattermostManaged from 'app/mattermost_managed'; +import ephemeralStore from 'app/store/ephemeral_store'; +import {setCSRFFromCookie} from 'app/utils/security'; + +const CURRENT_SERVER = '@currentServerUrl'; + +export const getCurrentServerUrl = async () => { + return AsyncStorage.getItem(CURRENT_SERVER); +}; + +export const setAppCredentials = (deviceToken, currentUserId, token, url) => { + if (!currentUserId) { + return; + } + + // Only save to keychain if the url and token are set + if (url && token) { + try { + const username = `${deviceToken}, ${currentUserId}`; + + ephemeralStore.deviceToken = deviceToken; + AsyncStorage.setItem(CURRENT_SERVER, url); + KeyChain.setInternetCredentials(url, username, token, {accessGroup: mattermostManaged.appGroupIdentifier}); + } catch (e) { + console.warn('could not set credentials', e); //eslint-disable-line no-console + } + } +}; + +export const getAppCredentials = async () => { + const serverUrl = await AsyncStorage.getItem(CURRENT_SERVER); + + if (serverUrl) { + return getInternetCredentials(serverUrl); + } + + return getCredentialsFromGenericKeyChain(); +}; + +export const removeAppCredentials = async () => { + const url = await getCurrentServerUrl(); + + Client4.setCSRF(null); + Client4.serverVersion = ''; + Client4.setUserId(''); + Client4.setToken(''); + Client4.setUrl(''); + + if (url) { + KeyChain.resetInternetCredentials(url); + } + + KeyChain.resetGenericPassword(); + AsyncStorage.removeItem(CURRENT_SERVER); +}; + +async function getCredentialsFromGenericKeyChain() { + try { + const credentials = await KeyChain.getGenericPassword(); + + if (credentials) { + const usernameParsed = credentials.username.split(','); + const passwordParsed = credentials.password.split(','); + + // username == deviceToken, currentUserId + // password == token, url + if (usernameParsed.length === 2 && passwordParsed.length === 2) { + const [deviceToken, currentUserId] = usernameParsed; + const [token, url] = passwordParsed; + + // if for any case the url and the token aren't valid proceed with re-hydration + if (url && url !== 'undefined' && token && token !== 'undefined') { + Client4.setUserId(currentUserId); + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); + + // Migration: remove the generic credentials and add a server specific one + setAppCredentials(deviceToken, currentUserId, token, url); + KeyChain.resetGenericPassword(); + + return { + username: usernameParsed, + password: token, + }; + } + } + } + + return null; + } catch (e) { + return null; + } +} + +async function getInternetCredentials(url) { + try { + const credentials = await KeyChain.getInternetCredentials(url); + + if (credentials) { + const usernameParsed = credentials.username.split(','); + const token = credentials.password; + const [deviceToken, currentUserId] = usernameParsed; + + if (token && token !== 'undefined') { + ephemeralStore.deviceToken = deviceToken; + Client4.setUserId(currentUserId); + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); + + return credentials; + } + } + + return null; + } catch (e) { + return null; + } +} diff --git a/app/init/emm_provider.js b/app/init/emm_provider.js new file mode 100644 index 000000000..77dfad013 --- /dev/null +++ b/app/init/emm_provider.js @@ -0,0 +1,165 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert, Platform} from 'react-native'; + +import {handleLoginIdChanged} from 'app/actions/views/login'; +import {handleServerUrlChanged} from 'app/actions/views/select_server'; +import {getTranslations} from 'app/i18n'; +import mattermostBucket from 'app/mattermost_bucket'; +import mattermostManaged from 'app/mattermost_managed'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import {t} from 'app/utils/i18n'; + +import {getAppCredentials} from './credentials'; + +import LocalConfig from 'assets/config'; + +class EMMProvider { + constructor() { + this.enabled = false; + this.performingAuthentication = false; + this.previousAppState = null; + + this.inAppPinCode = false; + this.blurApplicationScreen = false; + this.jailbreakProtection = false; + this.vendor = null; + + this.allowOtherServers = true; + this.emmServerUrl = null; + this.emmUsername = null; + } + + checkIfDeviceIsTrusted = (store) => { + const isTrusted = mattermostManaged.isTrustedDevice(); + + if (!isTrusted) { + const state = store.getState(); + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + Alert.alert( + translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor), + translations[t('mobile.managed.jailbreak')].replace('{vendor}', this.vendor), + [{ + text: translations[t('mobile.managed.exit')], + style: 'destructive', + onPress: () => { + mattermostManaged.quitApp(); + }, + }], + {cancelable: false} + ); + } + }; + + handleAuthentication = async (store, prompt = true) => { + this.performingAuthentication = true; + const isSecured = await mattermostManaged.isDeviceSecure(); + const state = store.getState(); + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + + if (isSecured) { + try { + mattermostBucket.setPreference('emm', this.vendor); + if (prompt) { + await mattermostManaged.authenticate({ + reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor), + fallbackToPasscode: true, + suppressEnterPassword: true, + }); + } + } catch (err) { + mattermostManaged.quitApp(); + return false; + } + } else { + await this.showNotSecuredAlert(translations); + + mattermostManaged.quitApp(); + return false; + } + + this.setPerformingAuthentication(false); + return true; + }; + + handleManagedConfig = async (store) => { + if (this.performingAuthentication) { + return true; + } + + const {dispatch} = store; + + if (LocalConfig.AutoSelectServerUrl) { + dispatch(handleServerUrlChanged(LocalConfig.DefaultServerUrl)); + this.allowOtherServers = false; + } + + const managedConfig = await mattermostManaged.getConfig(); + + if (managedConfig && Object.keys(managedConfig).length) { + this.enabled = true; + this.inAppPinCode = managedConfig.inAppPinCode === 'true'; + + this.blurApplicationScreen = managedConfig.blurApplicationScreen === 'true'; + this.jailbreakProtection = managedConfig.jailbreakProtection === 'true'; + this.vendor = managedConfig.vendor || 'Mattermost'; + + const credentials = await getAppCredentials(); + if (!credentials) { + this.emmServerUrl = managedConfig.serverUrl; + this.emmUsername = managedConfig.username; + + if (managedConfig.allowOtherServers && managedConfig.allowOtherServers === 'false') { + this.allowOtherServers = false; + } + } + + if (this.blurApplicationScreen) { + mattermostManaged.blurAppScreen(true); + } + + if (this.emmServerUrl) { + dispatch(handleServerUrlChanged(this.emmServerUrl)); + } + + if (this.emmUsername) { + dispatch(handleLoginIdChanged(this.emmUsername)); + } + } + + return true; + }; + + showNotSecuredAlert = (translations) => { + 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}', this.vendor), + Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')], + options, + {cancelable: false, onDismiss: resolve}, + ); + }); + } +} + +export default new EMMProvider(); diff --git a/app/fetch_preconfig.js b/app/init/fetch.js similarity index 97% rename from app/fetch_preconfig.js rename to app/init/fetch.js index f5f74050f..57cd167a4 100644 --- a/app/fetch_preconfig.js +++ b/app/init/fetch.js @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import {Platform} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; import RNFetchBlob from 'rn-fetch-blob'; import urlParse from 'url-parse'; @@ -146,6 +147,8 @@ const initFetchConfig = async () => { // no managed config } + Client4.setUserAgent(DeviceInfo.getUserAgent()); + if (Platform.OS === 'ios') { const certificate = await mattermostBucket.getPreference('cert'); fetchConfig = { diff --git a/app/init/global_event_handler.js b/app/init/global_event_handler.js new file mode 100644 index 000000000..73bc3d04c --- /dev/null +++ b/app/init/global_event_handler.js @@ -0,0 +1,233 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Alert, AppState, Dimensions, Linking, NativeModules, Platform} from 'react-native'; +import DeviceInfo from 'react-native-device-info'; +import semver from 'semver'; + +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 {Client4} from 'mattermost-redux/client'; +import {General} from 'mattermost-redux/constants'; +import EventEmitter from 'mattermost-redux/utils/event_emitter'; + +import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device'; +import {selectDefaultChannel} from 'app/actions/views/channel'; +import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root'; +import {NavigationTypes} from 'app/constants'; +import {getTranslations} from 'app/i18n'; +import mattermostManaged from 'app/mattermost_managed'; +import PushNotifications from 'app/push_notifications'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import {t} from 'app/utils/i18n'; +import {deleteFileCache} from 'app/utils/file'; + +import LocalConfig from 'assets/config'; + +import {getAppCredentials, removeAppCredentials} from './credentials'; +import emmProvider from './emm_provider'; + +const {StatusBarManager} = NativeModules; +const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 1000; + +class GlobalEventHandler { + constructor() { + EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.onLogout); + EventEmitter.on(NavigationTypes.RESTART_APP, this.onRestartApp); + EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged); + EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged); + EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel); + Dimensions.addEventListener('change', this.onOrientationChange); + AppState.addEventListener('change', this.onAppStateChange); + Linking.addEventListener('url', this.onDeepLink); + } + + appActive = async () => { + // if the app is being controlled by an EMM provider + if (emmProvider.enabled && emmProvider.inAppPinCode) { + const authExpired = (Date.now() - emmProvider.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; + + // Once the app becomes active we check if the device needs to have a passcode set + const prompt = emmProvider.inBackgroundSince && authExpired; // if more than 5 minutes have passed prompt for passcode + await emmProvider.handleAuthentication(this.store, prompt); + } + + emmProvider.inBackgroundSince = null; + }; + + appInactive = () => { + const {dispatch} = this.store; + + // 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 + emmProvider.inBackgroundSince = Date.now(); + + dispatch(startDataCleanup()); + }; + + configure = (opts) => { + this.store = opts.store; + this.launchEntry = opts.launchEntry; + + const window = Dimensions.get('window'); + this.onOrientationChange({window}); + + this.StatusBarSizeIOS = require('react-native-status-bar-size'); + if (Platform.OS === 'ios') { + this.StatusBarSizeIOS.addEventListener('willChange', this.onStatusBarHeightChange); + + StatusBarManager.getHeight( + (data) => { + this.onStatusBarHeightChange(data.height); + } + ); + } + + this.JavascriptAndNativeErrorHandler = require('app/utils/error_handling').default; + this.JavascriptAndNativeErrorHandler.initializeErrorHandling(this.store); + + mattermostManaged.addEventListener('managedConfigDidChange', this.onManagedConfigurationChange); + }; + + configureAnalytics = (config) => { + const initAnalytics = require('app/utils/segment').init; + + if (!__DEV__ && config && config.DiagnosticsEnabled === 'true' && config.DiagnosticId && LocalConfig.SegmentApiKey) { + initAnalytics(config); + } else { + global.analytics = null; + } + }; + + onAppStateChange = (appState) => { + const isActive = appState === 'active'; + const isBackground = appState === 'background'; + + if (this.store) { + this.store.dispatch(setAppState(isActive)); + } + + if (isActive && emmProvider.previousAppState === 'background') { + this.appActive(); + } else if (isBackground) { + this.appInactive(); + } + + emmProvider.previousAppState = appState; + }; + + onDeepLink = (event) => { + const {url} = event; + this.store.dispatch(setDeepLinkURL(url)); + }; + + onManagedConfigurationChange = () => { + emmProvider.handleManagedConfig(this.store, true); + }; + + onServerConfigChanged = (config) => { + this.configureAnalytics(config); + }; + + onLogout = async () => { + this.store.dispatch(closeWebSocket(false)); + this.store.dispatch(setServerVersion('')); + deleteFileCache(); + removeAppCredentials(); + + PushNotifications.setApplicationIconBadgeNumber(0); + PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server + + if (this.launchEntry) { + this.launchEntry(); + } + }; + + onOrientationChange = (dimensions) => { + if (this.store) { + const {dispatch} = this.store; + if (DeviceInfo.isTablet()) { + dispatch(setDeviceAsTablet()); + } + + const {height, width} = dimensions.window; + const orientation = height > width ? 'PORTRAIT' : 'LANDSCAPE'; + + dispatch(setDeviceOrientation(orientation)); + dispatch(setDeviceDimensions(height, width)); + } + }; + + onRestartApp = async () => { + await this.store.dispatch(loadConfigAndLicense()); + await this.store.dispatch(loadMe()); + + const window = Dimensions.get('window'); + this.onOrientationChange({window}); + + if (Platform.OS === 'ios') { + StatusBarManager.getHeight( + (data) => { + this.onStatusBarHeightChange(data.height); + } + ); + } + + if (this.launchEntry) { + const credentials = await getAppCredentials(); + this.launchEntry(credentials); + } + }; + + onServerVersionChanged = async (serverVersion) => { + const {dispatch, getState} = this.store; + const state = getState(); + const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; + const locale = getCurrentLocale(state); + const translations = getTranslations(locale); + + 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: this.serverUpgradeNeeded, + }], + {cancelable: false} + ); + } else if (state.entities.users && state.entities.users.currentUserId) { + dispatch(setServerVersion(serverVersion)); + const data = await dispatch(loadConfigAndLicense()); + this.configureAnalytics(data.config); + } + } + }; + + onStatusBarHeightChange = (nextStatusBarHeight) => { + this.store.dispatch(setStatusBarHeight(nextStatusBarHeight)); + }; + + onSwitchToDefaultChannel = (teamId) => { + this.store.dispatch(selectDefaultChannel(teamId)); + }; + + serverUpgradeNeeded = async () => { + const {dispatch} = this.store; + + dispatch(setServerVersion('')); + Client4.serverVersion = ''; + PushNotifications.setApplicationIconBadgeNumber(0); + PushNotifications.cancelAllLocalNotifications(); // TODO: Only cancel the notification that belongs to this server + + const credentials = await getAppCredentials(); + + if (credentials) { + dispatch(logout()); + } + }; +} + +export default new GlobalEventHandler(); diff --git a/app/mattermost.js b/app/mattermost.js index 93338f7fe..9ad9e2190 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -1,389 +1,47 @@ // 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 {Linking, NativeModules, Platform} from 'react-native'; 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 {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 {loadMe} from 'mattermost-redux/actions/users'; +import {setDeepLinkURL} 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 {getAppCredentials} from 'app/init/credentials'; +import emmProvider from 'app/init/emm_provider'; +import 'app/init/fetch'; +import globalEventHandler from 'app/init/global_event_handler'; 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 configureStore from 'app/store'; +import ephemeralStore from 'app/store/ephemeral_store'; import telemetry from 'app/telemetry'; +import pushNotificationsUtils from 'app/utils/push_notifications'; -import App from './app'; -import './fetch_preconfig'; - -const PROMPT_IN_APP_PIN_CODE_AFTER = 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(); +const {MattermostShare} = NativeModules; +const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened; 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, - }; -}; - -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); +const init = async () => { + const credentials = await getAppCredentials(); + pushNotificationsUtils.configure(store); + globalEventHandler.configure({ + store, + launchEntry, }); - if (config) { - configureAnalytics(config); + registerScreens(store, Provider); + + if (startedSharedExtension) { + ephemeralStore.appStarted = true; } - handleOrientationChange({window}); - - if (Platform.OS === 'ios') { - StatusBarSizeIOS.addEventListener('willChange', handleStatusBarHeightChange); - - StatusBarManager.getHeight( - (data) => { - handleStatusBarHeightChange(data.height); - } - ); + if (!ephemeralStore.appStarted) { + launchEntryAndAuthenticateIfNeeded(credentials); } }; -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(); - launchSelectServer(); -}; - -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 - } - - launchChannel(); -}; - -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, prompt = true) => { - app.setPerformingEMMAuthentication(true); - const isSecured = await mattermostManaged.isDeviceSecure(); - const translations = app.getTranslations(); - - if (isSecured) { - try { - mattermostBucket.setPreference('emm', vendor); - if (prompt) { - 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, translations); - - mattermostManaged.quitApp(); - return false; - } - - app.setPerformingEMMAuthentication(false); - return true; -}; - -function showNotSecuredAlert(vendor, translations) { - 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 launchSelectServer = () => { Navigation.startSingleScreenApp({ screen: { @@ -396,7 +54,7 @@ const launchSelectServer = () => { }, }, passProps: { - allowOtherServers: app.allowOtherServers, + allowOtherServers: emmProvider.allowOtherServers, }, appStyle: { orientation: 'auto', @@ -415,9 +73,9 @@ const launchChannel = (skipMetrics = false) => { statusBarHideWithNavBar: false, screenBackgroundColor: 'transparent', }, - passProps: { - skipMetrics, - }, + }, + passProps: { + skipMetrics, }, appStyle: { orientation: 'auto', @@ -426,108 +84,52 @@ const launchChannel = (skipMetrics = false) => { }); }; -const handleAppStateChange = (appState) => { - const isActive = appState === 'active'; - const isBackground = appState === 'background'; - - store.dispatch(setAppState(isActive)); - - if (isActive && app.previousAppState === 'background') { - handleAppActive(); - } else if (isBackground) { - handleAppInActive(); - } - - app.previousAppState = appState; -}; - -const handleAppActive = async () => { - // 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); - } - - // if the app is being controlled by an EMM provider - if (app.emmEnabled) { - const config = await mattermostManaged.getConfig(); - const authNeeded = config.inAppPinCode === 'true'; - const authExpired = (Date.now() - app.inBackgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER; - - // Once the app becomes active we check if the device needs to have a passcode set - if (authNeeded) { - const prompt = app.inBackgroundSince && authExpired; // if more than 5 minutes have passed prompt for passcode - await handleAuthentication(config.vendor, prompt); - } - } - - 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 = () => { +const launchEntry = (credentials) => { telemetry.start([ 'start:select_server_screen', 'start:channel_screen', ]); - Navigation.startSingleScreenApp({ - screen: { - screen: 'Entry', - navigatorStyle: { - navBarHidden: true, - statusBarHidden: false, - statusBarHideWithNavBar: false, - }, - }, - passProps: { - initializeModules, - }, - appStyle: { - orientation: 'auto', - }, - animationType: 'fade', - }); + if (credentials) { + store.dispatch(loadMe()); + launchChannel(); + } else { + launchSelectServer(); + } telemetry.startSinceLaunch(['start:splash_screen']); + ephemeralStore.appStarted = true; }; -configurePushNotifications(); -const startedSharedExtension = Platform.OS === 'android' && MattermostShare.isOpened; +const launchEntryAndAuthenticateIfNeeded = async (credentials) => { + await emmProvider.handleManagedConfig(store); + launchEntry(credentials); -if (startedSharedExtension) { - // Hold on launching Entry screen - app.setAppStarted(true); -} + if (emmProvider.enabled) { + if (emmProvider.jailbreakProtection) { + emmProvider.checkIfDeviceIsTrusted(); + } -if (!app.appStarted) { - launchEntry(); -} + if (emmProvider.inAppPinCode) { + await emmProvider.handleAuthentication(store); + } + } -new NativeEventsReceiver().appLaunched(() => { + Linking.getInitialURL().then((url) => { + store.dispatch(setDeepLinkURL(url)); + }); +}; + +new NativeEventsReceiver().appLaunched(async () => { + const credentials = await getAppCredentials(); if (startedSharedExtension) { - app.setAppStarted(false); - launchEntry(); - } else if (app.token && app.url) { + ephemeralStore.appStarted = true; + await launchEntryAndAuthenticateIfNeeded(credentials); + } else if (credentials) { launchChannel(true); } else { launchSelectServer(); } }); + +init(); diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js index 3cdd00095..778396a9f 100644 --- a/app/mattermost_managed/mattermost-managed.android.js +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -36,6 +36,8 @@ export default { }, authenticate: LocalAuth.auth, blurAppScreen: MattermostManaged.blurAppScreen, + appGroupIdentifier: null, + hasSafeAreaInsets: null, isRunningInSplitView: MattermostManaged.isRunningInSplitView, getConfig: async () => { try { diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js index 9c2415da2..8073f9a26 100644 --- a/app/mattermost_managed/mattermost-managed.ios.js +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -51,6 +51,7 @@ export default { getCachedConfig: () => { return cachedConfig; }, + appGroupIdentifier: MattermostManaged.appGroupIdentifier, hasSafeAreaInsets: MattermostManaged.hasSafeAreaInsets, isRunningInSplitView: MattermostManaged.isRunningInSplitView, isDeviceSecure: async () => { diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index 7dd030714..f370f09f7 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -14,8 +14,6 @@ import { import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {app} from 'app/mattermost'; - import EmptyToolbar from 'app/components/start/empty_toolbar'; import InteractiveDialogController from 'app/components/interactive_dialog_controller'; import MainSidebar from 'app/components/sidebars/main'; @@ -24,6 +22,7 @@ import SettingsSidebar from 'app/components/sidebars/settings'; import {preventDoubleTap} from 'app/utils/tap'; import PushNotifications from 'app/push_notifications'; +import ephemeralStore from 'app/store/ephemeral_store'; import tracker from 'app/utils/time_tracker'; import telemetry from 'app/telemetry'; @@ -232,8 +231,8 @@ export default class ChannelBase extends PureComponent { loadChannelsIfNecessary(teamId).then(() => { loadProfilesAndTeamMembersForDMSidebar(teamId); - if (app.startAppFromPushNotification) { - app.setStartAppFromPushNotification(false); + if (ephemeralStore.appStartedFromPushNotification) { + ephemeralStore.appStartedFromPushNotification = false; } else { selectInitialChannel(teamId); } diff --git a/app/screens/entry/entry.js b/app/screens/entry/entry.js deleted file mode 100644 index 500023a61..000000000 --- a/app/screens/entry/entry.js +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import { - View, - AppState, - Platform, -} from 'react-native'; - -import DeviceInfo from 'react-native-device-info'; - -import {setSystemEmojis} from 'mattermost-redux/actions/emojis'; -import {Client4} from 'mattermost-redux/client'; -import EventEmitter from 'mattermost-redux/utils/event_emitter'; - -import { - app, - store, -} from 'app/mattermost'; -import {ViewTypes} from 'app/constants'; -import PushNotifications from 'app/push_notifications'; -import {stripTrailingSlashes} from 'app/utils/url'; -import {wrapWithContextProvider} from 'app/utils/wrap_context_provider'; - -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'; -import StatusBar from 'app/components/status_bar'; - -const lazyLoadSelectServer = () => { - return require('app/screens/select_server').default; -}; - -const lazyLoadChannel = () => { - return require('app/screens/channel').default; -}; - -const lazyLoadPushNotifications = () => { - return require('app/utils/push_notifications').configurePushNotifications; -}; - -const lazyLoadReplyPushNotifications = () => { - return require('app/utils/push_notifications').onPushNotificationReply; -}; - -/** - * Entry Component: - * With very little overhead navigate to - * - Login or - * - Channel Component - * - * The idea is to render something to the user as soon as possible - */ -export default class Entry extends PureComponent { - static propTypes = { - theme: PropTypes.object, - navigator: PropTypes.object, - isLandscape: PropTypes.bool, - enableTimezone: PropTypes.bool, - deviceTimezone: PropTypes.string, - initializeModules: PropTypes.func, - actions: PropTypes.shape({ - autoUpdateTimezone: PropTypes.func.isRequired, - setDeviceToken: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - launchLogin: false, - launchChannel: false, - }; - - this.unsubscribeFromStore = null; - } - - componentDidMount() { - Client4.setUserAgent(DeviceInfo.getUserAgent()); - - if (store.getState().views.root.hydrationComplete) { - this.handleHydrationComplete(); - } else { - this.unsubscribeFromStore = store.subscribe(this.listenForHydration); - } - - EventEmitter.on(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin); - EventEmitter.on(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel); - } - - componentWillUnmount() { - EventEmitter.off(ViewTypes.LAUNCH_LOGIN, this.handleLaunchLogin); - EventEmitter.off(ViewTypes.LAUNCH_CHANNEL, this.handleLaunchChannel); - } - - handleLaunchLogin = (initializeModules) => { - this.setState({launchLogin: true}); - - if (initializeModules) { - this.props.initializeModules(); - } - }; - - handleLaunchChannel = (initializeModules) => { - this.setState({launchChannel: true}); - - if (initializeModules) { - this.props.initializeModules(); - } - }; - - listenForHydration = () => { - const {getState} = store; - const state = getState(); - - if (!app.isNotificationsConfigured) { - this.configurePushNotifications(); - } - - if (state.views.root.hydrationComplete) { - this.handleHydrationComplete(); - } - }; - - handleHydrationComplete = () => { - if (this.unsubscribeFromStore) { - this.unsubscribeFromStore(); - this.unsubscribeFromStore = null; - } - - this.autoUpdateTimezone(); - this.setAppCredentials(); - this.setStartupThemes(); - this.handleNotification(); - this.loadSystemEmojis(); - - if (Platform.OS === 'android') { - this.launchForAndroid(); - } else { - this.launchForiOS(); - } - }; - - autoUpdateTimezone = () => { - const { - actions: { - autoUpdateTimezone, - }, - enableTimezone, - deviceTimezone, - } = this.props; - - if (enableTimezone) { - autoUpdateTimezone(deviceTimezone); - } - }; - - configurePushNotifications = () => { - const configureNotifications = lazyLoadPushNotifications(); - configureNotifications(); - }; - - setAppCredentials = () => { - const { - actions: { - setDeviceToken, - }, - } = this.props; - const {getState} = store; - const state = getState(); - - const {credentials} = state.entities.general; - const {currentUserId} = state.entities.users; - - if (app.deviceToken) { - setDeviceToken(app.deviceToken); - } - - if (credentials.token && credentials.url) { - Client4.setToken(credentials.token); - Client4.setUrl(stripTrailingSlashes(credentials.url)); - } else if (app.waitForRehydration) { - app.waitForRehydration = false; - } - - if (currentUserId) { - Client4.setUserId(currentUserId); - } - - app.setAppCredentials(app.deviceToken, currentUserId, credentials.token, credentials.url); - }; - - setStartupThemes = () => { - const {theme} = this.props; - if (app.toolbarBackground === theme.sidebarHeaderBg) { - return; - } - - app.setStartupThemes( - theme.sidebarHeaderBg, - theme.sidebarHeaderTextColor, - theme.centerChannelBg - ); - }; - - handleNotification = async () => { - const notification = PushNotifications.getNotification(); - - // If notification exists, it means that the app was started through a reply - // and the app was not sitting in the background nor opened - if (notification || app.replyNotificationData) { - const onPushNotificationReply = lazyLoadReplyPushNotifications(); - const notificationData = notification || app.replyNotificationData; - const {data, text, badge, completed} = notificationData; - - // if the notification has a completed property it means that we are replying to a notification - if (completed) { - onPushNotificationReply(data, text, badge, completed); - } - PushNotifications.resetNotification(); - } - }; - - launchForAndroid = () => { - app.launchApp(); - }; - - launchForiOS = () => { - const appNotActive = AppState.currentState !== 'active'; - - if (appNotActive) { - // for iOS replying from push notification starts the app in the background - app.setShouldRelaunchWhenActive(true); - } else { - app.launchApp(); - } - }; - - loadSystemEmojis = () => { - const EmojiIndicesByAlias = require('app/utils/emojis').EmojiIndicesByAlias; - setSystemEmojis(EmojiIndicesByAlias); - }; - - renderLogin = () => { - const SelectServer = lazyLoadSelectServer(); - const props = { - allowOtherServers: app.allowOtherServers, - navigator: this.props.navigator, - }; - - return wrapWithContextProvider(SelectServer)(props); - }; - - renderChannel = () => { - const ChannelScreen = lazyLoadChannel(); - const props = { - navigator: this.props.navigator, - }; - - return wrapWithContextProvider(ChannelScreen, false)(props); - }; - - render() { - const { - navigator, - isLandscape, - } = this.props; - - if (this.state.launchLogin) { - return this.renderLogin(); - } - - if (this.state.launchChannel) { - return this.renderChannel(); - } - - let toolbar = null; - let loading = null; - const backgroundColor = app.appBackground ? app.appBackground : '#ffff'; - if (app.token && app.toolbarBackground) { - const toolbarTheme = { - sidebarHeaderBg: app.toolbarBackground, - sidebarHeaderTextColor: app.toolbarTextColor, - }; - - toolbar = ( - - - - - ); - - loading = ( - - ); - } else { - loading = ; - } - - return ( - - {toolbar} - {loading} - - ); - } -} diff --git a/app/screens/entry/index.js b/app/screens/entry/index.js deleted file mode 100644 index 22bebdae8..000000000 --- a/app/screens/entry/index.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {bindActionCreators} from 'redux'; -import {connect} from 'react-redux'; - -import {setDeviceToken} from 'mattermost-redux/actions/general'; -import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; - -import {isLandscape} from 'app/selectors/device'; -import {getDeviceTimezone} from 'app/utils/timezone'; - -const lazyLoadEntry = () => { - return require('./entry').default; -}; - -function mapStateToProps(state) { - const enableTimezone = isTimezoneEnabled(state); - const deviceTimezone = getDeviceTimezone(); - - return { - theme: getTheme(state), - isLandscape: isLandscape(state), - enableTimezone, - deviceTimezone, - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators({ - autoUpdateTimezone, - setDeviceToken, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(lazyLoadEntry()); diff --git a/app/screens/index.js b/app/screens/index.js index b2cfa0072..fa83753fb 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -5,7 +5,6 @@ import {Navigation} from 'react-native-navigation'; import {gestureHandlerRootHOC} from 'react-native-gesture-handler'; import Channel from 'app/screens/channel'; -import Entry from 'app/screens/entry'; import SelectServer from 'app/screens/select_server'; import {wrapWithContextProvider} from 'app/utils/wrap_context_provider'; @@ -26,7 +25,6 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('EditChannel', () => wrapWithContextProvider(require('app/screens/edit_channel').default), store, Provider); Navigation.registerComponent('EditPost', () => wrapWithContextProvider(require('app/screens/edit_post').default), store, Provider); Navigation.registerComponent('EditProfile', () => wrapWithContextProvider(require('app/screens/edit_profile').default), store, Provider); - Navigation.registerComponent('Entry', () => Entry, store, Provider); Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapWithContextProvider(require('app/screens/expanded_announcement_banner').default), store, Provider); Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider); Navigation.registerComponent('ForgotPassword', () => wrapWithContextProvider(require('app/screens/forgot_password').default), store, Provider); diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index e10e1b031..79d291ac3 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -25,7 +25,7 @@ import {Client4} from 'mattermost-redux/client'; import ErrorText from 'app/components/error_text'; import FormattedText from 'app/components/formatted_text'; -import fetchConfig from 'app/fetch_preconfig'; +import fetchConfig from 'app/init/fetch'; import mattermostBucket from 'app/mattermost_bucket'; import {GlobalStyles} from 'app/styles'; import {checkUpgradeType, isUpgradeAvailable} from 'app/utils/client_upgrade'; @@ -67,6 +67,16 @@ export default class SelectServer extends PureComponent { intl: intlShape.isRequired, }; + static getDerivedStateFromProps(props, state) { + if (props.serverUrl && !state.url) { + return { + url: props.serverUrl, + }; + } + + return null; + } + constructor(props) { super(props); @@ -99,19 +109,19 @@ export default class SelectServer extends PureComponent { telemetry.save(); } - componentWillUpdate(nextProps, nextState) { - if (nextState.connected && nextProps.hasConfigAndLicense && !(this.state.connected && this.props.hasConfigAndLicense)) { + componentDidUpdate(prevProps, prevState) { + if (this.state.connected && this.props.hasConfigAndLicense && !(prevState.connected && prevProps.hasConfigAndLicense)) { if (LocalConfig.EnableMobileClientUpgrade) { this.props.actions.setLastUpgradeCheck(); - const {currentVersion, minVersion, latestVersion} = nextProps; + const {currentVersion, minVersion, latestVersion} = prevProps; const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion); if (isUpgradeAvailable(upgradeType)) { this.handleShowClientUpgrade(upgradeType); } else { - this.handleLoginOptions(nextProps); + this.handleLoginOptions(prevProps); } } else { - this.handleLoginOptions(nextProps); + this.handleLoginOptions(prevProps); } } } diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js new file mode 100644 index 000000000..dad1dac11 --- /dev/null +++ b/app/store/ephemeral_store.js @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +class EphemeralStore { + constructor() { + this.appStarted = false; + this.appStartedFromPushNotification = false; + this.deviceToken = null; + } +} + +export default new EphemeralStore(); diff --git a/app/store/index.js b/app/store/index.js index 46743abd2..be1155865 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -248,7 +248,6 @@ export default function configureAppStore(initialState) { type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: { url: state.entities.general.credentials.url, - token: state.entities.general.credentials.token, }, }, { diff --git a/app/utils/error_handling.js b/app/utils/error_handling.js index f6597f935..64fa5a59d 100644 --- a/app/utils/error_handling.js +++ b/app/utils/error_handling.js @@ -14,6 +14,8 @@ import {logError} from 'mattermost-redux/actions/errors'; import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {purgeOfflineStore} from 'app/actions/views/root'; +import {DEFAULT_LOCALE, getTranslations} from 'app/i18n'; +import {t} from 'app/utils/i18n'; import { captureException, captureJSException, @@ -21,54 +23,55 @@ import { LOGGER_NATIVE, } from 'app/utils/sentry'; -import {app, store} from 'app/mattermost'; - -import {t} from 'app/utils/i18n'; - -const errorHandler = (e, isFatal) => { - if (__DEV__ && !e && !isFatal) { - // react-native-exception-handler redirects console.error to call this, and React calls - // console.error without an exception when prop type validation fails, so this ends up - // being called with no arguments when the error handler is enabled in dev mode. - return; +class JavascriptAndNativeErrorHandler { + initializeErrorHandling = (store) => { + this.store = store; + initializeSentry(); + setJSExceptionHandler(this.errorHandler, false); + setNativeExceptionHandler(this.nativeErrorHandler, false); } - console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console - captureJSException(e, isFatal, store); + nativeErrorHandler = (e) => { + console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console + captureException(e, LOGGER_NATIVE, this.store); + }; - const {dispatch} = store; + errorHandler = (e, isFatal) => { + if (__DEV__ && !e && !isFatal) { + // react-native-exception-handler redirects console.error to call this, and React calls + // console.error without an exception when prop type validation fails, so this ends up + // being called with no arguments when the error handler is enabled in dev mode. + return; + } - dispatch(closeWebSocket()); + console.warn('Handling Javascript error', e, isFatal); // eslint-disable-line no-console + captureJSException(e, isFatal, this.store); - if (Client4.getUrl()) { - dispatch(logError(e)); - } + const {dispatch} = this.store; - if (isFatal && e instanceof Error) { - const translations = app.getTranslations(); + dispatch(closeWebSocket()); - Alert.alert( - translations[t('mobile.error_handler.title')], - translations[t('mobile.error_handler.description')], - [{ - text: translations[t('mobile.error_handler.button')], - onPress: () => { - // purge the store - dispatch(purgeOfflineStore()); - }, - }], - {cancelable: false} - ); - } -}; + if (Client4.getUrl()) { + dispatch(logError(e)); + } -const nativeErrorHandler = (e) => { - console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console - captureException(e, LOGGER_NATIVE, store); -}; + if (isFatal && e instanceof Error) { + const translations = getTranslations(DEFAULT_LOCALE); -export function initializeErrorHandling() { - initializeSentry(); - setJSExceptionHandler(errorHandler, false); - setNativeExceptionHandler(nativeErrorHandler, false); + Alert.alert( + translations[t('mobile.error_handler.title')], + translations[t('mobile.error_handler.description')], + [{ + text: translations[t('mobile.error_handler.button')], + onPress: () => { + // purge the store + dispatch(purgeOfflineStore()); + }, + }], + {cancelable: false} + ); + } + }; } + +export default new JavascriptAndNativeErrorHandler(); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index a26781e90..7418f1046 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -8,7 +8,6 @@ import {setDeviceToken} from 'mattermost-redux/actions/general'; import {getPosts} from 'mattermost-redux/actions/posts'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {markChannelViewedAndRead, retryGetPostsAction} from 'app/actions/views/channel'; @@ -17,169 +16,179 @@ import { loadFromPushNotification, } from 'app/actions/views/root'; import {ViewTypes} from 'app/constants'; -import {DEFAULT_LOCALE, getLocalizedMessage} from 'app/i18n'; -import {t} from 'app/utils/i18n'; -import { - app, - store, -} from 'app/mattermost'; +import {getLocalizedMessage} from 'app/i18n'; +import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; import PushNotifications from 'app/push_notifications'; +import {getCurrentLocale} from 'app/selectors/i18n'; +import ephemeralStore from 'app/store/ephemeral_store'; +import {t} from 'app/utils/i18n'; -const onRegisterDevice = (data) => { - app.setIsNotificationsConfigured(true); - const state = store.getState(); - - let prefix; - if (Platform.OS === 'ios') { - prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE; - if (DeviceInfo.getBundleId().includes('rnbeta')) { - prefix = `${prefix}beta`; - } - } else { - prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; +class PushNotificationUtils { + constructor() { + this.configured = false; + this.replyNotificationData = null; } - const token = `${prefix}:${data.token}`; - if (state.views.root.hydrationComplete) { - app.setDeviceToken(token); - store.dispatch(setDeviceToken(token)); - } else { - app.setDeviceToken(token); - } -}; + configure = (store) => { + this.store = store; -const loadFromNotification = async (notification) => { - await store.dispatch(loadFromPushNotification(notification, true)); - if (!app.startAppFromPushNotification) { - EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); - PushNotifications.resetNotification(); - } -}; - -const onPushNotification = async (deviceNotification) => { - const {dispatch, getState} = store; - let unsubscribeFromStore = null; - let stopLoadingNotification = false; - - // mark the app as started as soon as possible - if (!app.appStarted) { - app.setStartAppFromPushNotification(true); - } - - const {data, foreground, message, userInfo, userInteraction} = deviceNotification; - const notification = { - data, - message, + PushNotifications.configure({ + onRegister: this.onRegisterDevice, + onNotification: this.onPushNotification, + onReply: this.onPushNotificationReply, + popInitialNotification: true, + requestPermissions: true, + }); }; - if (userInfo) { - notification.localNotification = userInfo.localNotification; - } + loadFromNotification = async (notification) => { + await this.store.dispatch(loadFromPushNotification(notification, true)); - if (data.type === 'clear') { - dispatch(markChannelViewedAndRead(data.channel_id, null, false)); - } else { - // get the posts for the channel as soon as possible - retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - - if (foreground) { - EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); - } else if (userInteraction && !notification.localNotification) { - EventEmitter.emit('close_channel_drawer'); - if (getState().views.root.hydrationComplete) { - setTimeout(() => { - loadFromNotification(notification); - }, 0); - } else { - const waitForHydration = () => { - if (getState().views.root.hydrationComplete && !stopLoadingNotification) { - stopLoadingNotification = true; - unsubscribeFromStore(); - loadFromNotification(notification); - } - }; - - unsubscribeFromStore = store.subscribe(waitForHydration); - } + if (!ephemeralStore.appStartedFromPushNotification) { + EventEmitter.emit(ViewTypes.NOTIFICATION_TAPPED); + PushNotifications.resetNotification(); } - } -}; + }; -export const onPushNotificationReply = async (data, text, badge, completed) => { - const {dispatch, getState} = store; - const state = getState(); - const reduxCurrentUser = getCurrentUser(state); - const reduxCredentialsUrl = state.entities.general.credentials.url; - const reduxCredentialsToken = state.entities.general.credentials.token; + onPushNotification = async (deviceNotification) => { + const {dispatch, getState} = this.store; + let unsubscribeFromStore = null; + let stopLoadingNotification = false; - const currentUserId = reduxCurrentUser ? reduxCurrentUser.id : app.currentUserId; - const url = reduxCredentialsUrl || app.url; - const token = reduxCredentialsToken || app.token; + // mark the app as started as soon as possible + if (!ephemeralStore.appStarted) { + ephemeralStore.appStartedFromPushNotification = true; + } - if (currentUserId) { - // one thing to note is that for android it will reply to the last post in the stack - const rootId = data.root_id || data.post_id; - const post = { - user_id: currentUserId, - channel_id: data.channel_id, - root_id: rootId, - parent_id: rootId, - message: text, + const {data, foreground, message, userInfo, userInteraction} = deviceNotification; + const notification = { + data, + message, }; - if (!Client4.getUrl()) { - // Make sure the Client has the server url set - Client4.setUrl(url); + if (userInfo) { + notification.localNotification = userInfo.localNotification; } - if (!Client4.getToken()) { - // Make sure the Client has the server token set - Client4.setToken(token); - } + if (data.type === 'clear') { + dispatch(markChannelViewedAndRead(data.channel_id, null, false)); + } else { + // get the posts for the channel as soon as possible + retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); - const result = await dispatch(createPostForNotificationReply(post)); - if (result.error) { - const locale = reduxCurrentUser ? reduxCurrentUser.locale : DEFAULT_LOCALE; - PushNotifications.localNotification({ - message: getLocalizedMessage(locale, t('mobile.reply_post.failed')), - userInfo: { - localNotification: true, - localTest: true, - }, - }); - console.warn('Failed to send reply to push notification', result.error); // eslint-disable-line no-console + if (foreground) { + EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); + } else if (userInteraction && !notification.localNotification) { + EventEmitter.emit('close_channel_drawer'); + if (getState().views.root.hydrationComplete) { //TODO: Replace when realm is ready + setTimeout(() => { + this.loadFromNotification(notification); + }, 0); + } else { + const waitForHydration = () => { + if (getState().views.root.hydrationComplete && !stopLoadingNotification) { + stopLoadingNotification = true; + unsubscribeFromStore(); + this.loadFromNotification(notification); + } + }; + + unsubscribeFromStore = this.store.subscribe(waitForHydration); + } + } + } + }; + + onPushNotificationReply = async (data, text, badge, completed) => { + const {dispatch, getState} = this.store; + const state = getState(); + const credentials = await getAppCredentials(); // TODO Change to handle multiple servers + const url = await getCurrentServerUrl(); // TODO Change to handle multiple servers + const token = credentials.password; + const usernameParsed = credentials.username.split(','); + const [, currentUserId] = usernameParsed; + + if (currentUserId) { + // one thing to note is that for android it will reply to the last post in the stack + const rootId = data.root_id || data.post_id; + const post = { + user_id: currentUserId, + channel_id: data.channel_id, + root_id: rootId, + parent_id: rootId, + message: text, + }; + + if (!Client4.getUrl()) { + // Make sure the Client has the server url set + Client4.setUrl(url); + } + + if (!Client4.getToken()) { + // Make sure the Client has the server token set + Client4.setToken(token); + } + + retryGetPostsAction(getPosts(data.channel_id), dispatch, getState); + const result = await dispatch(createPostForNotificationReply(post)); + if (result.error) { + const locale = getCurrentLocale(state); + PushNotifications.localNotification({ + message: getLocalizedMessage(locale, t('mobile.reply_post.failed')), + userInfo: { + localNotification: true, + localTest: true, + }, + }); + console.warn('Failed to send reply to push notification', result.error); // eslint-disable-line no-console + completed(); + return; + } + + if (badge >= 0) { + PushNotifications.setApplicationIconBadgeNumber(badge); + } + + dispatch(markChannelViewedAndRead(data.channel_id)); + this.replyNotificationData = null; completed(); - return; + } else { + this.replyNotificationData = { + data, + text, + badge, + completed, + }; + } + }; + + onRegisterDevice = (data) => { + const {dispatch, getState} = this.store; + let unsubscribeFromStore = null; + + let prefix; + if (Platform.OS === 'ios') { + prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE; + if (DeviceInfo.getBundleId().includes('rnbeta')) { + prefix = `${prefix}beta`; + } + } else { + prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; } - if (badge >= 0) { - PushNotifications.setApplicationIconBadgeNumber(badge); - } + ephemeralStore.deviceToken = `${prefix}:${data.token}`; - dispatch(markChannelViewedAndRead(data.channel_id)); - app.setReplyNotificationData(null); - completed(); - } else { - app.setReplyNotificationData({ - data, - text, - badge, - completed, - }); + // TODO: Remove when realm is ready + const waitForHydration = () => { + if (getState().views.root.hydrationComplete && !this.configured) { + this.configured = true; + dispatch(setDeviceToken(ephemeralStore.deviceToken)); + unsubscribeFromStore(); + } + }; + + unsubscribeFromStore = this.store.subscribe(waitForHydration); } -}; +} -export const configurePushNotifications = () => { - PushNotifications.configure({ - onRegister: onRegisterDevice, - onNotification: onPushNotification, - onReply: onPushNotificationReply, - popInitialNotification: true, - requestPermissions: true, - }); - - if (app) { - app.setIsNotificationsConfigured(true); - } -}; +export default new PushNotificationUtils(); diff --git a/app/utils/url.js b/app/utils/url.js index 296a4e62c..e0feebc8f 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -103,9 +103,10 @@ export function matchDeepLink(url, serverURL, siteURL) { return null; } - const linkRoot = `(?:${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`; + const linkRoot = `(?:${escapeRegex('mattermost:/')}|${escapeRegex(serverURL)}|${escapeRegex(siteURL)})?`; let match = new RegExp('^' + linkRoot + '\\/([^\\/]+)\\/channels\\/(\\S+)').exec(url); + if (match) { return {type: DeepLinkTypes.CHANNEL, teamName: match[1], channelName: match[2]}; } diff --git a/index.js b/index.js index 54e26e7aa..84ce150c3 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import 'react-native/Libraries/Core/InitializeCore'; -import {AppRegistry, DeviceEventEmitter, Platform, YellowBox} from 'react-native'; +import {AppRegistry, DeviceEventEmitter, Platform, Text, YellowBox} from 'react-native'; import 'react-native-gesture-handler'; import LocalConfig from 'assets/config'; @@ -10,16 +10,46 @@ import LocalConfig from 'assets/config'; import telemetry from 'app/telemetry'; import 'app/mattermost'; -import ShareExtension from 'share_extension/android'; -YellowBox.ignoreWarnings([ - 'Warning: componentWillMount is deprecated', - 'Warning: componentWillUpdate is deprecated', - 'Warning: componentWillReceiveProps is deprecated', -]); +if (__DEV__) { + YellowBox.ignoreWarnings([ + 'Warning: componentWillMount is deprecated', + 'Warning: componentWillUpdate is deprecated', + 'Warning: componentWillReceiveProps is deprecated', + + // Hide warnings caused by React Native (https://github.com/facebook/react-native/issues/20841) + 'Require cycle: node_modules/react-native/Libraries/Network/fetch.js', + ]); +} + +const setFontFamily = () => { + // Set a global font for Android + const defaultFontFamily = { + style: { + fontFamily: 'Roboto', + }, + }; + const TextRender = Text.render; + const initialDefaultProps = Text.defaultProps; + Text.defaultProps = { + ...initialDefaultProps, + ...defaultFontFamily, + }; + Text.render = function render(props, ...args) { + const oldProps = props; + let newProps = {...props, style: [defaultFontFamily.style, props.style]}; + try { + return Reflect.apply(TextRender, this, [newProps, ...args]); + } finally { + newProps = oldProps; + } + }; +}; if (Platform.OS === 'android') { + const ShareExtension = require('share_extension/android').default; AppRegistry.registerComponent('MattermostShare', () => ShareExtension); + setFontFamily(); if (LocalConfig.TelemetryEnabled) { const metricsSubscription = DeviceEventEmitter.addListener('nativeMetrics', (metrics) => { diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index b2f5c5ce1..a96f94d2f 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -68,6 +68,7 @@ RCT_EXPORT_MODULE(); return @{ @"hasSafeAreaInsets": @([self hasSafeAreaInsets]), + @"appGroupIdentifier": APP_GROUP_ID }; } diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj index 871083eef..35da9ccc2 100644 --- a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -7,7 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE054221387B500D0F595 /* Constants.h */; }; + 7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE054221387B500D0F595 /* MMMConstants.h */; }; + 7F82907122C295AC0035544F /* MMKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F82907022C295AC0035544F /* MMKeychainManager.m */; }; + 7F82907322C296970035544F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F82907222C296970035544F /* Security.framework */; }; + 7F82907522C296A10035544F /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F82907422C296A10035544F /* LocalAuthentication.framework */; }; 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE04C2213818900D0F595 /* MattermostBucket.m */; }; 7FABE055221387B500D0F595 /* MMMConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE053221387B400D0F595 /* MMMConstants.m */; }; 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE057221388D600D0F595 /* UploadSessionManager.swift */; }; @@ -25,13 +28,17 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( - 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */, + 7F80232C229C91AD0034D6D4 /* MMMConstants.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 7F82906F22C295AB0035544F /* MMKeychainManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMKeychainManager.h; sourceTree = ""; }; + 7F82907022C295AC0035544F /* MMKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMKeychainManager.m; sourceTree = ""; }; + 7F82907222C296970035544F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 7F82907422C296A10035544F /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; 7FABE03622137F2900D0F595 /* libUploadAttachments.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libUploadAttachments.a; sourceTree = BUILT_PRODUCTS_DIR; }; 7FABE04B2213818900D0F595 /* UploadAttachments-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UploadAttachments-Bridging-Header.h"; sourceTree = ""; }; 7FABE04C2213818900D0F595 /* MattermostBucket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostBucket.m; sourceTree = ""; }; @@ -52,17 +59,29 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7F82907522C296A10035544F /* LocalAuthentication.framework in Frameworks */, + 7F82907322C296970035544F /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7F82901A22C28F770035544F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7F82907422C296A10035544F /* LocalAuthentication.framework */, + 7F82907222C296970035544F /* Security.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 7FABE02D22137F2900D0F595 = { isa = PBXGroup; children = ( 7FABE03822137F2900D0F595 /* UploadAttachments */, 7FABE03722137F2900D0F595 /* Products */, + 7F82901A22C28F770035544F /* Frameworks */, ); sourceTree = ""; }; @@ -77,6 +96,8 @@ 7FABE03822137F2900D0F595 /* UploadAttachments */ = { isa = PBXGroup; children = ( + 7F82906F22C295AB0035544F /* MMKeychainManager.h */, + 7F82907022C295AC0035544F /* MMKeychainManager.m */, 7FABE05A2213892100D0F595 /* AttachmentArray.swift */, 7FABE0592213892100D0F595 /* AttachmentItem.swift */, 7FABE054221387B500D0F595 /* MMMConstants.h */, @@ -175,6 +196,7 @@ files = ( 7FABE05C2213892200D0F595 /* AttachmentArray.swift in Sources */, 7FABE0FA2214674200D0F595 /* StoreManager.m in Sources */, + 7F82907122C295AC0035544F /* MMKeychainManager.m in Sources */, 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */, 7FABE0F7221466F900D0F595 /* UploadManager.swift in Sources */, 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */, @@ -309,6 +331,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + HEADER_SEARCH_PATHS = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -331,6 +354,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + HEADER_SEARCH_PATHS = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h new file mode 100644 index 000000000..13489b9db --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.h @@ -0,0 +1,3 @@ +@interface MMKeychainManager : NSObject +-(NSDictionary *)getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options; +@end diff --git a/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m new file mode 100644 index 000000000..866ce90fb --- /dev/null +++ b/ios/UploadAttachments/UploadAttachments/MMKeychainManager.m @@ -0,0 +1,73 @@ +#import +#import "MMKeychainManager.h" + +#import +#import + +@implementation MMKeychainManager + +// Messages from the comments in + +#pragma mark - Proposed functionality - Helpers + +#define kAuthenticationType @"authenticationType" +#define kAuthenticationTypeBiometrics @"AuthenticationWithBiometrics" + +#define kAccessControlType @"accessControl" +#define kAccessControlUserPresence @"UserPresence" +#define kAccessControlBiometryAny @"BiometryAny" +#define kAccessControlBiometryCurrentSet @"BiometryCurrentSet" +#define kAccessControlDevicePasscode @"DevicePasscode" +#define kAccessControlApplicationPassword @"ApplicationPassword" +#define kAccessControlBiometryAnyOrDevicePasscode @"BiometryAnyOrDevicePasscode" +#define kAccessControlBiometryCurrentSetOrDevicePasscode @"BiometryCurrentSetOrDevicePasscode" + +#define kBiometryTypeTouchID @"TouchID" +#define kBiometryTypeFaceID @"FaceID" + +#define kAuthenticationPromptMessage @"authenticationPrompt" + +#pragma mark - Native access + +-(NSDictionary *)getInternetCredentialsForServer:(NSString *)server withOptions:(NSDictionary *)options +{ + if (server == nil) { + return nil; + } + + NSDictionary *query = @{ + (__bridge NSString *)kSecClass: (__bridge id)(kSecClassInternetPassword), + (__bridge NSString *)kSecAttrServer: server, + (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne + }; + + // Look up server in the keychain + NSDictionary *found = nil; + CFTypeRef foundTypeRef = NULL; + OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef*)&foundTypeRef); + + if (osStatus != noErr && osStatus != errSecItemNotFound) { + return nil; + } + + found = (__bridge NSDictionary*)(foundTypeRef); + if (!found) { + return nil; + } + + // Found + NSString *username = (NSString *) [found objectForKey:(__bridge id)(kSecAttrAccount)]; + NSString *password = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding]; + + CFRelease(foundTypeRef); + NSDictionary *result = @{ + @"server": server, + @"username": username, + @"password": password + }; + return result; +} + +@end diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.h b/ios/UploadAttachments/UploadAttachments/StoreManager.h index ce4e4f5eb..23fb17be6 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.h +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.h @@ -1,8 +1,10 @@ #import #import "MattermostBucket.h" +#import "MMKeychainManager.h" @interface StoreManager : NSObject @property MattermostBucket *bucket; +@property MMKeychainManager *keychain; @property (nonatomic, strong) NSDictionary *entities; +(instancetype)shared; diff --git a/ios/UploadAttachments/UploadAttachments/StoreManager.m b/ios/UploadAttachments/UploadAttachments/StoreManager.m index 143f37a5d..bdaddae1b 100644 --- a/ios/UploadAttachments/UploadAttachments/StoreManager.m +++ b/ios/UploadAttachments/UploadAttachments/StoreManager.m @@ -15,8 +15,9 @@ -(instancetype)init { self = [super init]; if (self) { - self.bucket = [[MattermostBucket alloc] init]; - [self getEntities:true]; + self.bucket = [[MattermostBucket alloc] init]; + self.keychain = [[MMKeychainManager alloc] init]; + [self getEntities:true]; } return self; } @@ -150,10 +151,12 @@ } -(NSString *)getToken { - NSDictionary *general = [self.entities objectForKey:@"general"]; - NSDictionary *credentials = [general objectForKey:@"credentials"]; + NSDictionary *options = @{ + @"accessGroup": APP_GROUP_ID + }; + NSDictionary *credentials = [self.keychain getInternetCredentialsForServer:[self getServerUrl] withOptions:options]; - return [credentials objectForKey:@"token"]; + return [credentials objectForKey:@"password"]; } -(UInt64)scanValueFromConfig:(NSDictionary *)config key:(NSString *)key { diff --git a/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h index 194d712c7..ec1e7cbcc 100644 --- a/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h +++ b/ios/UploadAttachments/UploadAttachments/UploadAttachments-Bridging-Header.h @@ -5,3 +5,4 @@ #import "MMMConstants.h" #import "MattermostBucket.h" #import "StoreManager.h" +#import "MMKeychainManager.h" diff --git a/packager/moduleNames.js b/packager/moduleNames.js index 2b780a989..1263945e9 100644 --- a/packager/moduleNames.js +++ b/packager/moduleNames.js @@ -2,88 +2,8 @@ // See LICENSE.txt for license information. module.exports = [ - 'app/app.js', - 'app/components/app_icon.js', - 'app/components/at_mention/at_mention.js', - 'app/components/at_mention/index.js', - 'app/components/attachment_button.js', - 'app/components/badge.js', - 'app/components/emoji/emoji.js', - 'app/components/emoji/index.js', - 'app/components/fade.js', - 'app/components/file_attachment_list/file_attachment_icon.js', - 'app/components/file_attachment_list/file_attachment_image.js', - 'app/components/formatted_date.js', - 'app/components/formatted_markdown_text.js', - 'app/components/formatted_text.js', - 'app/components/formatted_time.js', - 'app/components/interactive_dialog_controller/index.js', - 'app/components/interactive_dialog_controller/interactive_dialog_controller.js', - 'app/components/layout/keyboard_layout/index.js', - 'app/components/layout/keyboard_layout/keyboard_layout.js', 'app/components/loading.js', - 'app/components/markdown/hashtag/hashtag.js', - 'app/components/markdown/hashtag/index.js', - 'app/components/markdown/index.js', - 'app/components/markdown/markdown.js', - 'app/components/markdown/markdown_block_quote.js', - 'app/components/markdown/markdown_code_block/index.js', - 'app/components/markdown/markdown_code_block/markdown_code_block.js', - 'app/components/markdown/markdown_emoji/index.js', - 'app/components/markdown/markdown_emoji/markdown_emoji.js', - 'app/components/markdown/markdown_image/index.js', - 'app/components/markdown/markdown_image/markdown_image.js', - 'app/components/markdown/markdown_link/index.js', - 'app/components/markdown/markdown_link/markdown_link.js', - 'app/components/markdown/markdown_list.js', - 'app/components/markdown/markdown_list_item.js', - 'app/components/markdown/markdown_table/index.js', - 'app/components/markdown/markdown_table/markdown_table.js', - 'app/components/markdown/markdown_table_cell/index.js', - 'app/components/markdown/markdown_table_cell/markdown_table_cell.js', - 'app/components/markdown/markdown_table_image/index.js', - 'app/components/markdown/markdown_table_image/markdown_table_image.js', - 'app/components/markdown/markdown_table_row/index.js', - 'app/components/markdown/markdown_table_row/markdown_table_row.js', - 'app/components/network_indicator/index.js', - 'app/components/network_indicator/network_indicator.js', 'app/components/paper_plane.js', - 'app/components/post/index.js', - 'app/components/post/post.js', - 'app/components/post_body/index.js', - 'app/components/post_body/post_body.js', - 'app/components/post_header/index.js', - 'app/components/post_header/post_header.js', - 'app/components/post_header/post_pre_header.js', - 'app/components/post_list/date_header/date_header.js', - 'app/components/post_list/date_header/index.js', - 'app/components/post_list/index.js', - 'app/components/post_list/new_messages_divider.js', - 'app/components/post_list/post_list.js', - 'app/components/post_list_retry.js', - 'app/components/post_profile_picture/index.js', - 'app/components/post_profile_picture/post_profile_picture.js', - 'app/components/post_textbox/components/typing/index.js', - 'app/components/post_textbox/components/typing/typing.js', - 'app/components/post_textbox/index.js', - 'app/components/post_textbox/post_textbox.android.js', - 'app/components/post_textbox/post_textbox_base.js', - 'app/components/profile_picture/index.js', - 'app/components/profile_picture/profile_picture.js', - 'app/components/progressive_image/index.js', - 'app/components/progressive_image/progressive_image.js', - 'app/components/reply_icon.js', - 'app/components/safe_area_view/index.js', - 'app/components/safe_area_view/safe_area_view.android.js', - 'app/components/send_button.js', - 'app/components/show_more_button/index.js', - 'app/components/show_more_button/show_more_button.js', - 'app/components/start/empty_toolbar.js', - 'app/components/status_bar/index.js', - 'app/components/status_bar/status_bar.js', - 'app/components/user_status/index.js', - 'app/components/user_status/user_status.js', - 'app/components/vector_icon.js', 'app/constants/custom_prop_types.js', 'app/constants/deep_linking.js', 'app/constants/device.js', @@ -92,7 +12,10 @@ module.exports = [ 'app/constants/navigation.js', 'app/constants/permissions.js', 'app/constants/view.js', - 'app/fetch_preconfig.js', + 'app/init/credentials.js', + 'app/init/emm_provider.js', + 'app/init/fetch.js', + 'app/init/global_event_handler.js', 'app/initial_state.js', 'app/mattermost.js', 'app/mattermost_bucket/index.js', @@ -128,61 +51,17 @@ module.exports = [ 'app/reducers/views/team.js', 'app/reducers/views/thread.js', 'app/reducers/views/user.js', - 'app/screens/channel/channel.android.js', - 'app/screens/channel/channel_base.js', - 'app/screens/channel/channel_nav_bar/channel_drawer_button.js', - 'app/screens/channel/channel_nav_bar/channel_nav_bar.js', - 'app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js', - 'app/screens/channel/channel_nav_bar/channel_search_button/index.js', - 'app/screens/channel/channel_nav_bar/channel_title/channel_title.js', - 'app/screens/channel/channel_nav_bar/channel_title/index.js', - 'app/screens/channel/channel_nav_bar/index.js', - 'app/screens/channel/channel_nav_bar/settings_drawer_button.js', - 'app/screens/channel/channel_post_list/channel_post_list.js', - 'app/screens/channel/channel_post_list/index.js', - 'app/screens/channel/index.js', - 'app/screens/entry/entry.js', - 'app/screens/entry/index.js', - 'app/screens/index.js', - 'app/screens/select_server/index.js', - 'app/screens/select_server/select_server.js', - 'app/selectors/client_upgrade.js', + 'app/store/ephemeral_store.js', 'app/store/index.js', 'app/store/middleware.js', 'app/store/thunk.js', 'app/telemetry/index.js', 'app/telemetry/telemetry.android.js', - 'app/telemetry/telemetry_utils.js', - 'app/utils/avoid_native_bridge.js', - 'app/utils/general.js', - 'app/utils/i18n.js', 'app/utils/image_cache_manager.js', - 'app/utils/network.js', 'app/utils/push_notifications.js', 'app/utils/sentry/middleware.js', 'app/utils/theme.js', - 'app/utils/time_tracker.js', 'dist/assets/config.json', - 'dist/assets/images/icons/audio.png', - 'dist/assets/images/icons/brokenimage.png', - 'dist/assets/images/icons/code.png', - 'dist/assets/images/icons/excel.png', - 'dist/assets/images/icons/generic.png', - 'dist/assets/images/icons/image.png', - 'dist/assets/images/icons/patch.png', - 'dist/assets/images/icons/pdf.png', - 'dist/assets/images/icons/ppt.png', - 'dist/assets/images/icons/video.png', - 'dist/assets/images/icons/webhook.jpg', - 'dist/assets/images/icons/word.png', - 'dist/assets/images/post_header/flag.png', - 'dist/assets/images/post_header/pin.png', - 'dist/assets/images/profile.jpg', - 'dist/assets/images/status/away.png', - 'dist/assets/images/status/dnd.png', - 'dist/assets/images/status/offline.png', - 'dist/assets/images/status/online.png', - 'dist/assets/images/thumb.png', 'index.js', 'node_modules/@babel/runtime/helpers/arrayWithHoles.js', 'node_modules/@babel/runtime/helpers/assertThisInitialized.js', @@ -208,9 +87,7 @@ module.exports = [ 'node_modules/@babel/runtime/regenerator/index.js', 'node_modules/@react-native-community/async-storage/lib/AsyncStorage.js', 'node_modules/@react-native-community/async-storage/lib/index.js', - 'node_modules/@react-native-community/netinfo/js/index.js', 'node_modules/base-64/base64.js', - 'node_modules/commonmark-react-renderer/src/commonmark-react-renderer.js', 'node_modules/component-emitter/index.js', 'node_modules/core-js/modules/_a-function.js', 'node_modules/core-js/modules/_add-to-unscopables.js', @@ -288,12 +165,10 @@ module.exports = [ 'node_modules/core-js/modules/es6.set.js', 'node_modules/core-js/modules/es6.string.includes.js', 'node_modules/core-js/modules/es6.string.iterator.js', - 'node_modules/core-js/modules/es6.string.starts-with.js', 'node_modules/core-js/modules/es6.symbol.js', 'node_modules/core-js/modules/es7.array.includes.js', 'node_modules/core-js/modules/es7.object.define-getter.js', 'node_modules/core-js/modules/es7.object.define-setter.js', - 'node_modules/core-js/modules/es7.object.entries.js', 'node_modules/core-js/modules/es7.object.values.js', 'node_modules/core-js/modules/es7.symbol.async-iterator.js', 'node_modules/core-js/modules/web.dom.iterable.js', @@ -311,7 +186,6 @@ module.exports = [ 'node_modules/fbjs/lib/keyOf.js', 'node_modules/fbjs/lib/performance.js', 'node_modules/fbjs/lib/performanceNow.js', - 'node_modules/fuse.js/dist/fuse.js', 'node_modules/global/window.js', 'node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js', 'node_modules/intl-format-cache/index.js', @@ -473,10 +347,6 @@ module.exports = [ 'node_modules/mattermost-redux/reducers/requests/search.js', 'node_modules/mattermost-redux/reducers/requests/teams.js', 'node_modules/mattermost-redux/reducers/requests/users.js', - 'node_modules/mattermost-redux/selectors/entities/emojis.js', - 'node_modules/mattermost-redux/selectors/entities/general.js', - 'node_modules/mattermost-redux/selectors/entities/integrations.js', - 'node_modules/mattermost-redux/selectors/entities/teams.js', 'node_modules/mattermost-redux/store/configureStore.prod.js', 'node_modules/mattermost-redux/store/helpers.js', 'node_modules/mattermost-redux/store/index.js', @@ -485,17 +355,9 @@ module.exports = [ 'node_modules/mattermost-redux/store/reducer_registry.js', 'node_modules/mattermost-redux/utils/deep_freeze.js', 'node_modules/mattermost-redux/utils/event_emitter.js', - 'node_modules/mattermost-redux/utils/file_utils.js', - 'node_modules/mattermost-redux/utils/helpers.js', 'node_modules/mattermost-redux/utils/key_mirror.js', - 'node_modules/mattermost-redux/utils/post_list.js', 'node_modules/mattermost-redux/utils/theme_utils.js', - 'node_modules/moment-timezone/data/packed/latest.json', - 'node_modules/moment-timezone/index.js', - 'node_modules/moment-timezone/moment-timezone.js', - 'node_modules/moment/moment.js', 'node_modules/object-assign/index.js', - 'node_modules/pascalcase/index.js', 'node_modules/path-to-regexp/index.js', 'node_modules/path-to-regexp/node_modules/isarray/index.js', 'node_modules/promise/setimmediate/core.js', @@ -517,11 +379,8 @@ module.exports = [ 'node_modules/react-intl/locale-data/index.js', 'node_modules/react-is/cjs/react-is.production.min.js', 'node_modules/react-is/index.js', - 'node_modules/react-native-button/Button.js', - 'node_modules/react-native-button/coalesceNonElementChildren.js', 'node_modules/react-native-device-info/deviceinfo.js', - 'node_modules/react-native-linear-gradient/common.js', - 'node_modules/react-native-linear-gradient/index.android.js', + 'node_modules/react-native-keychain/index.js', 'node_modules/react-native-local-auth/LocalAuth.android.js', 'node_modules/react-native-local-auth/index.js', 'node_modules/react-native-navigation/src/NativeEventsReceiver.js', @@ -541,7 +400,6 @@ module.exports = [ 'node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js', 'node_modules/react-native/Libraries/Animated/src/Easing.js', 'node_modules/react-native/Libraries/Animated/src/components/AnimatedText.js', - 'node_modules/react-native/Libraries/Animated/src/components/AnimatedView.js', 'node_modules/react-native/Libraries/Animated/src/createAnimatedComponent.js', 'node_modules/react-native/Libraries/Animated/src/nodes/AnimatedInterpolation.js', 'node_modules/react-native/Libraries/Animated/src/nodes/AnimatedNode.js', @@ -562,13 +420,9 @@ module.exports = [ 'node_modules/react-native/Libraries/Components/TextInput/TextInputState.js', 'node_modules/react-native/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js', 'node_modules/react-native/Libraries/Components/Touchable/Touchable.js', - 'node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', - 'node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', 'node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', 'node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js', 'node_modules/react-native/Libraries/Components/View/ReactNativeViewAttributes.js', - 'node_modules/react-native/Libraries/Components/View/View.js', - 'node_modules/react-native/Libraries/Components/View/ViewNativeComponent.js', 'node_modules/react-native/Libraries/Core/ExceptionsManager.js', 'node_modules/react-native/Libraries/Core/InitializeCore.js', 'node_modules/react-native/Libraries/Core/ReactNativeVersion.js', @@ -605,7 +459,6 @@ module.exports = [ 'node_modules/react-native/Libraries/Image/AssetRegistry.js', 'node_modules/react-native/Libraries/Image/AssetSourceResolver.js', 'node_modules/react-native/Libraries/Image/Image.android.js', - 'node_modules/react-native/Libraries/Image/ImageBackground.js', 'node_modules/react-native/Libraries/Image/resolveAssetSource.js', 'node_modules/react-native/Libraries/Interaction/PanResponder.js', 'node_modules/react-native/Libraries/JSInspector/InspectorAgent.js', @@ -614,10 +467,8 @@ module.exports = [ 'node_modules/react-native/Libraries/Linking/Linking.js', 'node_modules/react-native/Libraries/Network/NetInfo.js', 'node_modules/react-native/Libraries/Performance/Systrace.js', - 'node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js', 'node_modules/react-native/Libraries/Promise.js', 'node_modules/react-native/Libraries/ReactNative/AppRegistry.js', - 'node_modules/react-native/Libraries/ReactNative/FabricUIManager.js', 'node_modules/react-native/Libraries/ReactNative/I18nManager.js', 'node_modules/react-native/Libraries/ReactNative/UIManager.js', 'node_modules/react-native/Libraries/ReactNative/UIManagerProperties.js', @@ -634,7 +485,6 @@ module.exports = [ 'node_modules/react-native/Libraries/StyleSheet/processTransform.js', 'node_modules/react-native/Libraries/Text/Text.js', 'node_modules/react-native/Libraries/Text/TextStylePropTypes.js', - 'node_modules/react-native/Libraries/Utilities/BackHandler.android.js', 'node_modules/react-native/Libraries/Utilities/DeviceInfo.js', 'node_modules/react-native/Libraries/Utilities/Dimensions.js', 'node_modules/react-native/Libraries/Utilities/HMRClient.js', @@ -718,15 +568,30 @@ module.exports = [ 'node_modules/redux-thunk/lib/index.js', 'node_modules/redux/lib/redux.js', 'node_modules/regenerator-runtime/runtime.js', - 'node_modules/reselect/lib/index.js', 'node_modules/rn-fetch-blob/android.js', 'node_modules/rn-fetch-blob/cba/index.js', 'node_modules/rn-fetch-blob/class/RNFetchBlobFile.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobReadStream.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobSession.js', + 'node_modules/rn-fetch-blob/class/RNFetchBlobWriteStream.js', 'node_modules/rn-fetch-blob/fs.js', 'node_modules/rn-fetch-blob/index.js', 'node_modules/rn-fetch-blob/json-stream.js', 'node_modules/rn-fetch-blob/lib/oboe-browser.min.js', + 'node_modules/rn-fetch-blob/polyfill/Blob.js', + 'node_modules/rn-fetch-blob/polyfill/Event.js', + 'node_modules/rn-fetch-blob/polyfill/EventTarget.js', 'node_modules/rn-fetch-blob/polyfill/Fetch.js', + 'node_modules/rn-fetch-blob/polyfill/File.js', + 'node_modules/rn-fetch-blob/polyfill/FileReader.js', + 'node_modules/rn-fetch-blob/polyfill/ProgressEvent.js', + 'node_modules/rn-fetch-blob/polyfill/XMLHttpRequest.js', + 'node_modules/rn-fetch-blob/polyfill/XMLHttpRequestEventTarget.js', + 'node_modules/rn-fetch-blob/polyfill/index.js', + 'node_modules/rn-fetch-blob/utils/log.js', + 'node_modules/rn-fetch-blob/utils/unicode.js', + 'node_modules/rn-fetch-blob/utils/uri.js', + 'node_modules/rn-fetch-blob/utils/uuid.js', 'node_modules/rn-host-detect/index.js', 'node_modules/sc-errors/decycle.js', 'node_modules/sc-errors/index.js', @@ -736,10 +601,8 @@ module.exports = [ 'node_modules/scheduler/index.js', 'node_modules/scheduler/tracing.js', 'node_modules/semver/semver.js', - 'node_modules/shallow-equals/index.js', 'node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', 'node_modules/symbol-observable/lib/index.js', 'node_modules/symbol-observable/lib/ponyfill.js', - 'node_modules/tinycolor2/tinycolor.js', 'node_modules/url-parse/index.js', ]; diff --git a/packager/modulePaths.js b/packager/modulePaths.js index 0f657849a..301f51f92 100644 --- a/packager/modulePaths.js +++ b/packager/modulePaths.js @@ -1,87 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -module.exports = ['./node_modules/app/app.js', - './node_modules/app/components/app_icon.js', - './node_modules/app/components/at_mention/at_mention.js', - './node_modules/app/components/at_mention/index.js', - './node_modules/app/components/attachment_button.js', - './node_modules/app/components/badge.js', - './node_modules/app/components/emoji/emoji.js', - './node_modules/app/components/emoji/index.js', - './node_modules/app/components/fade.js', - './node_modules/app/components/file_attachment_list/file_attachment_icon.js', - './node_modules/app/components/file_attachment_list/file_attachment_image.js', - './node_modules/app/components/formatted_date.js', - './node_modules/app/components/formatted_markdown_text.js', - './node_modules/app/components/formatted_text.js', - './node_modules/app/components/formatted_time.js', - './node_modules/app/components/interactive_dialog_controller/index.js', - './node_modules/app/components/interactive_dialog_controller/interactive_dialog_controller.js', - './node_modules/app/components/layout/keyboard_layout/index.js', - './node_modules/app/components/layout/keyboard_layout/keyboard_layout.js', - './node_modules/app/components/loading.js', - './node_modules/app/components/markdown/hashtag/hashtag.js', - './node_modules/app/components/markdown/hashtag/index.js', - './node_modules/app/components/markdown/index.js', - './node_modules/app/components/markdown/markdown.js', - './node_modules/app/components/markdown/markdown_block_quote.js', - './node_modules/app/components/markdown/markdown_code_block/index.js', - './node_modules/app/components/markdown/markdown_code_block/markdown_code_block.js', - './node_modules/app/components/markdown/markdown_emoji/index.js', - './node_modules/app/components/markdown/markdown_emoji/markdown_emoji.js', - './node_modules/app/components/markdown/markdown_image/index.js', - './node_modules/app/components/markdown/markdown_image/markdown_image.js', - './node_modules/app/components/markdown/markdown_link/index.js', - './node_modules/app/components/markdown/markdown_link/markdown_link.js', - './node_modules/app/components/markdown/markdown_list.js', - './node_modules/app/components/markdown/markdown_list_item.js', - './node_modules/app/components/markdown/markdown_table/index.js', - './node_modules/app/components/markdown/markdown_table/markdown_table.js', - './node_modules/app/components/markdown/markdown_table_cell/index.js', - './node_modules/app/components/markdown/markdown_table_cell/markdown_table_cell.js', - './node_modules/app/components/markdown/markdown_table_image/index.js', - './node_modules/app/components/markdown/markdown_table_image/markdown_table_image.js', - './node_modules/app/components/markdown/markdown_table_row/index.js', - './node_modules/app/components/markdown/markdown_table_row/markdown_table_row.js', - './node_modules/app/components/network_indicator/index.js', - './node_modules/app/components/network_indicator/network_indicator.js', + +module.exports = ['./node_modules/app/components/loading.js', './node_modules/app/components/paper_plane.js', - './node_modules/app/components/post/index.js', - './node_modules/app/components/post/post.js', - './node_modules/app/components/post_body/index.js', - './node_modules/app/components/post_body/post_body.js', - './node_modules/app/components/post_header/index.js', - './node_modules/app/components/post_header/post_header.js', - './node_modules/app/components/post_header/post_pre_header.js', - './node_modules/app/components/post_list/date_header/date_header.js', - './node_modules/app/components/post_list/date_header/index.js', - './node_modules/app/components/post_list/index.js', - './node_modules/app/components/post_list/new_messages_divider.js', - './node_modules/app/components/post_list/post_list.js', - './node_modules/app/components/post_list_retry.js', - './node_modules/app/components/post_profile_picture/index.js', - './node_modules/app/components/post_profile_picture/post_profile_picture.js', - './node_modules/app/components/post_textbox/components/typing/index.js', - './node_modules/app/components/post_textbox/components/typing/typing.js', - './node_modules/app/components/post_textbox/index.js', - './node_modules/app/components/post_textbox/post_textbox.android.js', - './node_modules/app/components/post_textbox/post_textbox_base.js', - './node_modules/app/components/profile_picture/index.js', - './node_modules/app/components/profile_picture/profile_picture.js', - './node_modules/app/components/progressive_image/index.js', - './node_modules/app/components/progressive_image/progressive_image.js', - './node_modules/app/components/reply_icon.js', - './node_modules/app/components/safe_area_view/index.js', - './node_modules/app/components/safe_area_view/safe_area_view.android.js', - './node_modules/app/components/send_button.js', - './node_modules/app/components/show_more_button/index.js', - './node_modules/app/components/show_more_button/show_more_button.js', - './node_modules/app/components/start/empty_toolbar.js', - './node_modules/app/components/status_bar/index.js', - './node_modules/app/components/status_bar/status_bar.js', - './node_modules/app/components/user_status/index.js', - './node_modules/app/components/user_status/user_status.js', - './node_modules/app/components/vector_icon.js', './node_modules/app/constants/custom_prop_types.js', './node_modules/app/constants/deep_linking.js', './node_modules/app/constants/device.js', @@ -90,7 +11,10 @@ module.exports = ['./node_modules/app/app.js', './node_modules/app/constants/navigation.js', './node_modules/app/constants/permissions.js', './node_modules/app/constants/view.js', - './node_modules/app/fetch_preconfig.js', + './node_modules/app/init/credentials.js', + './node_modules/app/init/emm_provider.js', + './node_modules/app/init/fetch.js', + './node_modules/app/init/global_event_handler.js', './node_modules/app/initial_state.js', './node_modules/app/mattermost.js', './node_modules/app/mattermost_bucket/index.js', @@ -126,40 +50,16 @@ module.exports = ['./node_modules/app/app.js', './node_modules/app/reducers/views/team.js', './node_modules/app/reducers/views/thread.js', './node_modules/app/reducers/views/user.js', - './node_modules/app/screens/channel/channel.android.js', - './node_modules/app/screens/channel/channel_base.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_drawer_button.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_nav_bar.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_search_button/index.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_title/channel_title.js', - './node_modules/app/screens/channel/channel_nav_bar/channel_title/index.js', - './node_modules/app/screens/channel/channel_nav_bar/index.js', - './node_modules/app/screens/channel/channel_nav_bar/settings_drawer_button.js', - './node_modules/app/screens/channel/channel_post_list/channel_post_list.js', - './node_modules/app/screens/channel/channel_post_list/index.js', - './node_modules/app/screens/channel/index.js', - './node_modules/app/screens/entry/entry.js', - './node_modules/app/screens/entry/index.js', - './node_modules/app/screens/index.js', - './node_modules/app/screens/select_server/index.js', - './node_modules/app/screens/select_server/select_server.js', - './node_modules/app/selectors/client_upgrade.js', + './node_modules/app/store/ephemeral_store.js', './node_modules/app/store/index.js', './node_modules/app/store/middleware.js', './node_modules/app/store/thunk.js', './node_modules/app/telemetry/index.js', './node_modules/app/telemetry/telemetry.android.js', - './node_modules/app/telemetry/telemetry_utils.js', - './node_modules/app/utils/avoid_native_bridge.js', - './node_modules/app/utils/general.js', - './node_modules/app/utils/i18n.js', './node_modules/app/utils/image_cache_manager.js', - './node_modules/app/utils/network.js', './node_modules/app/utils/push_notifications.js', './node_modules/app/utils/sentry/middleware.js', './node_modules/app/utils/theme.js', - './node_modules/app/utils/time_tracker.js', './node_modules/index.js', './node_modules/node_modules/@babel/runtime/helpers/arrayWithHoles.js', './node_modules/node_modules/@babel/runtime/helpers/assertThisInitialized.js', @@ -185,9 +85,7 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/@babel/runtime/regenerator/index.js', './node_modules/node_modules/@react-native-community/async-storage/lib/AsyncStorage.js', './node_modules/node_modules/@react-native-community/async-storage/lib/index.js', - './node_modules/node_modules/@react-native-community/netinfo/js/index.js', './node_modules/node_modules/base-64/base64.js', - './node_modules/node_modules/commonmark-react-renderer/src/commonmark-react-renderer.js', './node_modules/node_modules/component-emitter/index.js', './node_modules/node_modules/core-js/modules/_a-function.js', './node_modules/node_modules/core-js/modules/_add-to-unscopables.js', @@ -265,12 +163,10 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/core-js/modules/es6.set.js', './node_modules/node_modules/core-js/modules/es6.string.includes.js', './node_modules/node_modules/core-js/modules/es6.string.iterator.js', - './node_modules/node_modules/core-js/modules/es6.string.starts-with.js', './node_modules/node_modules/core-js/modules/es6.symbol.js', './node_modules/node_modules/core-js/modules/es7.array.includes.js', './node_modules/node_modules/core-js/modules/es7.object.define-getter.js', './node_modules/node_modules/core-js/modules/es7.object.define-setter.js', - './node_modules/node_modules/core-js/modules/es7.object.entries.js', './node_modules/node_modules/core-js/modules/es7.object.values.js', './node_modules/node_modules/core-js/modules/es7.symbol.async-iterator.js', './node_modules/node_modules/core-js/modules/web.dom.iterable.js', @@ -288,7 +184,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/fbjs/lib/keyOf.js', './node_modules/node_modules/fbjs/lib/performance.js', './node_modules/node_modules/fbjs/lib/performanceNow.js', - './node_modules/node_modules/fuse.js/dist/fuse.js', './node_modules/node_modules/global/window.js', './node_modules/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js', './node_modules/node_modules/intl-format-cache/index.js', @@ -450,10 +345,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/mattermost-redux/reducers/requests/search.js', './node_modules/node_modules/mattermost-redux/reducers/requests/teams.js', './node_modules/node_modules/mattermost-redux/reducers/requests/users.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/emojis.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/general.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/integrations.js', - './node_modules/node_modules/mattermost-redux/selectors/entities/teams.js', './node_modules/node_modules/mattermost-redux/store/configureStore.prod.js', './node_modules/node_modules/mattermost-redux/store/helpers.js', './node_modules/node_modules/mattermost-redux/store/index.js', @@ -462,16 +353,9 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/mattermost-redux/store/reducer_registry.js', './node_modules/node_modules/mattermost-redux/utils/deep_freeze.js', './node_modules/node_modules/mattermost-redux/utils/event_emitter.js', - './node_modules/node_modules/mattermost-redux/utils/file_utils.js', - './node_modules/node_modules/mattermost-redux/utils/helpers.js', './node_modules/node_modules/mattermost-redux/utils/key_mirror.js', - './node_modules/node_modules/mattermost-redux/utils/post_list.js', './node_modules/node_modules/mattermost-redux/utils/theme_utils.js', - './node_modules/node_modules/moment-timezone/index.js', - './node_modules/node_modules/moment-timezone/moment-timezone.js', - './node_modules/node_modules/moment/moment.js', './node_modules/node_modules/object-assign/index.js', - './node_modules/node_modules/pascalcase/index.js', './node_modules/node_modules/path-to-regexp/index.js', './node_modules/node_modules/path-to-regexp/node_modules/isarray/index.js', './node_modules/node_modules/promise/setimmediate/core.js', @@ -493,11 +377,8 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-intl/locale-data/index.js', './node_modules/node_modules/react-is/cjs/react-is.production.min.js', './node_modules/node_modules/react-is/index.js', - './node_modules/node_modules/react-native-button/Button.js', - './node_modules/node_modules/react-native-button/coalesceNonElementChildren.js', './node_modules/node_modules/react-native-device-info/deviceinfo.js', - './node_modules/node_modules/react-native-linear-gradient/common.js', - './node_modules/node_modules/react-native-linear-gradient/index.android.js', + './node_modules/node_modules/react-native-keychain/index.js', './node_modules/node_modules/react-native-local-auth/LocalAuth.android.js', './node_modules/node_modules/react-native-local-auth/index.js', './node_modules/node_modules/react-native-navigation/src/NativeEventsReceiver.js', @@ -517,7 +398,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js', './node_modules/node_modules/react-native/Libraries/Animated/src/Easing.js', './node_modules/node_modules/react-native/Libraries/Animated/src/components/AnimatedText.js', - './node_modules/node_modules/react-native/Libraries/Animated/src/components/AnimatedView.js', './node_modules/node_modules/react-native/Libraries/Animated/src/createAnimatedComponent.js', './node_modules/node_modules/react-native/Libraries/Animated/src/nodes/AnimatedInterpolation.js', './node_modules/node_modules/react-native/Libraries/Animated/src/nodes/AnimatedNode.js', @@ -538,13 +418,9 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Components/TextInput/TextInputState.js', './node_modules/node_modules/react-native/Libraries/Components/ToolbarAndroid/ToolbarAndroid.android.js', './node_modules/node_modules/react-native/Libraries/Components/Touchable/Touchable.js', - './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableOpacity.js', - './node_modules/node_modules/react-native/Libraries/Components/Touchable/TouchableWithoutFeedback.js', './node_modules/node_modules/react-native/Libraries/Components/View/PlatformViewPropTypes.android.js', './node_modules/node_modules/react-native/Libraries/Components/View/ReactNativeStyleAttributes.js', './node_modules/node_modules/react-native/Libraries/Components/View/ReactNativeViewAttributes.js', - './node_modules/node_modules/react-native/Libraries/Components/View/View.js', - './node_modules/node_modules/react-native/Libraries/Components/View/ViewNativeComponent.js', './node_modules/node_modules/react-native/Libraries/Core/ExceptionsManager.js', './node_modules/node_modules/react-native/Libraries/Core/InitializeCore.js', './node_modules/node_modules/react-native/Libraries/Core/ReactNativeVersion.js', @@ -581,7 +457,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Image/AssetRegistry.js', './node_modules/node_modules/react-native/Libraries/Image/AssetSourceResolver.js', './node_modules/node_modules/react-native/Libraries/Image/Image.android.js', - './node_modules/node_modules/react-native/Libraries/Image/ImageBackground.js', './node_modules/node_modules/react-native/Libraries/Image/resolveAssetSource.js', './node_modules/node_modules/react-native/Libraries/Interaction/PanResponder.js', './node_modules/node_modules/react-native/Libraries/JSInspector/InspectorAgent.js', @@ -590,10 +465,8 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/Linking/Linking.js', './node_modules/node_modules/react-native/Libraries/Network/NetInfo.js', './node_modules/node_modules/react-native/Libraries/Performance/Systrace.js', - './node_modules/node_modules/react-native/Libraries/PermissionsAndroid/PermissionsAndroid.js', './node_modules/node_modules/react-native/Libraries/Promise.js', './node_modules/node_modules/react-native/Libraries/ReactNative/AppRegistry.js', - './node_modules/node_modules/react-native/Libraries/ReactNative/FabricUIManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/I18nManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/UIManager.js', './node_modules/node_modules/react-native/Libraries/ReactNative/UIManagerProperties.js', @@ -610,7 +483,6 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/react-native/Libraries/StyleSheet/processTransform.js', './node_modules/node_modules/react-native/Libraries/Text/Text.js', './node_modules/node_modules/react-native/Libraries/Text/TextStylePropTypes.js', - './node_modules/node_modules/react-native/Libraries/Utilities/BackHandler.android.js', './node_modules/node_modules/react-native/Libraries/Utilities/DeviceInfo.js', './node_modules/node_modules/react-native/Libraries/Utilities/Dimensions.js', './node_modules/node_modules/react-native/Libraries/Utilities/HMRClient.js', @@ -694,15 +566,30 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/redux-thunk/lib/index.js', './node_modules/node_modules/redux/lib/redux.js', './node_modules/node_modules/regenerator-runtime/runtime.js', - './node_modules/node_modules/reselect/lib/index.js', './node_modules/node_modules/rn-fetch-blob/android.js', './node_modules/node_modules/rn-fetch-blob/cba/index.js', './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobFile.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobReadStream.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobSession.js', + './node_modules/node_modules/rn-fetch-blob/class/RNFetchBlobWriteStream.js', './node_modules/node_modules/rn-fetch-blob/fs.js', './node_modules/node_modules/rn-fetch-blob/index.js', './node_modules/node_modules/rn-fetch-blob/json-stream.js', './node_modules/node_modules/rn-fetch-blob/lib/oboe-browser.min.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/Blob.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/Event.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/EventTarget.js', './node_modules/node_modules/rn-fetch-blob/polyfill/Fetch.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/File.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/FileReader.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/ProgressEvent.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/XMLHttpRequest.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/XMLHttpRequestEventTarget.js', + './node_modules/node_modules/rn-fetch-blob/polyfill/index.js', + './node_modules/node_modules/rn-fetch-blob/utils/log.js', + './node_modules/node_modules/rn-fetch-blob/utils/unicode.js', + './node_modules/node_modules/rn-fetch-blob/utils/uri.js', + './node_modules/node_modules/rn-fetch-blob/utils/uuid.js', './node_modules/node_modules/rn-host-detect/index.js', './node_modules/node_modules/sc-errors/decycle.js', './node_modules/node_modules/sc-errors/index.js', @@ -712,9 +599,7 @@ module.exports = ['./node_modules/app/app.js', './node_modules/node_modules/scheduler/index.js', './node_modules/node_modules/scheduler/tracing.js', './node_modules/node_modules/semver/semver.js', - './node_modules/node_modules/shallow-equals/index.js', './node_modules/node_modules/stacktrace-parser/dist/stack-trace-parser.umd.js', './node_modules/node_modules/symbol-observable/lib/index.js', './node_modules/node_modules/symbol-observable/lib/ponyfill.js', - './node_modules/node_modules/tinycolor2/tinycolor.js', - './node_modules/node_modules/url-parse/index.js']; \ No newline at end of file + './node_modules/node_modules/url-parse/index.js']; diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index a1cfa5679..00f357d03 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -21,7 +21,6 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import Video from 'react-native-video'; import LocalAuth from 'react-native-local-auth'; import RNFetchBlob from 'rn-fetch-blob'; -import {getGenericPassword} from 'react-native-keychain'; import {Client4} from 'mattermost-redux/client'; import {Preferences} from 'mattermost-redux/constants'; @@ -30,8 +29,8 @@ import {getFormattedFileSize, lookupMimeType} from 'mattermost-redux/utils/file_ import Loading from 'app/components/loading'; import PaperPlane from 'app/components/paper_plane'; import {MAX_FILE_COUNT} from 'app/constants/post_textbox'; +import {getCurrentServerUrl, getAppCredentials} from 'app/init/credentials'; import mattermostManaged from 'app/mattermost_managed'; -import avoidNativeBridge from 'app/utils/avoid_native_bridge'; import {getExtensionFromMime} from 'app/utils/file'; import {emptyFunction} from 'app/utils/general'; import {setCSRFFromCookie} from 'app/utils/security'; @@ -49,7 +48,6 @@ import { import ChannelButton from './channel_button'; import TeamButton from './team_button'; -const {Initialization} = NativeModules; const defaultTheme = Preferences.THEMES.default; const extensionSvg = { csv: ExcelSvg, @@ -232,32 +230,18 @@ export default class ExtensionPost extends PureComponent { getAppCredentials = async () => { try { - const credentials = await avoidNativeBridge( - () => { - return Initialization.credentialsExist; - }, - () => { - return Initialization.credentials; - }, - () => { - return getGenericPassword(); - } - ); + const url = await getCurrentServerUrl(); + const credentials = await getAppCredentials(); if (credentials) { - const passwordParsed = credentials.password.split(','); + const token = credentials.password; - // password == token, url - if (passwordParsed.length === 2) { - const [token, url] = passwordParsed; - - if (url && url !== 'undefined' && token && token !== 'undefined') { - this.token = token; - this.url = url; - Client4.setUrl(url); - Client4.setToken(token); - await setCSRFFromCookie(url); - } + if (url && url !== 'undefined' && token && token !== 'undefined') { + this.token = token; + this.url = url; + Client4.setUrl(url); + Client4.setToken(token); + await setCSRFFromCookie(url); } } } catch (error) {