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
This commit is contained in:
Harrison Healey 2019-05-27 19:38:32 -04:00 committed by Elias Nahum
parent fe2fb1d857
commit 64fbff6e71
No known key found for this signature in database
GPG key ID: E038DB71E0B61702
12 changed files with 159 additions and 6 deletions

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -48,6 +48,7 @@ export default {
getCachedConfig: () => {
return cachedConfig;
},
goToSecuritySettings: MattermostManaged.goToSecuritySettings,
isDeviceSecure: async () => {
try {
return await LocalAuth.isDeviceSecure();

View file

@ -45,6 +45,9 @@ export default {
return cachedConfig;
},
goToSecuritySettings: () => {
// Do nothing since iOS doesn't allow apps to do this
},
getCachedConfig: () => {
return cachedConfig;
},

View file

@ -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}:",

View file

@ -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 = "<group>"; };
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 = "<group>"; };
84E325FF229834C30055068A /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
9263CF9B16054263B13EA23B /* libRNSafeArea.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSafeArea.a; sourceTree = "<group>"; };
@ -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 */,
);

View file

@ -11,7 +11,7 @@
#import <React/RCTUtils.h>
@interface MattermostManaged : RCTEventEmitter <RCTBridgeModule>
- (NSUserDefaults *)bucketByName:(NSString*)name;
@property (nonatomic) NSUserDefaults *sharedUserDefaults;
+ (void)sendConfigChangedEvent;
@end

View file

@ -8,6 +8,7 @@
#import "RCTUITextView.h"
#import "MattermostManaged.h"
#import <UploadAttachments/Constants.h>
@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];

View file

@ -0,0 +1,5 @@
let configurationKey = "com.apple.configuration.managed"
func getManagedConfig() -> [String : Any] {
return UserDefaults.init(suiteName: APP_GROUP_ID)?.dictionary(forKey: configurationKey) ?? [:]
}

View file

@ -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()

View file

@ -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;
};

View file

@ -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;
};