diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index b194b797..b1d32f9c 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2C5F7A4E2D2A4D4C00A1B2C3 /* AppsflyerSdkPluginMultiEngineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2C5F7A4D2D2A4D4C00A1B2C3 /* AppsflyerSdkPluginMultiEngineTests.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -45,6 +46,7 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2C5F7A4D2D2A4D4C00A1B2C3 /* AppsflyerSdkPluginMultiEngineTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppsflyerSdkPluginMultiEngineTests.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -92,6 +94,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( + 2C5F7A4D2D2A4D4C00A1B2C3 /* AppsflyerSdkPluginMultiEngineTests.m */, 331C807B294A618700263BE5 /* RunnerTests.swift */, ); path = RunnerTests; @@ -394,6 +397,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2C5F7A4E2D2A4D4C00A1B2C3 /* AppsflyerSdkPluginMultiEngineTests.m in Sources */, 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/example/ios/RunnerTests/AppsflyerSdkPluginMultiEngineTests.m b/example/ios/RunnerTests/AppsflyerSdkPluginMultiEngineTests.m new file mode 100644 index 00000000..5e95f1d2 --- /dev/null +++ b/example/ios/RunnerTests/AppsflyerSdkPluginMultiEngineTests.m @@ -0,0 +1,254 @@ +#import +#import +#import + +@interface AppsflyerSdkPlugin (Testing) +- (instancetype)initWithMessenger:(NSObject *)messenger; +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result; ++ (void)invokeCallbackWithId:(NSString *)callbackId arguments:(id)arguments; +@end + +@interface RecordingBinaryMessenger : NSObject +@property(nonatomic, strong) NSMutableArray *sentChannels; +@property(nonatomic, strong) NSMutableArray *sentMethodCalls; +@end + +@implementation RecordingBinaryMessenger + +- (instancetype)init { + self = [super init]; + if (self) { + _sentChannels = [NSMutableArray array]; + _sentMethodCalls = [NSMutableArray array]; + } + return self; +} + +- (void)sendOnChannel:(NSString *)channel message:(NSData *)message { + [self recordChannel:channel message:message]; +} + +- (void)sendOnChannel:(NSString *)channel + message:(NSData *)message + binaryReply:(FlutterBinaryReply)callback { + [self recordChannel:channel message:message]; + if (callback) { + callback(nil); + } +} + +- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString *)channel + binaryMessageHandler:(FlutterBinaryMessageHandler)handler { + return 0; +} + +- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString *)channel + binaryMessageHandler:(FlutterBinaryMessageHandler)handler + taskQueue:(NSObject *)taskQueue { + return 0; +} + +- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { +} + +- (void)recordChannel:(NSString *)channel message:(NSData *)message { + [self.sentChannels addObject:channel]; + if (message == nil) { + return; + } + + FlutterMethodCall *methodCall = [[FlutterStandardMethodCodec sharedInstance] decodeMethodCall:message]; + if (methodCall != nil) { + [self.sentMethodCalls addObject:methodCall]; + } +} + +@end + +@interface AppsflyerSdkPluginMultiEngineTests : XCTestCase +@property(nonatomic, strong) NSMutableArray *pluginsToCancel; +@property(nonatomic, strong) NSMutableSet *callbackIdsToCancel; +@end + +@implementation AppsflyerSdkPluginMultiEngineTests + +- (void)setUp { + [super setUp]; + self.pluginsToCancel = [NSMutableArray array]; + self.callbackIdsToCancel = [NSMutableSet set]; +} + +- (void)tearDown { + for (AppsflyerSdkPlugin *plugin in self.pluginsToCancel) { + for (NSString *callbackId in self.callbackIdsToCancel) { + [self cancelListeningWithPlugin:plugin callbackId:callbackId]; + } + } + self.pluginsToCancel = nil; + self.callbackIdsToCancel = nil; + [super tearDown]; +} + +- (void)testDeepLinkCallbackOwnerSurvivesSecondaryEngineRegistration { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + (void)[self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afUDPCallback]; + + NSString *payload = @"{\"id\":\"onDeepLinking\",\"deepLinkStatus\":\"FOUND\",\"deepLinkObj\":{}}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(mainMessenger.sentChannels.firstObject, afCallbacksMethodChannel); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.arguments, payload); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 0); +} + +- (void)testSecondaryEngineCannotStealExistingDeepLinkListener { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afUDPCallback]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afUDPCallback]; + + NSString *payload = @"{\"id\":\"onDeepLinking\",\"deepLinkStatus\":\"FOUND\",\"deepLinkObj\":{}}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.arguments, payload); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 0); +} + +- (void)testCallbackOwnerCanMoveAfterOwnerCancelsListening { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afUDPCallback]; + [self cancelListeningWithPlugin:mainPlugin callbackId:afUDPCallback]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afUDPCallback]; + + NSString *payload = @"{\"id\":\"onDeepLinking\",\"deepLinkStatus\":\"FOUND\",\"deepLinkObj\":{}}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 0); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.arguments, payload); +} + +- (void)testSecondaryEngineCancelDoesNotRemoveDeepLinkListenerOwner { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afUDPCallback]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afUDPCallback]; + [self cancelListeningWithPlugin:backgroundPlugin callbackId:afUDPCallback]; + + XCTAssertTrue([AppsflyerSdkPlugin udpCallback]); + + NSString *payload = @"{\"id\":\"onDeepLinking\",\"deepLinkStatus\":\"FOUND\",\"deepLinkObj\":{}}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.arguments, payload); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 0); +} + +- (void)testCallbackOwnerCanMoveAfterOwnerIsDeallocated { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + __weak AppsflyerSdkPlugin *weakOwnerPlugin = nil; + + @autoreleasepool { + AppsflyerSdkPlugin *ownerPlugin = [[AppsflyerSdkPlugin alloc] initWithMessenger:mainMessenger]; + weakOwnerPlugin = ownerPlugin; + [self startListeningWithPlugin:ownerPlugin callbackId:afUDPCallback]; + } + + XCTAssertNil(weakOwnerPlugin); + + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afUDPCallback]; + + NSString *payload = @"{\"id\":\"onDeepLinking\",\"deepLinkStatus\":\"FOUND\",\"deepLinkObj\":{}}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 0); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.arguments, payload); +} + +- (void)testInstallConversionCallbackUsesOwningEngine { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afGCDCallback]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afGCDCallback]; + + XCTAssertTrue([AppsflyerSdkPlugin gcdCallback]); + + NSString *payload = @"{\"id\":\"onInstallConversionData\",\"data\":\"{}\",\"status\":\"success\"}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afGCDCallback arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(mainMessenger.sentMethodCalls.firstObject.arguments, payload); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 0); +} + +- (void)testRequestScopedCallbackUsesLatestEngine { + RecordingBinaryMessenger *mainMessenger = [[RecordingBinaryMessenger alloc] init]; + RecordingBinaryMessenger *backgroundMessenger = [[RecordingBinaryMessenger alloc] init]; + + AppsflyerSdkPlugin *mainPlugin = [self pluginWithMessenger:mainMessenger]; + AppsflyerSdkPlugin *backgroundPlugin = [self pluginWithMessenger:backgroundMessenger]; + + [self startListeningWithPlugin:mainPlugin callbackId:afGenerateInviteLinkSuccess]; + [self startListeningWithPlugin:backgroundPlugin callbackId:afGenerateInviteLinkSuccess]; + + NSString *payload = @"{\"id\":\"generateInviteLinkSuccess\",\"data\":\"{}\",\"status\":\"success\"}"; + [AppsflyerSdkPlugin invokeCallbackWithId:afGenerateInviteLinkSuccess arguments:payload]; + + XCTAssertEqual(mainMessenger.sentMethodCalls.count, 0); + XCTAssertEqual(backgroundMessenger.sentMethodCalls.count, 1); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.method, @"callListener"); + XCTAssertEqualObjects(backgroundMessenger.sentMethodCalls.firstObject.arguments, payload); +} + +- (AppsflyerSdkPlugin *)pluginWithMessenger:(NSObject *)messenger { + AppsflyerSdkPlugin *plugin = [[AppsflyerSdkPlugin alloc] initWithMessenger:messenger]; + [self.pluginsToCancel addObject:plugin]; + return plugin; +} + +- (void)startListeningWithPlugin:(AppsflyerSdkPlugin *)plugin callbackId:(NSString *)callbackId { + [self.callbackIdsToCancel addObject:callbackId]; + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"startListening" arguments:callbackId]; + [plugin handleMethodCall:call result:^(id result) {}]; +} + +- (void)cancelListeningWithPlugin:(AppsflyerSdkPlugin *)plugin callbackId:(NSString *)callbackId { + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"cancelListening" arguments:callbackId]; + [plugin handleMethodCall:call result:^(id result) {}]; +} + +@end diff --git a/ios/Classes/AppsFlyerStreamHandler.m b/ios/Classes/AppsFlyerStreamHandler.m index 42404a3f..703b633c 100644 --- a/ios/Classes/AppsFlyerStreamHandler.m +++ b/ios/Classes/AppsFlyerStreamHandler.m @@ -7,6 +7,10 @@ #import "AppsFlyerStreamHandler.h" +@interface AppsflyerSdkPlugin (CallbackRouting) ++ (void)invokeCallbackWithId:(NSString*)callbackId arguments:(id)arguments; +@end + @implementation AppsFlyerStreamHandler { } @@ -23,7 +27,7 @@ - (void)onConversionDataSuccess:(NSDictionary *)installData { @"status": afSuccess }; NSString *JSONString = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + [AppsflyerSdkPlugin invokeCallbackWithId:afGCDCallback arguments:JSONString]; return; }else if (error) { return; @@ -45,7 +49,7 @@ - (void)onConversionDataFail:(NSError *)error { @"status": afSuccess }; NSString *JSONString = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + [AppsflyerSdkPlugin invokeCallbackWithId:afGCDCallback arguments:JSONString]; return; } @@ -66,7 +70,7 @@ - (void)onAppOpenAttribution:(NSDictionary *)attributionData { @"status": afSuccess }; NSString *JSONString = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + [AppsflyerSdkPlugin invokeCallbackWithId:afOAOACallback arguments:JSONString]; return; } @@ -83,7 +87,7 @@ - (void)onAppOpenAttributionFailure:(NSError *)error { @"status": afSuccess }; NSString *JSONString = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + [AppsflyerSdkPlugin invokeCallbackWithId:afOAOACallback arguments:JSONString]; return; } @@ -108,7 +112,7 @@ - (void)didResolveDeepLink:(AppsFlyerDeepLinkResult* _Nonnull) deepLinkResult { } NSString *JSONString = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + [AppsflyerSdkPlugin invokeCallbackWithId:afUDPCallback arguments:JSONString]; return; } @@ -136,7 +140,7 @@ - (void)sendResponseToFlutter:(NSString *)responseID status:(NSString *)status d @"status": status }; JSONdata = [self mapToJson:fullResponse withError:error]; - [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONdata]; + [AppsflyerSdkPlugin invokeCallbackWithId:responseID arguments:JSONdata]; } - (NSString*) getStatusAsString:(AFSDKDeepLinkResultStatus)value{ diff --git a/ios/Classes/AppsflyerSdkPlugin.h b/ios/Classes/AppsflyerSdkPlugin.h index 213d0a81..7f55fb3d 100644 --- a/ios/Classes/AppsflyerSdkPlugin.h +++ b/ios/Classes/AppsflyerSdkPlugin.h @@ -62,4 +62,3 @@ #define afCallbacksMethodChannel @"callbacks" #define afEventChannel @"af-events" #define afValidatePurchaseChannel @"af-validate-purchase" - diff --git a/ios/Classes/AppsflyerSdkPlugin.m b/ios/Classes/AppsflyerSdkPlugin.m index 3a827814..14544a4d 100644 --- a/ios/Classes/AppsflyerSdkPlugin.m +++ b/ios/Classes/AppsflyerSdkPlugin.m @@ -9,15 +9,21 @@ typedef void (*bypassDisableAdvertisingIdentifier)(id, SEL, BOOL); typedef void (*bypassWaitForATTUserAuthorization)(id, SEL, NSTimeInterval); +@interface AppsflyerSdkPlugin () +- (void)releaseOwnedCallbackChannels; +- (void)releaseCallbackId:(NSString*)callbackId; +@end + @implementation AppsflyerSdkPlugin { FlutterEventChannel *_eventChannel; AppsFlyerStreamHandler *_streamHandler; - + FlutterMethodChannel *_callbackChannel; + FlutterMethodChannel *_methodChannel; } static NSMutableArray* _callbackById; -static FlutterMethodChannel* _callbackChannel; -static FlutterMethodChannel* _methodChannel; +static NSMutableDictionary* _callbackChannelsById; +static FlutterMethodChannel* _defaultCallbackChannel; static BOOL _gcdCallback = false; static BOOL _oaoaCallback = false; static BOOL _udpCallback = false; @@ -25,13 +31,31 @@ @implementation AppsflyerSdkPlugin { static BOOL _isSandboxEnabled = false; static BOOL _isSKADEnabled = false; +static BOOL AFUsesDurableCallbackOwner(NSString *callbackId) { + return [callbackId isEqualToString:afGCDCallback] + || [callbackId isEqualToString:afOAOACallback] + || [callbackId isEqualToString:afUDPCallback]; +} + + (FlutterMethodChannel*)callbackChannel{ - return _callbackChannel; + @synchronized (self) { + return _defaultCallbackChannel; + } } -+ (FlutterMethodChannel*)methodChannel{ - return _methodChannel; ++ (FlutterMethodChannel*)callbackChannelForCallbackId:(NSString*)callbackId{ + if (callbackId == nil) { + return nil; + } + @synchronized (self) { + return _callbackChannelsById[callbackId]; + } +} + ++ (void)invokeCallbackWithId:(NSString*)callbackId arguments:(id)arguments{ + FlutterMethodChannel *channel = [self callbackChannelForCallbackId:callbackId]; + [channel invokeMethod:@"callListener" arguments:arguments]; } + (BOOL)gcdCallback{ @@ -53,10 +77,17 @@ - (instancetype)initWithMessenger:(nonnull NSObject *)me _callbackChannel = [FlutterMethodChannel methodChannelWithName:afCallbacksMethodChannel binaryMessenger:messenger]; _eventChannel = [FlutterEventChannel eventChannelWithName:afEventChannel binaryMessenger:messenger]; _methodChannel = [FlutterMethodChannel methodChannelWithName:afMethodChannel binaryMessenger:messenger]; + @synchronized ([AppsflyerSdkPlugin class]) { + _defaultCallbackChannel = _callbackChannel; + } } return self; } +- (void)dealloc { + [self releaseOwnedCallbackChannels]; +} + + (void)registerWithRegistrar:(NSObject*)registrar { #ifdef ENABLE_PURCHASE_CONNECTOR [PurchaseConnectorPlugin registerWithRegistrar:registrar]; @@ -138,6 +169,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self logCrossPromotionAndOpenStore:call result:result]; }else if([@"startListening" isEqualToString:call.method]){ [self startListening:call result:result]; + }else if([@"cancelListening" isEqualToString:call.method]){ + [self cancelListening:call result:result]; }else if([@"setOneLinkCustomDomain" isEqualToString:call.method]){ [self setOneLinkCustomDomain:call result:result]; }else if([@"setPushNotification" isEqualToString:call.method]){ @@ -180,16 +213,17 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { -(void)startSDKwithHandler:(FlutterMethodCall*)call result:(FlutterResult)result { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; - + FlutterMethodChannel *methodChannel = _methodChannel; + [[AppsFlyerLib shared] startWithCompletionHandler:^(NSDictionary *dictionary, NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ if (error) { - [_methodChannel invokeMethod:@"onError" arguments:@{@"errorCode": @(error.code), @"errorMessage": error.localizedDescription ?: @"Unknown error"}]; + [methodChannel invokeMethod:@"onError" arguments:@{@"errorCode": @(error.code), @"errorMessage": error.localizedDescription ?: @"Unknown error"}]; } else if (dictionary) { - [_methodChannel invokeMethod:@"onSuccess" arguments:dictionary]; + [methodChannel invokeMethod:@"onSuccess" arguments:dictionary]; } else { NSString *genericErrorMsg = @"SDK started without error or success data"; - [_methodChannel invokeMethod:@"onError" arguments:@{@"errorCode": @(0), @"errorMessage": genericErrorMsg}]; + [methodChannel invokeMethod:@"onError" arguments:@{@"errorCode": @(0), @"errorMessage": genericErrorMsg}]; } result(nil); }); @@ -455,20 +489,83 @@ - (void)setOneLinkCustomDomain:(FlutterMethodCall*)call result:(FlutterResult)re } - (void)startListening:(FlutterMethodCall*)call result:(FlutterResult)result{ - // Prepare callback dictionary - if (_callbackById == nil) _callbackById = [NSMutableArray array]; - NSString* callbackId = call.arguments; + if (callbackId == nil) { + result(nil); + return; + } + + @synchronized ([AppsflyerSdkPlugin class]) { + if (_callbackById == nil) _callbackById = [NSMutableArray array]; + if (_callbackChannelsById == nil) _callbackChannelsById = [NSMutableDictionary dictionary]; + + FlutterMethodChannel *existingChannel = _callbackChannelsById[callbackId]; + if (AFUsesDurableCallbackOwner(callbackId) && existingChannel != nil && existingChannel != _callbackChannel) { + result(nil); + return; + } + + if ([callbackId isEqualToString:afGCDCallback]){ + _gcdCallback = true; + } + if ([callbackId isEqualToString:afOAOACallback]){ + _oaoaCallback = true; + } + if ([callbackId isEqualToString:afUDPCallback]){ + _udpCallback = true; + } + if (![_callbackById containsObject:callbackId]) { + [_callbackById addObject:callbackId]; + } + _callbackChannelsById[callbackId] = _callbackChannel; + } + + result(nil); +} + +- (void)cancelListening:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* callbackId = call.arguments; + if (callbackId == nil) { + result(nil); + return; + } + + @synchronized ([AppsflyerSdkPlugin class]) { + if (_callbackChannelsById[callbackId] == _callbackChannel) { + [self releaseCallbackId:callbackId]; + } + } + + result(nil); +} + +- (void)releaseOwnedCallbackChannels { + @synchronized ([AppsflyerSdkPlugin class]) { + NSArray *callbackIds = [_callbackChannelsById allKeys]; + for (NSString *callbackId in callbackIds) { + if (_callbackChannelsById[callbackId] == _callbackChannel) { + [self releaseCallbackId:callbackId]; + } + } + if (_defaultCallbackChannel == _callbackChannel) { + _defaultCallbackChannel = nil; + } + } +} + +- (void)releaseCallbackId:(NSString*)callbackId { + [_callbackChannelsById removeObjectForKey:callbackId]; + [_callbackById removeObject:callbackId]; + if ([callbackId isEqualToString:afGCDCallback]){ - _gcdCallback = true; + _gcdCallback = false; } if ([callbackId isEqualToString:afOAOACallback]){ - _oaoaCallback = true; + _oaoaCallback = false; } if ([callbackId isEqualToString:afUDPCallback]){ - _udpCallback = true; + _udpCallback = false; } - [_callbackById addObject:callbackId]; } - (void)generateInviteLink:(FlutterMethodCall*)call result:(FlutterResult)result{