Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#import "./include/flutter_local_notifications/FLNChannelBlocker.h"
#import <Flutter/Flutter.h>
#import <objc/runtime.h>

@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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,6 +145,8 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
}

[registrar addMethodCallDelegate:instance channel:channel];

[FLNChannelBlocker installBlocker];
}

+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback {
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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]) {
Expand All @@ -1009,6 +1040,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
NSArray<NSString *> *foregroundActionIdentifiers =
[[NSUserDefaults standardUserDefaults]
stringArrayForKey:FOREGROUND_ACTION_IDENTIFIERS];

if ([foregroundActionIdentifiers indexOfObject:response.actionIdentifier] !=
NSNotFound) {
if (_initialized) {
Expand All @@ -1028,6 +1060,8 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
registerPlugins:registerPlugins];
}

completionHandler();
} else {
completionHandler();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#import <Foundation/Foundation.h>

/// 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