Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-3275 Fix FCM iOS foreground desync #3314

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/adr/0055-ios-fcm-routing.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
35 changes: 34 additions & 1 deletion ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import flutter_local_notifications
@objc class AppDelegate: FlutterAppDelegate {

var notificationInteractionChannel: FlutterMethodChannel?
var fcmMethodChannel: FlutterMethodChannel?
var currentEmailId: String?

override func application(
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -69,6 +79,7 @@ import flutter_local_notifications

override func applicationDidBecomeActive(_ application: UIApplication) {
removeAppBadger()
updateApplicationStateInUserDefaults(true)
}

private func handleEmailAndress(open url: URL) -> URL? {
Expand All @@ -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([])
Expand Down Expand Up @@ -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)
}
}
3 changes: 3 additions & 0 deletions ios/TwakeCore/Utils/CoreUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, *) {
Expand Down
2 changes: 2 additions & 0 deletions ios/TwakeMailNSE/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
<key>AppGroupId</key>
<string>${APP_GROUP_ID}</string>
</dict>
</plist>
8 changes: 8 additions & 0 deletions ios/TwakeMailNSE/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Loading