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"))