diff --git a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FLNChannelBlocker.m b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FLNChannelBlocker.m new file mode 100644 index 000000000..a043cbab5 --- /dev/null +++ b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FLNChannelBlocker.m @@ -0,0 +1,63 @@ +#import "./include/flutter_local_notifications/FLNChannelBlocker.h" +#import +#import + +@implementation FLNChannelBlocker + +/// Determines if a FlutterMethodChannel call should be suppressed. +/// Suppresses all firebase_messaging notification taps when FLN is also handling notifications. ++ (BOOL)shouldSuppressChannel:(FlutterMethodChannel *)channel + method:(NSString *)method + args:(id)args { + // Suppress firebase_messaging background notification taps when FLN is also handling notifications + // since this is handler by onDidReceiveNotificationResponse of flutter_local_notifications + if ([method isEqualToString:@"Messaging#onMessageOpenedApp"]) return YES; + + // Do not suppress other methods + return NO; +} + +/// Installs method swizzling on FlutterMethodChannel to intercept and suppress +/// duplicate notification handling calls. ++ (void)swizzleChannelMethods { + Class cls = [FlutterMethodChannel class]; + + Method orig1 = class_getInstanceMethod(cls, @selector(invokeMethod:arguments:)); + Method orig2 = class_getInstanceMethod(cls, @selector(invokeMethod:arguments:result:)); + + IMP orig1IMP = method_getImplementation(orig1); + IMP orig2IMP = orig2 ? method_getImplementation(orig2) : NULL; + + // Replacement for -invokeMethod:arguments: + void (^block1)(id, NSString *, id) = ^(id selfObj, NSString *method, id args) { + if ([FLNChannelBlocker shouldSuppressChannel:selfObj method:method args:args]) return; + ((void(*)(id, SEL, NSString *, id))orig1IMP)(selfObj, @selector(invokeMethod:arguments:), method, args); + }; + IMP new1IMP = imp_implementationWithBlock(block1); + method_setImplementation(orig1, new1IMP); + + // Replacement for -invokeMethod:arguments:result: + if (orig2IMP) { + void (^block2)(id, NSString *, id, FlutterResult) = ^(id selfObj, NSString *method, id args, FlutterResult result) { + if ([FLNChannelBlocker shouldSuppressChannel:selfObj method:method args:args]) { + if (result) result(nil); + return; + } + ((void(*)(id, SEL, NSString *, id, FlutterResult))orig2IMP) + (selfObj, @selector(invokeMethod:arguments:result:), method, args, result); + }; + IMP new2IMP = imp_implementationWithBlock(block2); + method_setImplementation(orig2, new2IMP); + } +} + +/// Install the interception/suppression on FlutterMethodChannel. +/// This method should be called once during plugin registration. ++ (void)installBlocker { + static dispatch_once_t once; + dispatch_once(&once, ^{ + [FLNChannelBlocker swizzleChannelMethods]; + }); +} + +@end diff --git a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m index c15be9ba4..5d775c639 100644 --- a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m @@ -2,6 +2,7 @@ #import "./include/flutter_local_notifications/ActionEventSink.h" #import "./include/flutter_local_notifications/FlutterEngineManager.h" #import "./include/flutter_local_notifications/FlutterLocalNotificationsConverters.h" +#import "./include/flutter_local_notifications/FLNChannelBlocker.h" @implementation FlutterLocalNotificationsPlugin { FlutterMethodChannel *_channel; @@ -144,6 +145,8 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } [registrar addMethodCallDelegate:instance channel:channel]; + + [FLNChannelBlocker installBlocker]; } + (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { @@ -811,7 +814,6 @@ - (UNCalendarNotificationTrigger *)buildUserNotificationCalendarTrigger: - (UNTimeIntervalNotificationTrigger *)buildUserNotificationTimeIntervalTrigger: (id)arguments API_AVAILABLE(ios(10.0)) { - if ([self containsKey:REPEAT_INTERVAL_MILLISECODNS forDictionary:arguments]) { NSInteger repeatIntervalMilliseconds = [arguments[REPEAT_INTERVAL_MILLISECODNS] integerValue]; @@ -902,6 +904,39 @@ - (BOOL)containsKey:(NSString *)key forDictionary:(NSDictionary *)dictionary { return dictionary[key] != [NSNull null] && dictionary[key] != nil; } +- (NSString *)extractPayloadFromUserInfo:(NSDictionary *)userInfo { + BOOL isFlutterNotification = [self isAFlutterLocalNotification:userInfo]; + NSString *payload; + + if (isFlutterNotification) { + payload = (NSString *)userInfo[PAYLOAD]; + } else { + // For non-Flutter notifications, use the entire userInfo as payload + if (userInfo != nil) { + // Filter out FCM-specific keys + NSMutableDictionary *filteredUserInfo = [userInfo mutableCopy]; + NSArray *keysToRemove = @[@"aps", @"message_id", @"message_type", @"collapse_key", @"from", @"to", @"fcm_options"]; + + for (NSString *key in keysToRemove) { + [filteredUserInfo removeObjectForKey:key]; + } + + // Remove keys starting with "google." or "gcm." + for (NSString *key in [filteredUserInfo allKeys]) { + if ([key hasPrefix:@"google."] || [key hasPrefix:@"gcm."]) { + [filteredUserInfo removeObjectForKey:key]; + } + } + + payload = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:filteredUserInfo options:0 error:nil] encoding:NSUTF8StringEncoding]; + } else { + payload = nil; + } + } + + return payload; +} + #pragma mark - UNUserNotificationCenterDelegate - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification @@ -955,8 +990,9 @@ - (NSMutableDictionary *)extractNotificationResponseDict: [[NSMutableDictionary alloc] init]; NSInteger notificationId = [response.notification.request.identifier integerValue]; - NSString *payload = - (NSString *)response.notification.request.content.userInfo[PAYLOAD]; + NSDictionary *userInfo = response.notification.request.content.userInfo; + NSString *payload = [self extractPayloadFromUserInfo:userInfo]; + NSNumber *notificationIdNumber = [NSNumber numberWithInteger:notificationId]; notitificationResponseDict[@"notificationId"] = notificationIdNumber; notitificationResponseDict[PAYLOAD] = payload; @@ -983,15 +1019,10 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0)) { - if (![self isAFlutterLocalNotification:response.notification.request.content - .userInfo]) { - return; - } - + NSDictionary *userInfo = response.notification.request.content.userInfo; NSInteger notificationId = [response.notification.request.identifier integerValue]; - NSString *payload = - (NSString *)response.notification.request.content.userInfo[PAYLOAD]; + NSString *payload = [self extractPayloadFromUserInfo:userInfo]; if ([response.actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) { @@ -1009,6 +1040,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center NSArray *foregroundActionIdentifiers = [[NSUserDefaults standardUserDefaults] stringArrayForKey:FOREGROUND_ACTION_IDENTIFIERS]; + if ([foregroundActionIdentifiers indexOfObject:response.actionIdentifier] != NSNotFound) { if (_initialized) { @@ -1028,6 +1060,8 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center registerPlugins:registerPlugins]; } + completionHandler(); + } else { completionHandler(); } } diff --git a/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/include/flutter_local_notifications/FLNChannelBlocker.h b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/include/flutter_local_notifications/FLNChannelBlocker.h new file mode 100644 index 000000000..b084d60f0 --- /dev/null +++ b/flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/include/flutter_local_notifications/FLNChannelBlocker.h @@ -0,0 +1,11 @@ +#import + +/// Utility class for preventing duplicate notification handling between +/// flutter_local_notifications and firebase_messaging. +@interface FLNChannelBlocker : NSObject + +/// Install the interception/suppression on FlutterMethodChannel. +/// This method should be called once during plugin registration. ++ (void)installBlocker; + +@end