* 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>
187 lines
7.1 KiB
Objective-C
187 lines
7.1 KiB
Objective-C
// 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 we’re 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
|