diff --git a/docs/adr/0055-ios-fcm-routing.md b/docs/adr/0055-ios-fcm-routing.md new file mode 100644 index 000000000..09ae4eedf --- /dev/null +++ b/docs/adr/0055-ios-fcm-routing.md @@ -0,0 +1,24 @@ +# 55. iOS FCM routing + +Date: 2024-12-05 + +## Status + +Accepted + +## Context + +- Notification from FCM contains `mutable-content: true` +- Those notification will be transfered to Notification Service Extension ([See here](https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications#Configure-the-payload-for-the-remote-notification)) whether the app is in background or foreground +- Due to the app is in foreground, no notification will be shown +- Due to NSE handle the data, FCM on Flutter side cannot handle it + +## Decision + +- We check if the app is in foreground or not + - If the app is in foreground, we will not modify the payload and route the payload to FCM foreground method channel + - If the app is in background or terminated, we will keep the current implementation + +## Consequences + +- Twake Mail iOS app will get latest updates when app is in foreground diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fe3b934ce..ed11008cf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -1381,6 +1381,7 @@ F5D4EA082B2ABF090090DDFC /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_ID = group.com.linagora.teammail; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -1427,6 +1428,7 @@ F5D4EA092B2ABF090090DDFC /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_ID = group.com.linagora.teammail; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -1467,6 +1469,7 @@ F5D4EA0A2B2ABF090090DDFC /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { + APP_GROUP_ID = group.com.linagora.teammail; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index faa9d613d..82d520753 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -8,6 +8,7 @@ import flutter_local_notifications @objc class AppDelegate: FlutterAppDelegate { var notificationInteractionChannel: FlutterMethodChannel? + var fcmMethodChannel: FlutterMethodChannel? var currentEmailId: String? override func application( @@ -17,6 +18,7 @@ import flutter_local_notifications GeneratedPluginRegistrant.register(with: self) createNotificationInteractionChannel() + createFcmMethodChannel() if let payload = launchOptions?[.remoteNotification] as? [AnyHashable : Any], let emailId = payload[JmapConstants.EMAIL_ID] as? String, @@ -52,6 +54,14 @@ import flutter_local_notifications return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + override func applicationDidEnterBackground(_ application: UIApplication) { + updateApplicationStateInUserDefaults(false) + } + + override func applicationWillTerminate(_ application: UIApplication) { + updateApplicationStateInUserDefaults(false) + } + override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { let sharingIntent = SwiftReceiveSharingIntentPlugin.instance if sharingIntent.hasMatchingSchemePrefix(url: url) { @@ -69,6 +79,7 @@ import flutter_local_notifications override func applicationDidBecomeActive(_ application: UIApplication) { removeAppBadger() + updateApplicationStateInUserDefaults(true) } private func handleEmailAndress(open url: URL) -> URL? { @@ -90,12 +101,19 @@ import flutter_local_notifications willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent::notification: \(notification)") + TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent::notificationContent: \(notification.request.content.userInfo)") if let notificationBadgeCount = notification.request.content.badge?.intValue, notificationBadgeCount > 0 { let newBadgeCount = UIApplication.shared.applicationIconBadgeNumber + notificationBadgeCount TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent:newBadgeCount: \(newBadgeCount)") updateAppBadger(newBadgeCount: newBadgeCount) } - if validateDisplayPushNotification(userInfo: notification.request.content.userInfo) { + if UIApplication.shared.applicationState == .active { + fcmMethodChannel?.invokeMethod( + CoreUtils.FCM_ON_MESSAGE_METHOD_NAME, + arguments: notification.request.content.userInfo) + + completionHandler([]) + } else if validateDisplayPushNotification(userInfo: notification.request.content.userInfo) { completionHandler([.alert, .badge, .sound]) } else { completionHandler([]) @@ -167,4 +185,19 @@ extension AppDelegate { } } } + + private func createFcmMethodChannel() { + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + + self.fcmMethodChannel = FlutterMethodChannel( + name: CoreUtils.FCM_METHOD_CHANNEL_NAME, + binaryMessenger: controller.binaryMessenger + ) + } + + private func updateApplicationStateInUserDefaults(_ appIsActive: Bool) { + let appGroupId = (Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String) ?? "group.\(Bundle.main.bundleIdentifier!)" + let userDefaults = UserDefaults(suiteName: appGroupId) + userDefaults?.set(appIsActive, forKey: CoreUtils.APPLICATION_STATE) + } } diff --git a/ios/TwakeCore/Utils/CoreUtils.swift b/ios/TwakeCore/Utils/CoreUtils.swift index 254d08cda..007a18445 100644 --- a/ios/TwakeCore/Utils/CoreUtils.swift +++ b/ios/TwakeCore/Utils/CoreUtils.swift @@ -8,6 +8,9 @@ class CoreUtils { static let NOTIFICATION_INTERACTION_CHANNEL_NAME = "notification_interaction_channel" static let CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_FOREGROUND_OR_BACKGROUND = "current_email_id_in_notification_click_when_app_foreground_or_background" static let CURRENT_EMAIL_ID_IN_NOTIFICATION_CLICK_WHEN_APP_TERMINATED = "current_email_id_in_notification_click_when_app_terminated" + static let FCM_METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_messaging" + static let FCM_ON_MESSAGE_METHOD_NAME = "Messaging#onMessage" + static let APPLICATION_STATE = "applicationState" func getCurrentDate() -> Date { if #available(iOS 15, *) { diff --git a/ios/TwakeMailNSE/Info.plist b/ios/TwakeMailNSE/Info.plist index 05d36c34d..d5e4f2a46 100644 --- a/ios/TwakeMailNSE/Info.plist +++ b/ios/TwakeMailNSE/Info.plist @@ -13,5 +13,7 @@ NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService + AppGroupId + ${APP_GROUP_ID} diff --git a/ios/TwakeMailNSE/NotificationService.swift b/ios/TwakeMailNSE/NotificationService.swift index e61f93521..5388ac1a1 100644 --- a/ios/TwakeMailNSE/NotificationService.swift +++ b/ios/TwakeMailNSE/NotificationService.swift @@ -16,6 +16,14 @@ class NotificationService: UNNotificationServiceExtension { handler = contentHandler modifiedContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + let appGroupId = (Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String) ?? "group.\(Bundle.main.bundleIdentifier!)" + let userDefaults = UserDefaults(suiteName: appGroupId) + let isAppActive = userDefaults?.value(forKey: CoreUtils.APPLICATION_STATE) as? Bool + if isAppActive == true { + self.modifiedContent?.userInfo = request.content.userInfo.merging(["data": request.content.userInfo], uniquingKeysWith: {(_, new) in new}) + contentHandler(self.modifiedContent ?? request.content) + } + guard let payloadData = request.content.userInfo as? [String: Any], !keychainController.retrieveSharingSessions().isEmpty else { self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))