nexo/apps/mattermost/ios/Mattermost/AppDelegate.swift

311 lines
11 KiB
Swift

import AVFoundation
internal import Expo
import React
import ReactAppDependencyProvider
import RNKeychain
import RNNotifications
import RNSentry
import react_native_paste_input
import mattermost_rnutils
import mattermost_hardware_keyboard
import TurboLogIOSNative
import UIKit
import UserNotifications
import os.log
#if canImport(MSAL)
import MSAL
#endif
#if canImport(mattermost_intune)
import mattermost_intune
#endif
private let notificationClearAction = "clear"
private let notificationTestAction = "test"
@main
class AppDelegate: ExpoAppDelegate, OrientationLockable {
@objc var orientationLock: UIInterfaceOrientationMask = .allButUpsideDown
var window: UIWindow?
var reactNativeDelegate: ExpoReactNativeFactoryDelegate?
var reactNativeFactory: RCTReactNativeFactory?
override func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
os_log("Mattermost will attach session from handleEventsForBackgroundURLSession!! identifier=%{public}@", identifier)
GekidouWrapper.default.attachSession(identifier, completionHandler: completionHandler)
os_log("Mattermost session ATTACHED from handleEventsForBackgroundURLSession!! identifier=%{public}@", identifier)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Initialize Sentry early for native crash reporting (only when DSN is configured)
if let optionsPath = Bundle.main.path(forResource: "sentry.options", ofType: "json"),
let data = try? Data(contentsOf: URL(fileURLWithPath: optionsPath)),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let dsn = json["dsn"] as? String, !dsn.isEmpty {
RNSentrySDK.start()
}
// Configure TurboLog
if let appGroupId = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as? String,
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) {
let logsURL = containerURL.appendingPathComponent("Logs")
do {
try TurboLogger.configure(
dailyRolling: false,
maximumFileSize: 1024 * 1024,
maximumNumberOfFiles: 2,
logsDirectory: logsURL.path,
logsFilename: "MMLogs"
)
} catch {
NSLog("Failed to configure TurboLog: %@", error.localizedDescription)
}
TurboLogger.write(level: .info, message: "Configured turbolog")
}
// Configure Gekidou to use TurboLog via wrapper
GekidouWrapper.default.configureTurboLogForGekidou()
#if canImport(mattermost_intune)
// Initialize Intune MAM delegates BEFORE React Native
IntuneAccess.initializeIntuneDelegates()
#endif
OrientationManager.shared.delegate = self
// Clear keychain on first run in case of reinstallation
if UserDefaults.standard.object(forKey: "FirstRun") == nil {
let keychain = RNKeychainManager()
if let servers = keychain.getAllServersForInternetPasswords() {
TurboLogger.write(level: .info, message: "Servers \(servers)")
for server in servers {
keychain.deleteCredentials(forServer: server, withOptions: nil)
}
}
UserDefaults.standard.set(true, forKey: "FirstRun")
UserDefaults.standard.synchronize()
}
try? AVAudioSession.sharedInstance().setCategory(.playback)
GekidouWrapper.default.setPreference("true", forKey: "ApplicationIsRunning")
RNNotifications.startMonitorNotifications()
let delegate = ReactNativeDelegate()
let factory = ExpoReactNativeFactory(delegate: delegate)
delegate.dependencyProvider = RCTAppDependencyProvider()
reactNativeDelegate = delegate
reactNativeFactory = factory
#if os(iOS) || os(tvOS)
window = UIWindow(frame: UIScreen.main.bounds)
factory.startReactNative(
withModuleName: "Mattermost",
in: window,
launchOptions: launchOptions
)
#endif
let result = super.application(application, didFinishLaunchingWithOptions: launchOptions)
os_log("Mattermost started!!")
if let factory = reactNativeFactory {
PasteInputModule.setup(factory.rootViewFactory)
}
#if canImport(mattermost_intune)
// Restore enrollments if needed (silent, non-blocking)
IntuneAccess.checkAndRestoreEnrollmentOnLaunch()
#endif
return result
}
// MARK: - Remote Notifications
override func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
RNNotifications.didRegisterForRemoteNotifications(withDeviceToken: deviceToken)
}
override func application(
_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error
) {
RNNotifications.didFailToRegisterForRemoteNotificationsWithError(error)
}
override func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
let state = UIApplication.shared.applicationState
let action = userInfo["type"] as? String
let isClearAction = action == notificationClearAction
let isTestAction = action == notificationTestAction
if isTestAction {
completionHandler(.noData)
return
}
if !GekidouWrapper.default.verifySignature(userInfo) {
var notification = userInfo
notification["verified"] = "false"
RNNotifications.didReceiveBackgroundNotification(notification, withCompletionHandler: completionHandler)
return
}
if isClearAction {
NotificationHelper.default.clearChannelOrThreadNotifications(userInfo: userInfo as NSDictionary)
GekidouWrapper.default.postNotificationReceipt(userInfo)
RNNotifications.didReceiveBackgroundNotification(userInfo, withCompletionHandler: completionHandler)
return
}
if state != .active {
GekidouWrapper.default.fetchDataForPushNotification(userInfo) { data in
var notification = userInfo
if let data = data {
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
notification["data"] = json
} else {
TurboLogger.write(level: .warning, message: "Mattermost AppDelegate: JSON serialization returned nil without error")
}
} catch {
TurboLogger.write(level: .error, message: "Mattermost AppDelegate: JSON serialization error \(error.localizedDescription)")
}
}
RNNotifications.didReceiveBackgroundNotification(notification, withCompletionHandler: completionHandler)
}
} else {
completionHandler(.newData)
}
}
// MARK: - Deep Linking
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
if handleMSALURL(url) {
return true
}
return RCTLinkingManager.application(app, open: url, options: options)
}
func application(
_ application: UIApplication,
open url: URL,
sourceApplication: String?,
annotation: Any
) -> Bool {
if handleMSALURL(url) {
return true
}
return RCTLinkingManager.application(application, open: url, sourceApplication: sourceApplication, annotation: annotation)
}
private func handleMSALURL(_ url: URL) -> Bool {
#if canImport(MSAL)
if let bundleId = Bundle.main.bundleIdentifier,
url.absoluteString.hasPrefix("msauth.\(bundleId)") {
TurboLogger.write(level: .info, message: "[Intune] MSAL URL handled")
return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: nil)
}
#endif
return false
}
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
// MARK: - App Lifecycle
override func applicationDidBecomeActive(_ application: UIApplication) {
super.applicationDidBecomeActive(application)
GekidouWrapper.default.setPreference("true", forKey: "ApplicationIsForeground")
}
override func applicationWillResignActive(_ application: UIApplication) {
super.applicationWillResignActive(application)
GekidouWrapper.default.setPreference("false", forKey: "ApplicationIsForeground")
}
override func applicationDidEnterBackground(_ application: UIApplication) {
super.applicationDidEnterBackground(application)
GekidouWrapper.default.setPreference("false", forKey: "ApplicationIsForeground")
}
override func applicationWillTerminate(_ application: UIApplication) {
super.applicationWillTerminate(application)
GekidouWrapper.default.setPreference("false", forKey: "ApplicationIsForeground")
GekidouWrapper.default.setPreference("false", forKey: "ApplicationIsRunning")
}
// MARK: - Orientation
override func application(
_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?
) -> UIInterfaceOrientationMask {
return orientationLock
}
// MARK: - Hardware Keyboard
override var keyCommands: [UIKeyCommand]? {
return MattermostHardwareKeyboardWrapper.registerKeyCommands(
enterPressed: #selector(sendEnter(_:)),
shiftEnterPressed: #selector(sendShiftEnter(_:)),
findChannels: #selector(sendFindChannels(_:))
) as? [UIKeyCommand]
}
@objc private func sendEnter(_ sender: UIKeyCommand) {
MattermostHardwareKeyboardWrapper.enterKeyPressed()
}
@objc private func sendShiftEnter(_ sender: UIKeyCommand) {
MattermostHardwareKeyboardWrapper.shiftEnterKeyPressed()
}
@objc private func sendFindChannels(_ sender: UIKeyCommand) {
MattermostHardwareKeyboardWrapper.findChannels()
}
}
// MARK: - ReactNativeDelegate
class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
override func bundleURL() -> URL? {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
#else
return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
}
}