From 64fbff6e71c5f8cbec7109fff2f067d4942979de Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 27 May 2019 19:38:32 -0400 Subject: [PATCH] MM-15643 Send user to settings when passcode is required (#2836) * MM-15643 Send user to settings when passcode is required * Allow access to the managed config in the iOS extensions --- .../rnbeta/MattermostManagedModule.java | 9 ++++ app/mattermost.js | 43 +++++++++++++++++-- .../mattermost-managed.android.js | 1 + .../mattermost-managed.ios.js | 3 ++ assets/base/i18n/en.json | 3 ++ ios/Mattermost.xcodeproj/project.pbxproj | 6 ++- ios/Mattermost/MattermostManaged.h | 2 +- ios/Mattermost/MattermostManaged.m | 10 +++++ ios/MattermostShare/Config.swift | 5 +++ ios/MattermostShare/ShareViewController.swift | 39 +++++++++++++++++ .../project.pbxproj | 2 + .../android/extension_post/extension_post.js | 42 ++++++++++++++++++ 12 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 ios/MattermostShare/Config.swift diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java index 3992990c2..1e72ad295 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MattermostManagedModule.java @@ -1,7 +1,9 @@ package com.mattermost.rnbeta; +import android.app.Activity; import android.app.Application; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import com.facebook.react.bridge.Arguments; @@ -62,4 +64,11 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule { promise.resolve(Arguments.createMap()); } } + + @ReactMethod + // Close the current activity and open the security settings. + public void goToSecuritySettings() { + getCurrentActivity().finish(); + getReactApplicationContext().startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS)); + } } diff --git a/app/mattermost.js b/app/mattermost.js index a997ca4d1..cb11fe909 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -261,9 +261,9 @@ export const handleManagedConfig = async (eventFromEmmServer = false) => { if (config && Object.keys(config).length) { app.setEMMEnabled(true); - authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; - blurApplicationScreen = config.blurApplicationScreen && config.blurApplicationScreen === 'true'; - jailbreakProtection = config.jailbreakProtection && config.jailbreakProtection === 'true'; + authNeeded = config.inAppPinCode === 'true'; + blurApplicationScreen = config.blurApplicationScreen === 'true'; + jailbreakProtection = config.jailbreakProtection === 'true'; vendor = config.vendor || 'Mattermost'; if (!state.entities.general.credentials.token) { @@ -339,12 +339,47 @@ const handleAuthentication = async (vendor) => { mattermostManaged.quitApp(); return false; } + } else { + await showNotSecuredAlert(vendor); + + mattermostManaged.quitApp(); + return false; } app.setPerformingEMMAuthentication(false); return true; }; +function showNotSecuredAlert(vendor) { + const translations = app.getTranslations(); + + return new Promise((resolve) => { + const options = []; + + if (Platform.OS === 'android') { + options.push({ + text: translations[t('mobile.managed.settings')], + onPress: () => { + mattermostManaged.goToSecuritySettings(); + }, + }); + } + + options.push({ + text: translations[t('mobile.managed.exit')], + onPress: resolve, + style: 'cancel', + }); + + Alert.alert( + translations[t('mobile.managed.blocked_by')].replace('{vendor}', vendor), + Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')], + options, + {cancelable: false, onDismiss: resolve}, + ); + }); +} + const handleSwitchToDefaultChannel = (teamId) => { store.dispatch(selectDefaultChannel(teamId)); }; @@ -415,7 +450,7 @@ const handleAppActive = async () => { if (app.emmEnabled && app.inBackgroundSince && authExpired) { try { const config = await mattermostManaged.getConfig(); - const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + const authNeeded = config.inAppPinCode === 'true'; if (authNeeded) { await handleAuthentication(config.vendor); } diff --git a/app/mattermost_managed/mattermost-managed.android.js b/app/mattermost_managed/mattermost-managed.android.js index 272036a0c..026959989 100644 --- a/app/mattermost_managed/mattermost-managed.android.js +++ b/app/mattermost_managed/mattermost-managed.android.js @@ -48,6 +48,7 @@ export default { getCachedConfig: () => { return cachedConfig; }, + goToSecuritySettings: MattermostManaged.goToSecuritySettings, isDeviceSecure: async () => { try { return await LocalAuth.isDeviceSecure(); diff --git a/app/mattermost_managed/mattermost-managed.ios.js b/app/mattermost_managed/mattermost-managed.ios.js index b81d226a9..7b6d81525 100644 --- a/app/mattermost_managed/mattermost-managed.ios.js +++ b/app/mattermost_managed/mattermost-managed.ios.js @@ -45,6 +45,9 @@ export default { return cachedConfig; }, + goToSecuritySettings: () => { + // Do nothing since iOS doesn't allow apps to do this + }, getCachedConfig: () => { return cachedConfig; }, diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 48857f78e..404a009d4 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -272,7 +272,10 @@ "mobile.managed.blocked_by": "Blocked by {vendor}", "mobile.managed.exit": "Exit", "mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.", + "mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.", + "mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.", "mobile.managed.secured_by": "Secured by {vendor}", + "mobile.managed.settings": "Go to settings", "mobile.markdown.code.copy_code": "Copy Code", "mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}", "mobile.markdown.image.too_large": "Image exceeds max dimensions of {maxWidth} by {maxHeight}:", diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 468dda2a9..6a6f011d1 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -76,10 +76,10 @@ 7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */; }; 7F43D63F1F6BFA19001FC614 /* libBVLinearGradient.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37D8FEC21E80B5230091F3BD /* libBVLinearGradient.a */; }; 7F43D6401F6BFA82001FC614 /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F63D2811E6C957C001FAE12 /* libRCTPushNotification.a */; }; + 7F4C2598227E3B11009144EF /* libRNCNetInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4C2595227E3AE9009144EF /* libRNCNetInfo.a */; }; 7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; }; 7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7F581F78221EEA7C0099E66B /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; }; - 7F4C2598227E3B11009144EF /* libRNCNetInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4C2595227E3AE9009144EF /* libRNCNetInfo.a */; }; 7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F5CA991208FE38F004F91CE /* libRNDocumentPicker.a */; }; 7F642DF02093533300F3165E /* libRNDeviceInfo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F642DED2093530B00F3165E /* libRNDeviceInfo.a */; }; 7F72F2EE2211220500F98FFF /* GenericPreview.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F72F2ED2211220500F98FFF /* GenericPreview.xib */; }; @@ -100,6 +100,7 @@ 7FF2AF8B2086483E00FFBDF4 /* KeyShareConsumer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7FF2AF8A2086483E00FFBDF4 /* KeyShareConsumer.storyboard */; }; 7FF31C1421330B7900680B75 /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FF31C1321330B4200680B75 /* libRNFetchBlob.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + 84E3264B229834C30055068A /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E325FF229834C30055068A /* Config.swift */; }; 895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; }; 8D26455C994F46C39B1392F2 /* libRNSafeArea.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9263CF9B16054263B13EA23B /* libRNSafeArea.a */; }; 9358B95F95184EE0A4DCE629 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D4B1B363C2414DA19C1AC521 /* OpenSans-Bold.ttf */; }; @@ -853,6 +854,7 @@ 7FFE32BF1FD9CCAA0038C7A0 /* Sentry.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sentry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 849D881A0372465294DE7315 /* RNSafeArea.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSafeArea.xcodeproj; path = "../node_modules/react-native-safe-area/ios/RNSafeArea.xcodeproj"; sourceTree = ""; }; + 84E325FF229834C30055068A /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 8F0B22D2C9924FAFA7FB681C /* Roboto-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Roboto-LightItalic.ttf"; path = "../assets/fonts/Roboto-LightItalic.ttf"; sourceTree = ""; }; 920B7301B6F84DDD80677487 /* RNGestureHandler.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNGestureHandler.xcodeproj; path = "../node_modules/react-native-gesture-handler/ios/RNGestureHandler.xcodeproj"; sourceTree = ""; }; 9263CF9B16054263B13EA23B /* libRNSafeArea.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSafeArea.a; sourceTree = ""; }; @@ -1271,6 +1273,7 @@ 7F240A1A220D3A2300637665 /* MattermostShare */ = { isa = PBXGroup; children = ( + 84E325FF229834C30055068A /* Config.swift */, 7F72F2E4221113DF00F98FFF /* Images */, 7F240A20220D3A2300637665 /* Info.plist */, 7F240ACF220D4A6100637665 /* KeyChainDataSource.h */, @@ -2627,6 +2630,7 @@ 7F240ADB220E089300637665 /* Item.swift in Sources */, 7F240A1C220D3A2300637665 /* ShareViewController.swift in Sources */, 7FABDFC22211A39000D0F595 /* Section.swift in Sources */, + 84E3264B229834C30055068A /* Config.swift in Sources */, 7FABE00A2212650600D0F595 /* ChannelsViewController.swift in Sources */, 7F240ADD220E094A00637665 /* TeamsViewController.swift in Sources */, ); diff --git a/ios/Mattermost/MattermostManaged.h b/ios/Mattermost/MattermostManaged.h index e0b76f9c6..29f6bc336 100644 --- a/ios/Mattermost/MattermostManaged.h +++ b/ios/Mattermost/MattermostManaged.h @@ -11,7 +11,7 @@ #import @interface MattermostManaged : RCTEventEmitter -- (NSUserDefaults *)bucketByName:(NSString*)name; +@property (nonatomic) NSUserDefaults *sharedUserDefaults; + (void)sendConfigChangedEvent; @end diff --git a/ios/Mattermost/MattermostManaged.m b/ios/Mattermost/MattermostManaged.m index 1fa61aace..4c261b3f5 100644 --- a/ios/Mattermost/MattermostManaged.m +++ b/ios/Mattermost/MattermostManaged.m @@ -8,6 +8,7 @@ #import "RCTUITextView.h" #import "MattermostManaged.h" +#import @implementation MattermostManaged { bool hasListeners; @@ -85,6 +86,8 @@ RCT_EXPORT_MODULE(); - (instancetype)init { self = [super init]; if (self) { + _sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:APP_GROUP_ID]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(managedConfigDidChange:) name:@"managedConfigDidChange" object:nil]; [[NSNotificationCenter defaultCenter] addObserverForName:NSUserDefaultsDidChangeNotification object:nil @@ -122,6 +125,13 @@ static NSString * const feedbackKey = @"com.apple.feedback.managed"; - (void) remoteConfigChanged { NSDictionary *response = [[NSUserDefaults standardUserDefaults] dictionaryForKey:configurationKey]; + NSDictionary *group = [self.sharedUserDefaults dictionaryForKey:configurationKey]; + + if (response && ![response isEqualToDictionary:group]) { + // copies the managed configuration so it is accessible in the Extensions + [self.sharedUserDefaults setObject:response forKey:configurationKey]; + } + if (hasListeners) { @try { [self sendEventWithName:@"managedConfigDidChange" body:response]; diff --git a/ios/MattermostShare/Config.swift b/ios/MattermostShare/Config.swift new file mode 100644 index 000000000..f23253560 --- /dev/null +++ b/ios/MattermostShare/Config.swift @@ -0,0 +1,5 @@ +let configurationKey = "com.apple.configuration.managed" + +func getManagedConfig() -> [String : Any] { + return UserDefaults.init(suiteName: APP_GROUP_ID)?.dictionary(forKey: configurationKey) ?? [:] +} diff --git a/ios/MattermostShare/ShareViewController.swift b/ios/MattermostShare/ShareViewController.swift index ebeb46f7a..409f985ac 100644 --- a/ios/MattermostShare/ShareViewController.swift +++ b/ios/MattermostShare/ShareViewController.swift @@ -2,6 +2,7 @@ import UIKit import Social import MobileCoreServices import UploadAttachments +import LocalAuthentication extension Bundle { var displayName: String? { @@ -31,6 +32,44 @@ class ShareViewController: SLComposeServiceViewController { title = Bundle.main.displayName placeholder = "Write a message..." + + let config = getManagedConfig() + if let inAppPinCode = config["inAppPinCode"] as? String, inAppPinCode == "true" { + self.auth(vendor: config["vendor"] as? String) + } else { + self.loadData() + } + } + + func auth(vendor: String?) { + let context = LAContext() + + var error: NSError? + if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { + if let error = error, error.code == kLAErrorPasscodeNotSet { + self.showErrorMessage( + title: "", + message: "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.", + VC: self + ) + } else { + self.showErrorMessage(title: "", message: "Unable to authenticate device owner for Mattermost", VC: self) + } + + return + } + + let reason = "Secured by " + (vendor ?? "Mattermost") + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in + if success { + self.loadData() + } else { + self.showErrorMessage(title: "", message: "Unable to authenticate device owner for Mattermost", VC: self) + } + } + } + + func loadData() { entities = store.getEntities(true) as [AnyHashable:Any]? sessionToken = store.getToken() serverURL = store.getServerUrl() diff --git a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj index 0e60d8d3b..6868bb220 100644 --- a/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj +++ b/ios/UploadAttachments/UploadAttachments.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FABE054221387B500D0F595 /* Constants.h */; }; 7FABE04E2213818A00D0F595 /* MattermostBucket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE04C2213818900D0F595 /* MattermostBucket.m */; }; 7FABE055221387B500D0F595 /* Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE053221387B400D0F595 /* Constants.m */; }; 7FABE058221388D700D0F595 /* UploadSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FABE057221388D600D0F595 /* UploadSessionManager.swift */; }; @@ -24,6 +25,7 @@ dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( + 7F80232C229C91AD0034D6D4 /* Constants.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/share_extension/android/extension_post/extension_post.js b/share_extension/android/extension_post/extension_post.js index 1a61ba20e..d4afdad8b 100644 --- a/share_extension/android/extension_post/extension_post.js +++ b/share_extension/android/extension_post/extension_post.js @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'; import {intlShape} from 'react-intl'; import { + Alert, Image, NativeModules, PermissionsAndroid, @@ -175,6 +176,10 @@ export default class ExtensionPost extends PureComponent { } catch (err) { return this.onClose({nativeEvent: true}); } + } else { + await this.showNotSecuredAlert(vendor); + + return this.onClose({nativeEvent: true}); } } } @@ -185,6 +190,43 @@ export default class ExtensionPost extends PureComponent { return this.initialize(); }; + showNotSecuredAlert(vendor) { + const {formatMessage} = this.context.intl; + + return new Promise((resolve) => { + Alert.alert( + formatMessage({ + id: 'mobile.managed.blocked_by', + defaultMessage: 'Blocked by {vendor}', + }, {vendor}), + formatMessage({ + id: 'mobile.managed.not_secured.android', + defaultMessage: 'This device must be secured with a screen lock to use Mattermost.', + }), + [ + { + text: formatMessage({ + id: 'mobile.managed.settings', + defaultMessage: 'Go to settings', + }), + onPress: () => { + mattermostManaged.goToSecuritySettings(); + }, + }, + { + text: formatMessage({ + id: 'mobile.managed.exit', + defaultMessage: 'Exit', + }), + onPress: resolve, + style: 'cancel', + }, + ], + {onDismiss: resolve} + ); + }); + } + getInputRef = (ref) => { this.input = ref; };