TF-3275 Fix FCM iOS foreground desync (#3314)
This commit is contained in:
@@ -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
|
||||||
@@ -1381,6 +1381,7 @@
|
|||||||
F5D4EA082B2ABF090090DDFC /* Debug */ = {
|
F5D4EA082B2ABF090090DDFC /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APP_GROUP_ID = group.com.linagora.teammail;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -1427,6 +1428,7 @@
|
|||||||
F5D4EA092B2ABF090090DDFC /* Release */ = {
|
F5D4EA092B2ABF090090DDFC /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APP_GROUP_ID = group.com.linagora.teammail;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
@@ -1467,6 +1469,7 @@
|
|||||||
F5D4EA0A2B2ABF090090DDFC /* Profile */ = {
|
F5D4EA0A2B2ABF090090DDFC /* Profile */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
APP_GROUP_ID = group.com.linagora.teammail;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import flutter_local_notifications
|
|||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
|
||||||
var notificationInteractionChannel: FlutterMethodChannel?
|
var notificationInteractionChannel: FlutterMethodChannel?
|
||||||
|
var fcmMethodChannel: FlutterMethodChannel?
|
||||||
var currentEmailId: String?
|
var currentEmailId: String?
|
||||||
|
|
||||||
override func application(
|
override func application(
|
||||||
@@ -17,6 +18,7 @@ import flutter_local_notifications
|
|||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
createNotificationInteractionChannel()
|
createNotificationInteractionChannel()
|
||||||
|
createFcmMethodChannel()
|
||||||
|
|
||||||
if let payload = launchOptions?[.remoteNotification] as? [AnyHashable : Any],
|
if let payload = launchOptions?[.remoteNotification] as? [AnyHashable : Any],
|
||||||
let emailId = payload[JmapConstants.EMAIL_ID] as? String,
|
let emailId = payload[JmapConstants.EMAIL_ID] as? String,
|
||||||
@@ -52,6 +54,14 @@ import flutter_local_notifications
|
|||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
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 {
|
override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||||
let sharingIntent = SwiftReceiveSharingIntentPlugin.instance
|
let sharingIntent = SwiftReceiveSharingIntentPlugin.instance
|
||||||
if sharingIntent.hasMatchingSchemePrefix(url: url) {
|
if sharingIntent.hasMatchingSchemePrefix(url: url) {
|
||||||
@@ -69,6 +79,7 @@ import flutter_local_notifications
|
|||||||
|
|
||||||
override func applicationDidBecomeActive(_ application: UIApplication) {
|
override func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
removeAppBadger()
|
removeAppBadger()
|
||||||
|
updateApplicationStateInUserDefaults(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleEmailAndress(open url: URL) -> URL? {
|
private func handleEmailAndress(open url: URL) -> URL? {
|
||||||
@@ -90,12 +101,19 @@ import flutter_local_notifications
|
|||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent::notification: \(notification)")
|
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 {
|
if let notificationBadgeCount = notification.request.content.badge?.intValue, notificationBadgeCount > 0 {
|
||||||
let newBadgeCount = UIApplication.shared.applicationIconBadgeNumber + notificationBadgeCount
|
let newBadgeCount = UIApplication.shared.applicationIconBadgeNumber + notificationBadgeCount
|
||||||
TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent:newBadgeCount: \(newBadgeCount)")
|
TwakeLogger.shared.log(message: "AppDelegate::userNotificationCenter::willPresent:newBadgeCount: \(newBadgeCount)")
|
||||||
updateAppBadger(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])
|
completionHandler([.alert, .badge, .sound])
|
||||||
} else {
|
} else {
|
||||||
completionHandler([])
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ class CoreUtils {
|
|||||||
static let NOTIFICATION_INTERACTION_CHANNEL_NAME = "notification_interaction_channel"
|
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_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 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 {
|
func getCurrentDate() -> Date {
|
||||||
if #available(iOS 15, *) {
|
if #available(iOS 15, *) {
|
||||||
|
|||||||
@@ -13,5 +13,7 @@
|
|||||||
<key>NSExtensionPrincipalClass</key>
|
<key>NSExtensionPrincipalClass</key>
|
||||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>AppGroupId</key>
|
||||||
|
<string>${APP_GROUP_ID}</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
handler = contentHandler
|
handler = contentHandler
|
||||||
modifiedContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
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],
|
guard let payloadData = request.content.userInfo as? [String: Any],
|
||||||
!keychainController.retrieveSharingSessions().isEmpty else {
|
!keychainController.retrieveSharingSessions().isEmpty else {
|
||||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||||
|
|||||||
Reference in New Issue
Block a user