From cd5fb7168148914eb9e6d3f0a64fa854e55776f4 Mon Sep 17 00:00:00 2001 From: enahum Date: Fri, 18 Aug 2017 13:19:00 -0300 Subject: [PATCH] Appconfig support for iOS and Android (#856) * AppConfig support for iOS * AppConfig support for Android * Fix typo * Java feedback review --- NOTICE.txt | 93 ++++++ android/app/build.gradle | 2 + android/app/src/main/AndroidManifest.xml | 5 +- .../com/mattermost/rnbeta/MainActivity.java | 14 +- .../mattermost/rnbeta/MainApplication.java | 16 +- .../rnbeta/MattermostManagedModule.java | 65 ++++ .../rnbeta/MattermostManagedPackage.java | 28 ++ .../rnbeta/NotificationsLifecycleFacade.java | 139 +++++++- .../app/src/main/res/xml/app_restrictions.xml | 37 +++ android/settings.gradle | 4 + app/mattermost.js | 176 +++++++++- app/mattermost_managed/index.js | 6 + .../mattermost-managed.android.js | 36 ++ .../mattermost-managed.ios.js | 36 ++ app/screens/root/root.js | 13 +- app/screens/select_server/select_server.js | 13 +- assets/base/i18n/en.json | 4 + fastlane/Fastfile | 12 + fastlane/Gemfile.lock | 4 +- ios/BlurAppScreen.h | 13 + ios/BlurAppScreen.m | 75 +++++ ios/Mattermost.xcodeproj/project.pbxproj | 135 +++++++- ios/Mattermost/AppDelegate.m | 1 + ios/MattermostManaged.h | 16 + ios/MattermostManaged.m | 99 ++++++ ios/UIImage+ImageEffects.h | 112 +++++++ ios/UIImage+ImageEffects.m | 310 ++++++++++++++++++ package.json | 5 +- yarn.lock | 28 +- 29 files changed, 1458 insertions(+), 39 deletions(-) create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedPackage.java create mode 100644 android/app/src/main/res/xml/app_restrictions.xml create mode 100644 app/mattermost_managed/index.js create mode 100644 app/mattermost_managed/mattermost-managed.android.js create mode 100644 app/mattermost_managed/mattermost-managed.ios.js create mode 100644 ios/BlurAppScreen.h create mode 100644 ios/BlurAppScreen.m create mode 100644 ios/MattermostManaged.h create mode 100644 ios/MattermostManaged.m create mode 100644 ios/UIImage+ImageEffects.h create mode 100644 ios/UIImage+ImageEffects.m diff --git a/NOTICE.txt b/NOTICE.txt index 68e08b42d..f466e4913 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1154,3 +1154,96 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +## react-native-passcode-status + +This product contains 'react-native-passcode-status', A thin wrapper around UIDevice-PasscodeStatus. + +* HOMEPAGE: + * https://github.com/tradle/react-native-passcode-status + +* LICENSE: + +The MIT License (MIT) + +Copyright (c) 2015 Tradle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +## react-native-local-auth + +This product contains 'react-native-local-auth', Authenticate users with Touch ID, with optional fallback to passcode. + +* HOMEPAGE: + * https://github.com/tradle/react-native-local-auth + +* LICENSE: + +ISC License + +Copyright (c) 2015, [Tradle, Inc](http://tradle.io/) + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +## jail-monkey + +This product contains 'jail-monkey', Identify if a phone has been jail-broken or rooted for iOS/Android. + +* HOMEPAGE: + * https://github.com/GantMan/jail-monkey/ + +* LICENSE: + +MIT License + +Copyright (c) 2017 Gant Laborde + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/android/app/build.gradle b/android/app/build.gradle index d431c0e4f..4e504e9a1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -160,6 +160,8 @@ dependencies { compile project(':react-native-linear-gradient') compile project(':react-native-vector-icons') compile project(':react-native-svg') + compile project(':react-native-local-auth') + compile project(':jail-monkey') } // Run this once to be able to run the application with BUCK diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d627a78f8..6cc5eb9de 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,7 +29,10 @@ android:icon="@mipmap/ic_launcher" android:theme="@style/AppTheme" > - + + + (this); + LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + Context context = getActivity(); final int drawableId = getImageId(); - Context context = getActivity(); + NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity()); + imageView = new ImageView(context); imageView.setImageResource(drawableId); - LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); imageView.setLayoutParams(layoutParams); - imageView.setScaleType(ImageView.ScaleType.CENTER); LinearLayout view = new LinearLayout(this); - view.setBackgroundColor(Color.parseColor("#FFFFFF")); view.setGravity(Gravity.CENTER); - view.addView(imageView); + return view; } 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 7b8a04ab0..b23c2201f 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -1,12 +1,13 @@ package com.mattermost.rnbeta; import android.app.Application; -import android.util.Log; import android.support.annotation.NonNull; import android.content.Context; import android.os.Bundle; import com.facebook.react.ReactApplication; +import com.gantix.JailMonkey.JailMonkeyPackage; +import io.tradle.react.LocalAuthPackage; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; @@ -33,8 +34,7 @@ import java.util.Arrays; import java.util.List; public class MainApplication extends NavigationApplication implements INotificationsApplication { - - NotificationsLifecycleFacade notificationsLifecycleFacade; + public NotificationsLifecycleFacade notificationsLifecycleFacade; @Override public boolean isDebug() { @@ -55,20 +55,22 @@ public class MainApplication extends NavigationApplication implements INotificat new SvgPackage(), new LinearGradientPackage(), new OrientationPackage(), - new RNNotificationsPackage(MainApplication.this) + new RNNotificationsPackage(this), + new LocalAuthPackage(), + new JailMonkeyPackage(), + new MattermostManagedPackage() ); } @Override public void onCreate() { super.onCreate(); - + instance = this; // Create an object of the custom facade impl - notificationsLifecycleFacade = new NotificationsLifecycleFacade(); + notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance(); // Attach it to react-native-navigation setActivityCallbacks(notificationsLifecycleFacade); - SoLoader.init(this, /* native exopackage */ false); } diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java new file mode 100644 index 000000000..78808e2db --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -0,0 +1,65 @@ +package com.mattermost.rnbeta; + +import android.app.Application; +import android.content.Context; +import android.os.Bundle; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; + +public class MattermostManagedModule extends ReactContextBaseJavaModule { + private static MattermostManagedModule instance; + + private boolean shouldBlurAppScreen = false; + + private MattermostManagedModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) { + if (instance == null) { + instance = new MattermostManagedModule(reactContext); + } + + return instance; + } + + public static MattermostManagedModule getInstance() { + return instance; + } + + @Override + public String getName() { + return "MattermostManaged"; + } + + @ReactMethod + public void blurAppScreen(boolean enabled) { + shouldBlurAppScreen = enabled; + } + + public boolean isBlurAppScreenEnabled() { + return shouldBlurAppScreen; + } + + @ReactMethod + public void getConfig(final Promise promise) { + try { + Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig(); + + if (config != null) { + Object result = Arguments.fromBundle(config); + promise.resolve(result); + } else { + throw new Exception("The MDM vendor has not sent any Managed configuration"); + } + } catch (Exception e) { + promise.reject("no managed configuration", e); + } + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedPackage.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedPackage.java new file mode 100644 index 000000000..485ccbcc7 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedPackage.java @@ -0,0 +1,28 @@ +package com.mattermost.rnbeta; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; + +public class MattermostManagedPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(MattermostManagedModule.getInstance(reactContext)); + } + + @Override + 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/NotificationsLifecycleFacade.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java index a34e975df..8faba1f5a 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationsLifecycleFacade.java @@ -1,9 +1,20 @@ package com.mattermost.rnbeta; import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.RestrictionsManager; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; import android.util.Log; +import android.util.ArraySet; +import android.view.WindowManager.LayoutParams; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.core.DeviceEventManagerModule; + import com.reactnativenavigation.NavigationApplication; import com.reactnativenavigation.controllers.ActivityCallbacks; import com.reactnativenavigation.react.ReactGateway; @@ -12,16 +23,72 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; + public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade { - private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName(); + private static NotificationsLifecycleFacade instance; + private Bundle managedConfig = null; private Activity mVisibleActivity; private Set mListeners = new CopyOnWriteArraySet<>(); + private final IntentFilter restrictionsFilter = + new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + + private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + + if (mVisibleActivity != null) { + // Get the current configuration bundle + RestrictionsManager myRestrictionsMgr = + (RestrictionsManager) mVisibleActivity + .getSystemService(Context.RESTRICTIONS_SERVICE); + managedConfig = myRestrictionsMgr.getApplicationRestrictions(); + + // Check current configuration settings, change your app's UI and + // functionality as necessary. + Log.i("ReactNative", "Managed Configuration Changed"); + sendConfigChanged(managedConfig); + } + } + }; + + public static NotificationsLifecycleFacade getInstance() { + if (instance == null) { + instance = new NotificationsLifecycleFacade(); + } + + return instance; + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + MattermostManagedModule managedModule = MattermostManagedModule.getInstance(); + if (managedModule != null && managedModule.isBlurAppScreenEnabled()) { + activity.getWindow().setFlags(LayoutParams.FLAG_SECURE, + LayoutParams.FLAG_SECURE); + } + if (managedConfig!= null && managedConfig.size() > 0) { + activity.registerReceiver(restrictionsReceiver, restrictionsFilter); + } + } + @Override public void onActivityResumed(Activity activity) { switchToVisible(activity); + + if (managedConfig != null && managedConfig.size() > 0) { + RestrictionsManager myRestrictionsMgr = + (RestrictionsManager) activity + .getSystemService(Context.RESTRICTIONS_SERVICE); + + Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions(); + if (!equalBundles(newConfig ,managedConfig)) { + Log.i("ReactNative", "onResumed Managed Configuration Changed"); + managedConfig = newConfig; + sendConfigChanged(managedConfig); + } + } } @Override @@ -32,6 +99,13 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A @Override public void onActivityStopped(Activity activity) { switchToInvisible(activity); + if (managedConfig != null && managedConfig.size() > 0) { + try { + activity.unregisterReceiver(restrictionsReceiver); + } catch (IllegalArgumentException e) { + // Just ignore this cause the receiver wasn't registered for this activity + } + } } @Override @@ -88,4 +162,67 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A } } } + + public synchronized void LoadManagedConfig(Activity activity) { + RestrictionsManager myRestrictionsMgr = + (RestrictionsManager) activity + .getSystemService(Context.RESTRICTIONS_SERVICE); + + managedConfig = myRestrictionsMgr.getApplicationRestrictions(); + myRestrictionsMgr = null; + } + + public synchronized Bundle getManagedConfig() { + if (managedConfig!= null && managedConfig.size() > 0) { + return managedConfig; + } + + if (mVisibleActivity != null) { + LoadManagedConfig(mVisibleActivity); + return managedConfig; + } + + return null; + } + + public void sendConfigChanged(Bundle config) { + Object result = Arguments.fromBundle(config); + getRunningReactContext(). + getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class). + emit("managedConfigDidChange", result); + } + + private boolean equalBundles(Bundle one, Bundle two) { + if (one == null || two == null) + return false; + + if(one.size() != two.size()) + return false; + + Set setOne = new ArraySet(); + setOne.addAll(one.keySet()); + setOne.addAll(two.keySet()); + Object valueOne; + Object valueTwo; + + for(String key : setOne) { + if (!one.containsKey(key) || !two.containsKey(key)) + return false; + + valueOne = one.get(key); + valueTwo = two.get(key); + if(valueOne instanceof Bundle && valueTwo instanceof Bundle && + !equalBundles((Bundle) valueOne, (Bundle) valueTwo)) { + return false; + } + else if(valueOne == null) { + if(valueTwo != null) + return false; + } + else if(!valueOne.equals(valueTwo)) + return false; + } + + return true; + } } diff --git a/android/app/src/main/res/xml/app_restrictions.xml b/android/app/src/main/res/xml/app_restrictions.xml new file mode 100644 index 000000000..608e8a56d --- /dev/null +++ b/android/app/src/main/res/xml/app_restrictions.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/android/settings.gradle b/android/settings.gradle index 1f8b709f2..0cb14b138 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'Mattermost' +include ':jail-monkey' +project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android') +include ':react-native-local-auth' +project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android') include ':react-native-navigation' project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/') include ':react-native-image-picker' diff --git a/app/mattermost.js b/app/mattermost.js index 4940a2546..e7a65bf26 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -28,19 +28,30 @@ import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client, Client4} from 'mattermost-redux/client'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; -import {goToNotification, loadConfigAndLicense, queueNotification, setStatusBarHeight, purgeOfflineStore} from 'app/actions/views/root'; +import { + goToNotification, + loadConfigAndLicense, + queueNotification, + setStatusBarHeight, + purgeOfflineStore +} from 'app/actions/views/root'; import {setChannelDisplayName} from 'app/actions/views/channel'; +import {handleLoginIdChanged} from 'app/actions/views/login'; +import {handleServerUrlChanged} from 'app/actions/views/select_server'; import {NavigationTypes, ViewTypes} from 'app/constants'; import {getTranslations} from 'app/i18n'; import initialState from 'app/initial_state'; import PushNotifications from 'app/push_notifications'; import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; +import mattermostManaged from 'app/mattermost_managed'; import Config from 'assets/config'; const {StatusBarManager} = NativeModules; const store = configureStore(initialState); +const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000; + registerScreens(store, Provider); export default class Mattermost { @@ -52,6 +63,7 @@ export default class Mattermost { console.ignoredYellowBox = ['`scaleY`']; //eslint-disable-line } this.isConfigured = false; + this.allowOtherServers = true; setJSExceptionHandler(this.errorHandler, false); Orientation.lockToPortrait(); this.unsubscribeFromStore = store.subscribe(this.listenForHydration); @@ -61,6 +73,8 @@ export default class Mattermost { EventEmitter.on(General.DEFAULT_CHANNEL, this.handleResetDisplayName); EventEmitter.on(NavigationTypes.RESTART_APP, this.restartApp); + mattermostManaged.addEventListener('managedConfigDidChange', this.handleManagedConfig); + this.handleAppStateChange(AppState.currentState); Client4.setUserAgent(DeviceInfo.getUserAgent()); @@ -140,9 +154,52 @@ export default class Mattermost { }); }; - handleAppStateChange = (appState) => { + handleAppStateChange = async (appState) => { const {dispatch, getState} = store; - setAppState(appState === 'active')(dispatch, getState); + const isActive = appState === 'active'; + setAppState(isActive)(dispatch, getState); + try { + if (!isActive && !this.inBackgroundSince) { + this.inBackgroundSince = Date.now(); + } else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) { + this.inBackgroundSince = null; + if (this.mdmEnabled) { + const config = await mattermostManaged.getConfig(); + const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + if (authNeeded) { + const authenticated = await this.handleAuthentication(config.vendor); + if (!authenticated) { + mattermostManaged.quitApp(); + } + } + } + } + } catch (error) { + // do nothing + } + }; + + handleAuthentication = async (vendor) => { + const isSecured = await mattermostManaged.isDeviceSecure(); + + const intl = this.getIntl(); + if (isSecured) { + try { + await mattermostManaged.authenticate({ + reason: intl.formatMessage({ + id: 'mobile.managed.secured_by', + defaultMessage: 'Secured by {vendor}' + }, {vendor}), + fallbackToPasscode: true, + suppressEnterPassword: true + }); + } catch (err) { + mattermostManaged.quitApp(); + return false; + } + } + + return true; }; handleConfigChanged = async (serverVersion) => { @@ -169,6 +226,94 @@ export default class Mattermost { } }; + handleManagedConfig = async (serverConfig) => { + 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; + + try { + const config = await mattermostManaged.getConfig(); + if (config) { + this.mdmEnabled = true; + authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + blurApplicationScreen = config.blurApplicationScreen && config.blurApplicationScreen === 'true'; + jailbreakProtection = config.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') { + this.allowOtherServers = false; + } + } + } + } catch (error) { + return true; + } + + if (this.mdmEnabled) { + if (jailbreakProtection) { + const isTrusted = mattermostManaged.isTrustedDevice(); + + if (!isTrusted) { + const intl = this.getIntl(); + Alert.alert( + intl.formatMessage({ + id: 'mobile.managed.blocked_by', + defaultMessage: 'Blocked by {vendor}' + }, {vendor}), + intl.formatMessage({ + id: 'mobile.managed.jailbreak', + defaultMessage: 'Jailbroken devices are not trusted by {vendor}, please exit the app.' + }, {vendor}), + [{ + text: intl.formatMessage({id: 'mobile.managed.exit', defaultMessage: 'Exit'}), + style: 'destructive', + onPress: () => { + mattermostManaged.quitApp(); + } + }], + {cancelable: false} + ); + return false; + } + } + + if (authNeeded && !serverConfig) { + if (Platform.OS === 'android') { + //Start a fake app as we need at least one activity for android + await this.startFakeApp(); + } + const authenticated = await this.handleAuthentication(vendor); + if (!authenticated) { + return false; + } + } + + if (blurApplicationScreen) { + mattermostManaged.blurAppScreen(true); + } + + if (serverUrl) { + handleServerUrlChanged(serverUrl)(dispatch, getState); + } + + if (username) { + handleLoginIdChanged(username)(dispatch, getState); + } + } + + return true; + }; + handleReset = () => { this.resetBadgeAndVersion(); this.startApp('fade'); @@ -202,7 +347,11 @@ export default class Mattermost { const state = store.getState(); if (state.views.root.hydrationComplete) { this.unsubscribeFromStore(); - this.startApp(); + this.handleManagedConfig().then((shouldStart) => { + if (shouldStart) { + this.startApp(); + } + }); } }; @@ -274,6 +423,22 @@ export default class Mattermost { this.startApp('fade'); }; + startFakeApp = async () => { + return Navigation.startSingleScreenApp({ + screen: { + screen: 'Root', + navigatorStyle: { + navBarHidden: true, + statusBarHidden: false, + statusBarHideWithNavBar: false + } + }, + passProps: { + justInit: true + } + }); + }; + startApp = (animationType = 'none') => { Navigation.startSingleScreenApp({ screen: { @@ -284,6 +449,9 @@ export default class Mattermost { statusBarHideWithNavBar: false } }, + passProps: { + allowOtherServers: this.allowOtherServers + }, animationType }); diff --git a/app/mattermost_managed/index.js b/app/mattermost_managed/index.js new file mode 100644 index 000000000..64af5be50 --- /dev/null +++ b/app/mattermost_managed/index.js @@ -0,0 +1,6 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// Used to leverage the platform specific components +import MattermostManaged from './mattermost-managed'; +export default MattermostManaged; diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js new file mode 100644 index 000000000..8779fb209 --- /dev/null +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -0,0 +1,36 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {BackHandler, NativeModules, DeviceEventEmitter} from 'react-native'; +import LocalAuth from 'react-native-local-auth'; +import JailMonkey from 'jail-monkey'; + +const {MattermostManaged} = NativeModules; + +export default { + addEventListener: (name, callback) => { + DeviceEventEmitter.addListener(name, (config) => { + if (callback && typeof callback === 'function') { + callback(config); + } + }); + }, + authenticate: LocalAuth.authenticate, + blurAppScreen: MattermostManaged.blurAppScreen, + getConfig: MattermostManaged.getConfig, + isDeviceSecure: async () => { + try { + return await LocalAuth.isDeviceSecure(); + } catch (err) { + return false; + } + }, + isTrustedDevice: () => { + if (__DEV__) { //eslint-disable-line no-undef + return true; + } + + return JailMonkey.trustFall(); + }, + quitApp: BackHandler.exitApp +}; diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js new file mode 100644 index 000000000..34eec68e9 --- /dev/null +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -0,0 +1,36 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +import {NativeModules, NativeEventEmitter} from 'react-native'; +import LocalAuth from 'react-native-local-auth'; +import JailMonkey from 'jail-monkey'; + +const {BlurAppScreen, MattermostManaged} = NativeModules; +const MattermostManagedEvents = new NativeEventEmitter(MattermostManaged); + +export default { + addEventListener: (name, callback) => { + MattermostManagedEvents.addListener(name, (config) => { + if (callback && typeof callback === 'function') { + callback(config); + } + }); + }, + authenticate: LocalAuth.authenticate, + blurAppScreen: BlurAppScreen.enabled, + getConfig: MattermostManaged.getConfig, + isDeviceSecure: async () => { + try { + return await LocalAuth.isDeviceSecure(); + } catch (err) { + return false; + } + }, + isTrustedDevice: () => { + if (__DEV__) { //eslint-disable-line no-undef + return true; + } + + return JailMonkey.trustFall(); + }, + quitApp: MattermostManaged.quitApp +}; diff --git a/app/screens/root/root.js b/app/screens/root/root.js index 04ae48b25..261a66f9f 100644 --- a/app/screens/root/root.js +++ b/app/screens/root/root.js @@ -12,7 +12,9 @@ import {stripTrailingSlashes} from 'app/utils/url'; export default class Root extends PureComponent { static propTypes = { + allowOtherServers: PropTypes.bool, credentials: PropTypes.object, + justInit: PropTypes.bool, loginRequest: PropTypes.object, navigator: PropTypes.object, theme: PropTypes.object, @@ -22,7 +24,9 @@ export default class Root extends PureComponent { }; componentDidMount() { - this.loadStoreAndScene(); + if (!this.props.justInit) { + this.loadStoreAndScene(); + } } goToLoadTeam = () => { @@ -44,7 +48,9 @@ export default class Root extends PureComponent { }; goToSelectServer = () => { - this.props.navigator.resetTo({ + const {allowOtherServers, navigator} = this.props; + + navigator.resetTo({ screen: 'SelectServer', animated: false, navigatorStyle: { @@ -52,6 +58,9 @@ export default class Root extends PureComponent { navBarBackgroundColor: 'black', statusBarHidden: false, statusBarHideWithNavBar: false + }, + passProps: { + allowOtherServers } }); }; diff --git a/app/screens/select_server/select_server.js b/app/screens/select_server/select_server.js index 1a4959cff..7d6612a7b 100644 --- a/app/screens/select_server/select_server.js +++ b/app/screens/select_server/select_server.js @@ -32,6 +32,7 @@ import logo from 'assets/images/logo.png'; class SelectServer extends PureComponent { static propTypes = { + allowOtherServers: PropTypes.bool, navigator: PropTypes.object, intl: intlShape.isRequired, config: PropTypes.object, @@ -59,6 +60,13 @@ class SelectServer extends PureComponent { } componentDidMount() { + const {allowOtherServers, pingRequest, serverUrl} = this.props; + if (pingRequest.status === RequestStatus.NOT_STARTED && !allowOtherServers && serverUrl) { + // If the app is managed, the server url is set and the user can't change it + // we automatically trigger the ping to move to the next screen + this.onClick(); + } + if (Platform.OS === 'android') { Keyboard.addListener('keyboardDidHide', this.handleAndroidKeyboard); } @@ -151,7 +159,7 @@ class SelectServer extends PureComponent { }; render() { - const {serverUrl, pingRequest, configRequest, licenseRequest} = this.props; + const {allowOtherServers, serverUrl, pingRequest, configRequest, licenseRequest} = this.props; const isLoading = pingRequest.status === RequestStatus.STARTED || configRequest.status === RequestStatus.STARTED || licenseRequest.status === RequestStatus.STARTED; @@ -203,9 +211,10 @@ class SelectServer extends PureComponent { = 0.0.5, < 1.0.0) dotenv (2.2.1) excon (0.58.0) - faraday (0.12.2) + faraday (0.13.0) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) faraday (>= 0.7.4) @@ -24,7 +24,7 @@ GEM faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) fastimage (2.1.0) - fastlane (2.50.1) + fastlane (2.53.1) CFPropertyList (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) diff --git a/ios/BlurAppScreen.h b/ios/BlurAppScreen.h new file mode 100644 index 000000000..2514843de --- /dev/null +++ b/ios/BlurAppScreen.h @@ -0,0 +1,13 @@ +// +// BlurAppScreen.h +// Mattermost +// +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +// + +#import + +@interface BlurAppScreen : NSObject + +@end diff --git a/ios/BlurAppScreen.m b/ios/BlurAppScreen.m new file mode 100644 index 000000000..32d23a319 --- /dev/null +++ b/ios/BlurAppScreen.m @@ -0,0 +1,75 @@ +// +// BlurAppScreen.m +// Mattermost +// +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +// + +#import "BlurAppScreen.h" +#import "UIImage+ImageEffects.h" + +@implementation BlurAppScreen{ + BOOL enabled; + UIImageView *obfuscatingView; +} + +RCT_EXPORT_MODULE(); + +- (instancetype)init { + if ((self = [super init])) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleAppStateResignActive) + name:UIApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleAppStateActive) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil]; +} + +- (void)handleAppStateResignActive { + if (self->enabled) { + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; + UIImageView *blurredScreenImageView = [[UIImageView alloc] initWithFrame:keyWindow.bounds]; + + UIGraphicsBeginImageContext(keyWindow.bounds.size); + [keyWindow drawViewHierarchyInRect:keyWindow.frame afterScreenUpdates:NO]; + UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + blurredScreenImageView.image = [viewImage applyLightEffect]; + + self->obfuscatingView = blurredScreenImageView; + [[UIApplication sharedApplication].keyWindow addSubview:self->obfuscatingView]; + + } +} + +- (void)handleAppStateActive { + if (self->obfuscatingView) { + [UIView animateWithDuration: 0.3 + animations: ^ { + self->obfuscatingView.alpha = 0; + } + completion: ^(BOOL finished) { + [self->obfuscatingView removeFromSuperview]; + self->obfuscatingView = nil; + } + ]; + } +} + +RCT_EXPORT_METHOD(enabled:(BOOL) _enable) { + self->enabled = _enable; +} + +@end diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index f12a9c0d6..b4c5245dd 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -30,18 +30,23 @@ 2E3655C7E15049DBBC3A666B /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DFE628F5A526417D85AD635E /* libRNImagePicker.a */; }; 374634801E8480C2005E1244 /* libRCTOrientation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 374634671E848085005E1244 /* libRCTOrientation.a */; }; 382D94CF15EE4FC292C3F341 /* Foundation.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51F5A483EA9F4A9A87ACFB59 /* Foundation.ttf */; }; + 38FB8B0A3AAD4F839EF6EF0B /* libJailMonkey.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 77810F0A063349439B0D8B6B /* libJailMonkey.a */; }; 3D38ABA732A34A9BB3294F90 /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 349FBA7338E74D9BBD709528 /* EvilIcons.ttf */; }; 420A7328E12C4B72AEF420CE /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = EDC04CBCF81642219D199CBB /* Octicons.ttf */; }; 55C6561DDBBA45929D88B6D1 /* OpenSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 32AC3D4EA79E44738A6E9766 /* OpenSans-BoldItalic.ttf */; }; 584837D6B55F405F908A2053 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ACC6B9FDC0AD45A6BFA4FBCD /* libBVLinearGradient.a */; }; 5E1AF7B72B8D4A4E9E53FF9D /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 005346E5C0E542BFABAE1411 /* FontAwesome.ttf */; }; 5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; }; + 615D2E7805814A3A9B9C69EC /* libRNLocalAuth.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EE3EE4548D3F4A49B1274722 /* libRNLocalAuth.a */; }; 62A8448264674B4D95A5A7C2 /* OpenSans-Semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */; }; 69AC753E496743BABB7A7124 /* OpenSans-SemiboldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0E617BF0F36D4E738F51D169 /* OpenSans-SemiboldItalic.ttf */; }; + 7F1B28A31F3A76BB00701A6A /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F1B28A21F3A76BB00701A6A /* MattermostManaged.m */; }; 7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F2691A01EE1DC51007574FE /* libRNNotifications.a */; }; 7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; }; 7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */; }; 7F292AA71E8ABB1100A450A3 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA51E8ABB1100A450A3 /* splash.png */; }; + 7F5052F61F3F3C1C00FE9812 /* UIImage+ImageEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F5052F51F3F3C1C00FE9812 /* UIImage+ImageEffects.m */; }; + 7F5053221F3F57AC00FE9812 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F5053211F3F57AC00FE9812 /* BlurAppScreen.m */; }; 7F6877B31E7836070094B63F /* libToolTipMenu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F6877B01E7835E50094B63F /* libToolTipMenu.a */; }; 7FBB5E9A1E1F5A4B000DE18A /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */; }; 7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; }; @@ -56,6 +61,7 @@ D719A67137964F08BE47A5FC /* OpenSans-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3647DF63D6764CF093375861 /* OpenSans-ExtraBold.ttf */; }; F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 356C9186FA374641A00EB2EA /* MaterialIcons.ttf */; }; F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; }; + FDEF24E9D9AB47728E11033B /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A75F5A3D0D4A4A995DCD76 /* libRNPasscodeStatus.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -297,6 +303,27 @@ remoteGlobalIDString = 4681C02C1B05271A004D67D4; remoteInfo = ToolTipMenuTests; }; + 7F8C49861F3DFC30003A22BA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RNLocalAuth; + }; + 7F8C49F71F3E0710003A22BA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 732D5BB41C30EA5700CA0A32; + remoteInfo = RNPasscodeStatus; + }; + 7F8C4A611F3E21FB003A22BA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 38C138441D3FC3F600A8162D; + remoteInfo = JailMonkey; + }; 7F906D961E6A1ADF00B8F49A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */; @@ -369,6 +396,8 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Mattermost/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Mattermost/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 17A75F5A3D0D4A4A995DCD76 /* libRNPasscodeStatus.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNPasscodeStatus.a; sourceTree = ""; }; + 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = JailMonkey.xcodeproj; path = "../node_modules/jail-monkey/JailMonkey.xcodeproj"; sourceTree = ""; }; 2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = ""; }; 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = ""; }; 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = ""; }; @@ -382,12 +411,19 @@ 5C3B95629BA74FB3A8377CB7 /* RCTOrientation.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTOrientation.xcodeproj; path = "../node_modules/react-native-orientation/iOS/RCTOrientation.xcodeproj"; sourceTree = ""; }; 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = ""; }; 6561AEAC21CC40B8A72ABB93 /* OpenSans-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Light.ttf"; path = "../assets/fonts/OpenSans-Light.ttf"; sourceTree = ""; }; + 77810F0A063349439B0D8B6B /* libJailMonkey.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libJailMonkey.a; sourceTree = ""; }; 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = BVLinearGradient.xcodeproj; path = "../node_modules/react-native-linear-gradient/BVLinearGradient.xcodeproj"; sourceTree = ""; }; + 7F1B28A11F3A76BB00701A6A /* MattermostManaged.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MattermostManaged.h; sourceTree = ""; }; + 7F1B28A21F3A76BB00701A6A /* MattermostManaged.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MattermostManaged.m; sourceTree = ""; }; 7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNNotifications.xcodeproj; path = "../node_modules/react-native-notifications/RNNotifications/RNNotifications.xcodeproj"; sourceTree = ""; }; 7F292A701E8AB73400A450A3 /* SplashScreenResource */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SplashScreenResource; sourceTree = ""; }; 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = LaunchScreen.xib; path = SplashScreenResource/LaunchScreen.xib; sourceTree = ""; }; 7F292AA51E8ABB1100A450A3 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = splash.png; path = SplashScreenResource/splash.png; sourceTree = ""; }; + 7F5052F41F3F3C1C00FE9812 /* UIImage+ImageEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+ImageEffects.h"; sourceTree = ""; }; + 7F5052F51F3F3C1C00FE9812 /* UIImage+ImageEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+ImageEffects.m"; sourceTree = ""; }; + 7F5053201F3F57AC00FE9812 /* BlurAppScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlurAppScreen.h; sourceTree = ""; }; + 7F5053211F3F57AC00FE9812 /* BlurAppScreen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlurAppScreen.m; sourceTree = ""; }; 7F63D27B1E6C957C001FAE12 /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = ""; }; 7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = ""; }; 7F6877AA1E7835E50094B63F /* ToolTipMenu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ToolTipMenu.xcodeproj; path = "../node_modules/react-native-tooltip/ToolTipMenu.xcodeproj"; sourceTree = ""; }; @@ -400,9 +436,12 @@ BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = ""; }; BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = ""; }; C78A387124874496AD2C1466 /* OpenSans-Semibold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Semibold.ttf"; path = "../assets/fonts/OpenSans-Semibold.ttf"; sourceTree = ""; }; + D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNLocalAuth.xcodeproj; path = "../node_modules/react-native-local-auth/RNLocalAuth.xcodeproj"; sourceTree = ""; }; D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Bold.ttf"; path = "../assets/fonts/OpenSans-Bold.ttf"; sourceTree = ""; }; DFE628F5A526417D85AD635E /* libRNImagePicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNImagePicker.a; sourceTree = ""; }; + EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNPasscodeStatus.xcodeproj; path = "../node_modules/react-native-passcode-status/ios/RNPasscodeStatus.xcodeproj"; sourceTree = ""; }; EDC04CBCF81642219D199CBB /* Octicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Octicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = ""; }; + EE3EE4548D3F4A49B1274722 /* libRNLocalAuth.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNLocalAuth.a; sourceTree = ""; }; EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; }; F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; }; FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = ""; }; @@ -442,6 +481,9 @@ 2E3655C7E15049DBBC3A666B /* libRNImagePicker.a in Frameworks */, 7FC200E81EBB65370099331B /* libReactNativeNavigation.a in Frameworks */, 7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */, + 615D2E7805814A3A9B9C69EC /* libRNLocalAuth.a in Frameworks */, + FDEF24E9D9AB47728E11033B /* libRNPasscodeStatus.a in Frameworks */, + 38FB8B0A3AAD4F839EF6EF0B /* libJailMonkey.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -563,6 +605,12 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, + 7F1B28A11F3A76BB00701A6A /* MattermostManaged.h */, + 7F1B28A21F3A76BB00701A6A /* MattermostManaged.m */, + 7F5052F41F3F3C1C00FE9812 /* UIImage+ImageEffects.h */, + 7F5052F51F3F3C1C00FE9812 /* UIImage+ImageEffects.m */, + 7F5053201F3F57AC00FE9812 /* BlurAppScreen.h */, + 7F5053211F3F57AC00FE9812 /* BlurAppScreen.m */, ); name = Mattermost; sourceTree = ""; @@ -658,6 +706,30 @@ name = Products; sourceTree = ""; }; + 7F8C49621F3DFC30003A22BA /* Products */ = { + isa = PBXGroup; + children = ( + 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */, + ); + name = Products; + sourceTree = ""; + }; + 7F8C49D11F3E070F003A22BA /* Products */ = { + isa = PBXGroup; + children = ( + 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */, + ); + name = Products; + sourceTree = ""; + }; + 7F8C4A5D1F3E21FB003A22BA /* Products */ = { + isa = PBXGroup; + children = ( + 7F8C4A621F3E21FB003A22BA /* libJailMonkey.a */, + ); + name = Products; + sourceTree = ""; + }; 7F906D7A1E6A1ADF00B8F49A /* Products */ = { isa = PBXGroup; children = ( @@ -715,6 +787,9 @@ 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */, 5C3B95629BA74FB3A8377CB7 /* RCTOrientation.xcodeproj */, 85E82BCA0FD245EEA520D106 /* RNImagePicker.xcodeproj */, + D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */, + EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */, + 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -831,6 +906,10 @@ ProductGroup = 37D8FEBE1E80B5230091F3BD /* Products */; ProjectRef = 7DCC3D826CE640AF8F491692 /* BVLinearGradient.xcodeproj */; }, + { + ProductGroup = 7F8C4A5D1F3E21FB003A22BA /* Products */; + ProjectRef = 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */; + }, { ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; @@ -899,10 +978,18 @@ ProductGroup = 3779BE7C1EB1235400D081C1 /* Products */; ProjectRef = 85E82BCA0FD245EEA520D106 /* RNImagePicker.xcodeproj */; }, + { + ProductGroup = 7F8C49621F3DFC30003A22BA /* Products */; + ProjectRef = D0281D64B98143668D6AD42B /* RNLocalAuth.xcodeproj */; + }, { ProductGroup = 7F26919C1EE1DC51007574FE /* Products */; ProjectRef = 7F26919B1EE1DC51007574FE /* RNNotifications.xcodeproj */; }, + { + ProductGroup = 7F8C49D11F3E070F003A22BA /* Products */; + ProjectRef = EBA6063A99C141098D40C67A /* RNPasscodeStatus.xcodeproj */; + }, { ProductGroup = 7FDF28C41E1F4B1F00DBBE56 /* Products */; ProjectRef = 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */; @@ -1156,6 +1243,27 @@ remoteRef = 7F6877B11E7835E50094B63F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F8C49871F3DFC30003A22BA /* libRNLocalAuth.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNLocalAuth.a; + remoteRef = 7F8C49861F3DFC30003A22BA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7F8C49F81F3E0710003A22BA /* libRNPasscodeStatus.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRNPasscodeStatus.a; + remoteRef = 7F8C49F71F3E0710003A22BA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 7F8C4A621F3E21FB003A22BA /* libJailMonkey.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libJailMonkey.a; + remoteRef = 7F8C4A611F3E21FB003A22BA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 7F906D971E6A1ADF00B8F49A /* libRNCookieManagerIOS.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; @@ -1266,6 +1374,9 @@ files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, + 7F5053221F3F57AC00FE9812 /* BlurAppScreen.m in Sources */, + 7F5052F61F3F3C1C00FE9812 /* UIImage+ImageEffects.m in Sources */, + 7F1B28A31F3A76BB00701A6A /* MattermostManaged.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1291,7 +1402,12 @@ INFOPLIST_FILE = MattermostTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mattermost.app/Mattermost"; @@ -1306,7 +1422,12 @@ INFOPLIST_FILE = MattermostTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - LIBRARY_SEARCH_PATHS = "$(inherited)"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + "\"$(SRCROOT)/$(TARGET_NAME)\"", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Mattermost.app/Mattermost"; @@ -1323,6 +1444,7 @@ CURRENT_PROJECT_VERSION = 47; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", @@ -1330,6 +1452,10 @@ "$(SRCROOT)/../node_modules/react-native-image-picker/ios", "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", + "$(SRCROOT)/../node_modules/react-native-local-auth", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", + "$(SRCROOT)/../node_modules/react-native/Libraries/**", ); INFOPLIST_FILE = Mattermost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1356,6 +1482,7 @@ CURRENT_PROJECT_VERSION = 47; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; + ENABLE_BITCODE = NO; HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../node_modules/react-native/Libraries/PushNotificationIOS/**", "$(SRCROOT)/../node_modules/react-native-orientation/iOS/RCTOrientation/**", @@ -1363,6 +1490,10 @@ "$(SRCROOT)/../node_modules/react-native-image-picker/ios", "$(SRCROOT)/../node_modules/react-native-navigation/ios/**", "$(SRCROOT)/../node_modules/react-native-notifications/RNNotifications/**", + "$(SRCROOT)/../node_modules/react-native-local-auth", + "$(SRCROOT)/../node_modules/react-native-passcode-status/ios", + "$(SRCROOT)/../node_modules/jail-monkey/JailMonkey", + "$(SRCROOT)/../node_modules/react-native/Libraries/**", ); INFOPLIST_FILE = Mattermost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 32a1b3b44..0dfe7074b 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -13,6 +13,7 @@ #import "Orientation.h" #import "RCCManager.h" #import "RNNotifications.h" +#import "MattermostManaged.h" @implementation AppDelegate diff --git a/ios/MattermostManaged.h b/ios/MattermostManaged.h new file mode 100644 index 000000000..779101e11 --- /dev/null +++ b/ios/MattermostManaged.h @@ -0,0 +1,16 @@ +// +// MattermostManaged.h +// Mattermost +// +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +// + +#import + + +@interface MattermostManaged : RCTEventEmitter + ++ (void)sendConfigChangedEvent; + +@end diff --git a/ios/MattermostManaged.m b/ios/MattermostManaged.m new file mode 100644 index 000000000..c2fc0dd0a --- /dev/null +++ b/ios/MattermostManaged.m @@ -0,0 +1,99 @@ +// +// MattermostManaged.m +// Mattermost +// +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. +// + +#import "RCTTextField.h" +#import "MattermostManaged.h" + +@implementation MattermostManaged + +RCT_EXPORT_MODULE(); + +-(void)startObserving { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(managedConfigDidChange:) name:@"managedConfigDidChange" object:nil]; + [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification + object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + [self remoteConfigChanged]; + }]; +} + +- (void)stopObserving +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSNotificationCenter defaultCenter] removeObserver:NSUserDefaultsDidChangeNotification]; +} + ++ (void)sendConfigChangedEvent { + [[NSNotificationCenter defaultCenter] postNotificationName:@"managedConfigDidChange" + object:self + userInfo:nil]; +} + +- (NSArray *)supportedEvents +{ + return @[@"managedConfigDidChange"]; +} + +// The Managed app configuration dictionary pushed down from an MDM server are stored in this key. +static NSString * const configurationKey = @"com.apple.configuration.managed"; + +// The dictionary that is sent back to the MDM server as feedback must be stored in this key. +static NSString * const feedbackKey = @"com.apple.feedback.managed"; + + +- (void)managedConfigDidChange:(NSNotification *)notification +{ + NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; + [self sendEventWithName:@"managedConfigDidChange" body:response]; +} + +- (void) remoteConfigChanged { + NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; + [self sendEventWithName:@"managedConfigDidChange" body:response]; +} + +RCT_EXPORT_METHOD(getConfig:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; + if (response) { + resolve(response); + } + else { + NSError *error = [NSError errorWithDomain:@"Mattermost Managed" code:-1 userInfo:nil]; + reject(@"no managed configuration", @"The MDM vendor has not sent any Managed configuration", error); + } +} + +RCT_EXPORT_METHOD(quitApp) +{ + exit(0); +} + +@end + +@implementation RCTTextField (DisableCopyPaste) + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; + if(response) { + NSString *copyPasteProtection = response[@"copyAndPasteProtection"]; + BOOL prevent = action == @selector(paste:) || + action == @selector(copy:) || + action == @selector(cut:) || + action == @selector(_share:); + + if ([copyPasteProtection isEqual: @"true"] && prevent) { + return NO; + } + } + + return [super canPerformAction:action withSender:sender]; +} + +@end diff --git a/ios/UIImage+ImageEffects.h b/ios/UIImage+ImageEffects.h new file mode 100644 index 000000000..f689b1597 --- /dev/null +++ b/ios/UIImage+ImageEffects.h @@ -0,0 +1,112 @@ +/* + File: UIImage+ImageEffects.h + Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur. + Version: 1.0 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple + Inc. ("Apple") in consideration of your agreement to the following + terms, and your use, installation, modification or redistribution of + this Apple software constitutes acceptance of these terms. If you do + not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a personal, non-exclusive + license, under Apple's copyrights in this original Apple software (the + "Apple Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE + MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION + THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2013 Apple Inc. All Rights Reserved. + + + Copyright © 2013 Apple Inc. All rights reserved. + WWDC 2013 License + + NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013 + Session. Please refer to the applicable WWDC 2013 Session for further + information. + + IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and + your use, installation, modification or redistribution of this Apple + software constitutes acceptance of these terms. If you do not agree with + these terms, please do not use, install, modify or redistribute this + Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple + Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES + NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE + IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + EA1002 + 5/3/2013 + */ + +@import UIKit; + +@interface UIImage (ImageEffects) + +- (UIImage *)applyLightEffect; +- (UIImage *)applyExtraLightEffect; +- (UIImage *)applyDarkEffect; +- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor; + +- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage; + +- (UIImage *)rotateByDegrees:(CGFloat)degrees; + ++ (UIImage *)imagePixelFromColor:(UIColor *)color; ++ (UIImage *)imageFromColor:(UIColor *)color withSize:(CGSize)size; + +@end diff --git a/ios/UIImage+ImageEffects.m b/ios/UIImage+ImageEffects.m new file mode 100644 index 000000000..4d19e3a42 --- /dev/null +++ b/ios/UIImage+ImageEffects.m @@ -0,0 +1,310 @@ +/* + File: UIImage+ImageEffects.m + Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur. + Version: 1.0 + + Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple + Inc. ("Apple") in consideration of your agreement to the following + terms, and your use, installation, modification or redistribution of + this Apple software constitutes acceptance of these terms. If you do + not agree with these terms, please do not use, install, modify or + redistribute this Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a personal, non-exclusive + license, under Apple's copyrights in this original Apple software (the + "Apple Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE + MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION + THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Copyright (C) 2013 Apple Inc. All Rights Reserved. + + + Copyright © 2013 Apple Inc. All rights reserved. + WWDC 2013 License + + NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013 + Session. Please refer to the applicable WWDC 2013 Session for further + information. + + IMPORTANT: This Apple software is supplied to you by Apple Inc. + ("Apple") in consideration of your agreement to the following terms, and + your use, installation, modification or redistribution of this Apple + software constitutes acceptance of these terms. If you do not agree with + these terms, please do not use, install, modify or redistribute this + Apple software. + + In consideration of your agreement to abide by the following terms, and + subject to these terms, Apple grants you a non-exclusive license, under + Apple's copyrights in this original Apple software (the "Apple + Software"), to use, reproduce, modify and redistribute the Apple + Software, with or without modifications, in source and/or binary forms; + provided that if you redistribute the Apple Software in its entirety and + without modifications, you must retain this notice and the following + text and disclaimers in all such redistributions of the Apple Software. + Neither the name, trademarks, service marks or logos of Apple Inc. may + be used to endorse or promote products derived from the Apple Software + without specific prior written permission from Apple. Except as + expressly stated in this notice, no other rights or licenses, express or + implied, are granted by Apple herein, including but not limited to any + patent rights that may be infringed by your derivative works or by other + works in which the Apple Software may be incorporated. + + The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES + NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE + IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND + OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + + IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, + MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED + AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), + STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + EA1002 + 5/3/2013 + */ + +#import "UIImage+ImageEffects.h" + +@import Accelerate; +#import + + +@implementation UIImage (ImageEffects) + + +- (UIImage *)applyLightEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3]; + return [self applyBlurWithRadius:30 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + + +- (UIImage *)applyExtraLightEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:0.97 alpha:0.82]; + return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + + +- (UIImage *)applyDarkEffect +{ + UIColor *tintColor = [UIColor colorWithWhite:0.11 alpha:0.73]; + return [self applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8 maskImage:nil]; +} + + +- (UIImage *)applyTintEffectWithColor:(UIColor *)tintColor +{ + const CGFloat EffectColorAlpha = 0.6; + UIColor *effectColor = tintColor; + int componentCount = CGColorGetNumberOfComponents(tintColor.CGColor); + if (componentCount == 2) { + CGFloat b; + if ([tintColor getWhite:&b alpha:NULL]) { + effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha]; + } + } + else { + CGFloat r, g, b; + if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) { + effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha]; + } + } + return [self applyBlurWithRadius:10 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil]; +} + + +- (UIImage *)applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage +{ + // Check pre-conditions. + if (self.size.width < 1 || self.size.height < 1) { + NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self); + return nil; + } + if (!self.CGImage) { + NSLog (@"*** error: image must be backed by a CGImage: %@", self); + return nil; + } + if (maskImage && !maskImage.CGImage) { + NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage); + return nil; + } + + CGRect imageRect = { CGPointZero, self.size }; + UIImage *effectImage = self; + + BOOL hasBlur = blurRadius > __FLT_EPSILON__; + BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__; + if (hasBlur || hasSaturationChange) { + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectInContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(effectInContext, 1.0, -1.0); + CGContextTranslateCTM(effectInContext, 0, -self.size.height); + CGContextDrawImage(effectInContext, imageRect, self.CGImage); + + vImage_Buffer effectInBuffer; + effectInBuffer.data = CGBitmapContextGetData(effectInContext); + effectInBuffer.width = CGBitmapContextGetWidth(effectInContext); + effectInBuffer.height = CGBitmapContextGetHeight(effectInContext); + effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext); + + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef effectOutContext = UIGraphicsGetCurrentContext(); + vImage_Buffer effectOutBuffer; + effectOutBuffer.data = CGBitmapContextGetData(effectOutContext); + effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext); + effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext); + effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext); + + if (hasBlur) { + // A description of how to compute the box kernel width from the Gaussian + // radius (aka standard deviation) appears in the SVG spec: + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // + // For larger values of 's' (s >= 2.0), an approximation can be used: Three + // successive box-blurs build a piece-wise quadratic convolution kernel, which + // approximates the Gaussian kernel to within roughly 3%. + // + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // + // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. + // + CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale]; + NSUInteger radius = floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5); + if (radius % 2 != 1) { + radius += 1; // force radius to be odd so that the three box-blur methodology works. + } + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend); + } + BOOL effectImageBuffersAreSwapped = NO; + if (hasSaturationChange) { + CGFloat s = saturationDeltaFactor; + CGFloat floatingPointSaturationMatrix[] = { + 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, + 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, + 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, + 0, 0, 0, 1, + }; + const int32_t divisor = 256; + NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]); + int16_t saturationMatrix[matrixSize]; + for (NSUInteger i = 0; i < matrixSize; ++i) { + saturationMatrix[i] = (int16_t)roundf(floatingPointSaturationMatrix[i] * divisor); + } + if (hasBlur) { + vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + effectImageBuffersAreSwapped = YES; + } + else { + vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags); + } + } + if (!effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + if (effectImageBuffersAreSwapped) + effectImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + // Set up output context. + UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]); + CGContextRef outputContext = UIGraphicsGetCurrentContext(); + CGContextScaleCTM(outputContext, 1.0, -1.0); + CGContextTranslateCTM(outputContext, 0, -self.size.height); + + // Draw base image. + CGContextDrawImage(outputContext, imageRect, self.CGImage); + + // Draw effect image. + if (hasBlur) { + CGContextSaveGState(outputContext); + if (maskImage) { + CGContextClipToMask(outputContext, imageRect, maskImage.CGImage); + } + CGContextDrawImage(outputContext, imageRect, effectImage.CGImage); + CGContextRestoreGState(outputContext); + } + + // Add in color tint. + if (tintColor) { + CGContextSaveGState(outputContext); + CGContextSetFillColorWithColor(outputContext, tintColor.CGColor); + CGContextFillRect(outputContext, imageRect); + CGContextRestoreGState(outputContext); + } + + // Output image is ready. + UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return outputImage; +} + +- (UIImage *)rotateByDegrees:(CGFloat)degrees { + UIView *rotatedViewBox = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.size.width, self.size.height)]; + CGAffineTransform t = CGAffineTransformMakeRotation(degrees * M_PI / 180.0f); + rotatedViewBox.transform = t; + CGSize rotatedSize = rotatedViewBox.frame.size; + + UIGraphicsBeginImageContext(rotatedSize); + CGContextRef bitmap = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(bitmap, rotatedSize.width/2, rotatedSize.height/2); + CGContextRotateCTM(bitmap, degrees * M_PI / 180.0f); + CGContextScaleCTM(bitmap, 1.0, -1.0); + CGContextDrawImage(bitmap, CGRectMake(-self.size.width / 2, -self.size.height / 2, self.size.width, self.size.height), self.CGImage); + + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; +} + ++ (UIImage *)imagePixelFromColor:(UIColor *)color { + return [UIImage imageFromColor:color withSize:CGSizeMake(1.0f, 1.0f)]; +} + ++ (UIImage *)imageFromColor:(UIColor *)color withSize:(CGSize)size { + CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height); + UIGraphicsBeginImageContext(rect.size); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSetFillColorWithColor(context, [color CGColor]); + CGContextFillRect(context, rect); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +@end diff --git a/package.json b/package.json index 54c072ad6..540b92023 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "commonmark-react-renderer": "hmhealey/commonmark-react-renderer#c5d00343664c89da40d5a2ffa8b083e7cc1615d7", "deep-equal": "1.0.1", "intl": "1.2.5", + "jail-monkey": "0.0.8", "mattermost-redux": "mattermost/mattermost-redux#master", "prop-types": "15.5.10", "react": "16.0.0-alpha.6", @@ -22,15 +23,17 @@ "react-native-bottom-sheet": "1.0.1", "react-native-button": "1.8.2", "react-native-cookies": "3.1.0", - "react-native-device-info": "0.10.2", + "react-native-device-info": "0.11.0", "react-native-drawer": "2.3.0", "react-native-exception-handler": "1.1.0", "react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4", "react-native-keyboard-aware-scroll-view": "0.2.8", "react-native-linear-gradient": "2.0.0", + "react-native-local-auth": "enahum/react-native-local-auth.git", "react-native-navigation": "1.1.131", "react-native-notifications": "enahum/react-native-notifications.git", "react-native-orientation": "enahum/react-native-orientation.git", + "react-native-passcode-status": "1.1.0", "react-native-status-bar-size": "0.3.2", "react-native-svg": "5.1.8", "react-native-swiper": "1.5.4", diff --git a/yarn.lock b/yarn.lock index e3689e9d0..8291d10cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1519,9 +1519,9 @@ commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" -commonmark-react-renderer@hmhealey/commonmark-react-renderer#11bf3a089900eda6f3e951f8b8c80663cff4d6a3: - version "4.3.2" - resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/11bf3a089900eda6f3e951f8b8c80663cff4d6a3" +commonmark-react-renderer@hmhealey/commonmark-react-renderer#c5d00343664c89da40d5a2ffa8b083e7cc1615d7: + version "4.3.3" + resolved "https://codeload.github.com/hmhealey/commonmark-react-renderer/tar.gz/c5d00343664c89da40d5a2ffa8b083e7cc1615d7" dependencies: in-publish "^2.0.0" lodash.assign "^4.2.0" @@ -1530,7 +1530,7 @@ commonmark-react-renderer@hmhealey/commonmark-react-renderer#11bf3a089900eda6f3e xss-filters "^1.2.6" commonmark@hmhealey/commonmark.js#9f33c90b5c82adfef30b87c7cd15aea2e2764b51: - version "0.27.0" + version "0.28.0" resolved "https://codeload.github.com/hmhealey/commonmark.js/tar.gz/9f33c90b5c82adfef30b87c7cd15aea2e2764b51" dependencies: entities "~ 1.1.1" @@ -3208,6 +3208,10 @@ istanbul-reports@^1.1.0: dependencies: handlebars "^4.0.3" +jail-monkey@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/jail-monkey/-/jail-monkey-0.0.8.tgz#076dfae94b4648950b825ecc2d33aac0642d97b6" + jest-haste-map@19.0.0: version "19.0.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-19.0.0.tgz#adde00b62b1fe04432a104b3254fc5004514b55e" @@ -3652,7 +3656,7 @@ makeerror@1.0.x: mattermost-redux@mattermost/mattermost-redux#master: version "0.0.1" - resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/e5297912f528b822b61c61a36735fb4d6699d08d" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/3bab6079caf711debd30eed5fc5283b6f97c1aeb" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" @@ -4477,9 +4481,9 @@ react-native-cookies@3.1.0: dependencies: invariant "^2.1.0" -react-native-device-info@0.10.2: - version "0.10.2" - resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.10.2.tgz#350cd68ed43839022ddea0a3a423439f16fa4fba" +react-native-device-info@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.11.0.tgz#e96dd0e3f8a7d7e3022c1f441f51fa912fdc7444" react-native-drawer@2.3.0: version "2.3.0" @@ -4505,6 +4509,10 @@ react-native-linear-gradient@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.0.0.tgz#7442c00e1fcd9fca329119dcbaf744d12bcd1b5a" +react-native-local-auth@enahum/react-native-local-auth.git: + version "1.4.1" + resolved "https://codeload.github.com/enahum/react-native-local-auth/tar.gz/ee2e53bcfe3fd0c49c28ff48f05cc19d5b56668c" + react-native-mock@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/react-native-mock/-/react-native-mock-0.3.1.tgz#13d22433c5351a8a7fb8aedd7862b614d087851c" @@ -4556,6 +4564,10 @@ react-native-orientation@enahum/react-native-orientation.git: version "1.17.0" resolved "https://codeload.github.com/enahum/react-native-orientation/tar.gz/8b8495c91d8e63598874a141279cf5409e26ad3a" +react-native-passcode-status@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-native-passcode-status/-/react-native-passcode-status-1.1.0.tgz#e5922d5f4d79bc09849438d620406e5ccd31168a" + react-native-status-bar-size@0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/react-native-status-bar-size/-/react-native-status-bar-size-0.3.2.tgz#e3ffd906f021220c256857a8de6b945f8ea3fabc"