mattermost-mobile/ios/Mattermost/Extensions/RNNotificationEventHandler+HandleReplyAction.m
Elias Nahum 44eb76bed7
feat(iOS): Add Microsoft Intune MAM integration with multi-server support (#9312)
* refactor: implement custom ExpoImage wrapper for cache control

Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app

* refactor(ios): convert Gekidou to CocoaPods

Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0

* npm audit

* update fastlane

* feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection

Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository

* fix tests by mocking @mattermost/intune

* feat: implement Intune MAM integration with comprehensive security enforcement

Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls

* fix alerts when no server database is present

* Unify cache strategy

* fix emit config changed after it was stored in the db

* Handle Mid-Session Enrollment Detection

* fix ADALLogOverrideDisabled missing in Fastfile

* fix flow for initial enrollment

* fix and add unit tests

* enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release

* Update intune submodule with addressed feedback

* fix validate-intune-clean workflow

* feat(intune): add comprehensive error handling and SAML+Entra support

Add production-ready error handling for native Entra authentication with
user-friendly i18n messages, comprehensive test coverage, and support for
Entra login when server requires SAML.

* update i18n

* update intune submodule

* update build-pr token

* fix race condition between server auth and intune enrollment

* fix CI workflow to build with intune

* use deploy key for intune submodule

* set the config directly in the submodule .git

* debug injection

* try setting GIT_SSH_COMMAND

* remove action debug

* fix server url input

* match pod cache with intune hash

* Fastfile and envs

* have workflows check for intune/.git

* have ci cache intune frameworks as well

* update Fastlane to set no-cache to artifacts uploaded

* fix s3 upload

* fix pblist template

* Attempt to remove the cache control for PR uploads to s3

* use hash from commit for S3 path

* Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain

* Fix surface errors from intune login

* fix postinstall scripts

* use cacheKey for draft md images

* remove unnecessary double await during test

* Have isMinimumLicenseTier accept valid license sku tier as target

* Add missing Auth error messages

* remove the last period for intune errors in i18n

* do not call unenroll with wipe on manual logout

* Fix tests and Intune error messages

* do not filter any SSO type regardless of which is used for Intune

* fix 412 to not retry

* fix tests, app logs sharing and share_extension avatar cache

* apply setScreenCapturePolicy on license change

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* re-apply screen capture on enrollment

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* use userData from intunr login and prevent getMe

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>

* Check for Biometrics and Jailbreak as we used to

---------

Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
2025-12-10 13:07:28 +02:00

187 lines
7.1 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// RNNotificationEventHandler+HandleReplyAction.m
#import "RNNotificationEventHandler+HandleReplyAction.h"
#import <UserNotifications/UserNotifications.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
@import Gekidou;
// ---- Forward declaration of the class method we want to call (no header needed)
@interface RNNotificationEventHandler (OriginalSelectorDecl)
- (void)didReceiveNotificationResponse:(UNNotificationResponse *)response
completionHandler:(void (^)(void))completionHandler;
@end
@interface RNNotificationParser : NSObject
+ (NSDictionary *)parseNotificationResponse:(UNNotificationResponse *)response;
@end
// ---------------------------------------------------------------------------
NSString *const ReplyActionID = @"REPLY_ACTION";
// The original method were swizzling on RNNotificationEventHandler
typedef void (*DidReceiveIMP)(id, SEL, UNNotificationResponse *, void (^)(void));
static DidReceiveIMP original_didReceive_impl = NULL;
#pragma mark - Helpers
static inline void callCompletionOnMain(void (^completion)(void)) {
if (!completion) return;
if (NSThread.isMainThread) { completion(); }
else { dispatch_async(dispatch_get_main_queue(), completion); }
}
static inline void postLocalFailureNotification(NSString *channelId) {
UNMutableNotificationContent *content = [UNMutableNotificationContent new];
content.body = @"Message failed to send.";
NSMutableDictionary *userInfo = [@{@"local": @YES, @"test": @NO} mutableCopy];
if (channelId) userInfo[@"channel_id"] = channelId;
content.userInfo = userInfo;
UNTimeIntervalNotificationTrigger *trigger =
[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 repeats:NO];
NSString *identifier = [NSString stringWithFormat:@"mm-fail-%f",
[NSDate timeIntervalSinceReferenceDate]];
UNNotificationRequest *req =
[UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[UNUserNotificationCenter.currentNotificationCenter addNotificationRequest:req withCompletionHandler:nil];
}
static inline void updateBadgeToDeliveredCount(void) {
[UNUserNotificationCenter.currentNotificationCenter
getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> *notes) {
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication.sharedApplication.applicationIconBadgeNumber = (NSInteger)notes.count;
});
}];
}
#pragma mark - Core sendReply (no RNNotifications headers)
static void sendReplyUsingMattermost(UNNotificationResponse *response,
void (^completion)(void)) {
NSDictionary *parsedResponse = nil;
// Guard with runtime presence of the class
Class Parser = NSClassFromString(@"RNNotificationParser");
if (Parser && [Parser respondsToSelector:@selector(parseNotificationResponse:)]) {
// Compiler knows the signature thanks to the forward declaration above
parsedResponse = [RNNotificationParser parseNotificationResponse:response];
}
NSString *serverUrl = [parsedResponse valueForKeyPath:@"notification.server_url"];
if (serverUrl.length == 0) {
// minimal fallback
serverUrl = response.notification.request.content.userInfo[@"server_url"];
}
if (serverUrl.length == 0) {
postLocalFailureNotification(nil);
callCompletionOnMain(completion);
return;
}
NSDictionary *credentials = [[Keychain default] getCredentialsObjcFor:serverUrl];
NSString *sessionToken = credentials[@"token"];
NSString *preauthSecret = credentials[@"preauthSecret"];
if (sessionToken.length == 0) {
postLocalFailureNotification(nil);
callCompletionOnMain(completion);
return;
}
NSString *completionKey = response.notification.request.identifier;
NSString *message = [parsedResponse valueForKeyPath:@"action.text"];
if (message.length == 0 && [response isKindOfClass:UNTextInputNotificationResponse.class]) {
message = ((UNTextInputNotificationResponse *)response).userText;
}
NSString *channelId = [parsedResponse valueForKeyPath:@"notification.channel_id"];
NSString *rootId = [parsedResponse valueForKeyPath:@"notification.root_id"];
if (rootId.length == 0) {
rootId = [parsedResponse valueForKeyPath:@"notification.post_id"];
}
NSDictionary *post = @{
@"message": message ?: @"",
@"channel_id": channelId ?: @"",
@"root_id": rootId ?: @""
};
NSError *jsonErr = nil;
NSData *postData = [NSJSONSerialization dataWithJSONObject:post options:0 error:&jsonErr];
if (!postData) {
postLocalFailureNotification(channelId);
callCompletionOnMain(completion);
return;
}
// Strip trailing slash
NSRegularExpression *re = [NSRegularExpression regularExpressionWithPattern:@"/$" options:0 error:nil];
NSString *urlString = [re stringByReplacingMatchesInString:serverUrl options:0
range:NSMakeRange(0, serverUrl.length)
withTemplate:@""];
NSURL *url = [NSURL URLWithString:[urlString stringByAppendingString:@"/api/v4/posts?set_online=false"]];
if (!url) {
postLocalFailureNotification(channelId);
callCompletionOnMain(completion);
return;
}
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
req.HTTPMethod = @"POST";
[req setValue:[NSString stringWithFormat:@"Bearer %@", sessionToken] forHTTPHeaderField:@"Authorization"];
[req setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
if ([preauthSecret isKindOfClass:NSString.class] && preauthSecret.length > 0) {
[req setValue:preauthSecret forHTTPHeaderField:@"X-Mattermost-Preauth-Secret"];
}
req.HTTPBody = postData;
NSURLSession *session = [NSURLSession sessionWithConfiguration:NSURLSessionConfiguration.ephemeralSessionConfiguration];
[[session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
NSInteger status = [(NSHTTPURLResponse *)resp statusCode];
if (!err && status == 201) {
updateBadgeToDeliveredCount();
callCompletionOnMain(completion);
} else {
postLocalFailureNotification(channelId);
callCompletionOnMain(completion);
}
}] resume];
}
#pragma mark - Swizzle SAME selector as original code
@implementation RNNotificationEventHandler (HandleReplyAction)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = self;
SEL sel = @selector(didReceiveNotificationResponse:completionHandler:);
Method m = class_getInstanceMethod(cls, sel);
if (!m) return;
original_didReceive_impl = (DidReceiveIMP)method_getImplementation(m);
IMP swizzled = imp_implementationWithBlock(^(
id _self,
UNNotificationResponse *response,
void (^completion)(void)
){
if ([response.actionIdentifier isEqualToString:ReplyActionID]) {
sendReplyUsingMattermost(response, completion);
} else if (original_didReceive_impl) {
original_didReceive_impl(_self, sel, response, completion);
} else {
callCompletionOnMain(completion);
}
});
method_setImplementation(m, swizzled);
});
}
@end