diff --git a/.gitignore b/.gitignore index b6103e9..edbdc3c 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,7 @@ coverage/ .af-e2e/reports/ .af-smoke/reports/ + +# Swift Package Manager +.build/ +.swiftpm/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 797b223..5f7d746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Versions +## 6.18.0+1 + +- Added iOS Swift Package Manager (SPM) support via `ios/appsflyer_sdk/Package.swift` +- Added SPM ObjC target mirroring `ios/Classes/` sources (ObjC-only, no mixed-language issues) +- CocoaPods path unchanged — full backward compatibility maintained +- Updated `.gitignore` with `.build/` and `.swiftpm/` entries + ## 6.18.0 - Updated Android SDK from 6.17.6 to 6.18.0 diff --git a/ios/appsflyer_sdk/Package.swift b/ios/appsflyer_sdk/Package.swift new file mode 100644 index 0000000..7cf2952 --- /dev/null +++ b/ios/appsflyer_sdk/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "appsflyer_sdk", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "appsflyer-sdk", targets: ["appsflyer_sdk"]) + ], + dependencies: [ + .package( + url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework", + from: "6.18.0" + ) + ], + targets: [ + .target( + name: "appsflyer_sdk", + dependencies: [ + .product(name: "AppsFlyerLib", package: "AppsFlyerFramework") + ], + path: "Sources/appsflyer_sdk", + publicHeadersPath: "include", + cSettings: [ + .headerSearchPath("include/appsflyer_sdk") + ] + ) + ] +) diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m new file mode 100644 index 0000000..0ed55ca --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerAttribution.m @@ -0,0 +1,86 @@ +// +// AppsFlyerAttribution.m +// flutter-appsflyer +// +// Created by Amit Kremer on 11/02/2021. +// + +#import +#import "AppsFlyerAttribution.h" + +@implementation AppsFlyerAttribution + ++ (id)shared { + static AppsFlyerAttribution *shared = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + shared = [[self alloc] init]; + }); + return shared; +} + +- (id)init { + if (self = [super init]) { + self.options = nil; + self.restorationHandler = nil; + self.url = nil; + self.userActivity = nil; + self.annotation = nil; + self.sourceApplication = nil; + self.isBridgeReady = NO; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveBridgeReadyNotification:) + name:AF_BRIDGE_SET + object:nil]; + } + return self; +} + +- (void) continueUserActivity: (NSUserActivity*_Nullable) userActivity restorationHandler: (void (^_Nullable)(NSArray * _Nullable))restorationHandler{ + if(self.isBridgeReady == YES){ + [[AppsFlyerLib shared] continueUserActivity:userActivity restorationHandler:restorationHandler]; + }else{ + [AppsFlyerAttribution shared].userActivity = userActivity; + [AppsFlyerAttribution shared].restorationHandler = restorationHandler; + } +} + +- (void) handleOpenUrl:(NSURL *)url options:(NSDictionary *)options{ + if(self.isBridgeReady == YES){ + [[AppsFlyerLib shared] handleOpenUrl:url options:options]; + }else{ + [AppsFlyerAttribution shared].url = url; + [AppsFlyerAttribution shared].options = options; + } +} + +- (void) handleOpenUrl:(NSURL *)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation{ + if(self.isBridgeReady == YES){ + [[AppsFlyerLib shared] handleOpenURL:url sourceApplication:sourceApplication withAnnotation:annotation]; + }else{ + [AppsFlyerAttribution shared].url = url; + [AppsFlyerAttribution shared].sourceApplication = sourceApplication; + [AppsFlyerAttribution shared].annotation = annotation; + } + +} + +- (void) receiveBridgeReadyNotification:(NSNotification *) notification +{ + NSLog (@"AppsFlyer Debug: handle deep link"); + if(self.url && self.sourceApplication && self.annotation){ + [[AppsFlyerLib shared] handleOpenURL:self.url sourceApplication:self.sourceApplication withAnnotation:self.annotation]; + self.url = nil; + self.sourceApplication = nil; + self.annotation = nil; + }else if(self.options && self.url){ + [[AppsFlyerLib shared] handleOpenUrl:self.url options:self.options]; + self.options = nil; + self.url = nil; + }else if(self.userActivity){ + [[AppsFlyerLib shared] continueUserActivity:self.userActivity restorationHandler:nil]; + self.userActivity = nil; + self.restorationHandler = nil; + } +} +@end diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m new file mode 100644 index 0000000..42404a3 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsFlyerStreamHandler.m @@ -0,0 +1,156 @@ +// +// AppsFlyerStreamHandler.m +// appsflyer_sdk +// +// Created by Shahar Cohen on 05/09/2019. +// + +#import "AppsFlyerStreamHandler.h" + +@implementation AppsFlyerStreamHandler { + +} + +- (void)onConversionDataSuccess:(NSDictionary *)installData { + NSError *error; + + //use callbacks + if([AppsflyerSdkPlugin gcdCallback]){ + NSString *installDataJson = [self mapToJson:installData withError:error]; + NSDictionary *fullResponse = @{ + @"id": afGCDCallback, + @"data": installDataJson, + @"status": afSuccess + }; + NSString *JSONString = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + return; + }else if (error) { + return; + } +} + +- (NSString *)mapToJson:(NSDictionary *)data withError:(NSError *)error{ + NSData *JSON = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error]; + NSString *JSONString = [[NSString alloc] initWithData:JSON encoding:NSUTF8StringEncoding]; + return JSONString; +} + +- (void)onConversionDataFail:(NSError *)error { + //use callbacks + if([AppsflyerSdkPlugin gcdCallback]){ + NSDictionary *fullResponse = @{ + @"id": afGCDCallback, + @"data": error.localizedDescription, + @"status": afSuccess + }; + NSString *JSONString = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + return; + } + + if (error) { + return; + } +} + + +- (void)onAppOpenAttribution:(NSDictionary *)attributionData { + NSError *error; + //use callbacks + if([AppsflyerSdkPlugin oaoaCallback]){ + NSString* attributionDataJson = [self mapToJson:attributionData withError:error]; + NSDictionary *fullResponse = @{ + @"id": afOAOACallback, + @"data": attributionDataJson, + @"status": afSuccess + }; + NSString *JSONString = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + return; + } + + if (error) { + return; + } +} + +- (void)onAppOpenAttributionFailure:(NSError *)error { + if([AppsflyerSdkPlugin oaoaCallback]){ + NSDictionary *fullResponse = @{ + @"id": afOAOACallback, + @"data": error.localizedDescription, + @"status": afSuccess + }; + NSString *JSONString = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + return; + } + +} + +- (void)didResolveDeepLink:(AppsFlyerDeepLinkResult* _Nonnull) deepLinkResult { + NSError *error; + if([AppsflyerSdkPlugin udpCallback]){ + NSMutableDictionary *fullResponse = [[NSMutableDictionary alloc] initWithCapacity:4]; + + fullResponse[ @"id"] = afUDPCallback; + fullResponse[ @"deepLinkStatus"] = [self getStatusAsString:deepLinkResult.status]; + if(deepLinkResult.deepLink != nil){ + NSMutableDictionary *dic = [[NSMutableDictionary alloc] initWithCapacity: deepLinkResult.deepLink.clickEvent.count + 1]; + [dic addEntriesFromDictionary:deepLinkResult.deepLink.clickEvent]; + dic[@"is_deferred"] = [NSNumber numberWithBool:deepLinkResult.deepLink.isDeferred]; + fullResponse [@"deepLinkObj"] = dic; + + } + if (deepLinkResult.error != nil && deepLinkResult.error.localizedDescription) { + fullResponse [@"deepLinkError"] = deepLinkResult.error.localizedDescription; + + } + NSString *JSONString = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONString]; + return; + } + + if (error) { + return; + } + } + + +- (void)sendResponseToFlutter:(NSString *)responseID status:(NSString *)status data:(NSDictionary *)data{ + NSError *error; + NSString *JSONdata; + + if(data != nil){ + JSONdata = [self mapToJson:data withError:error]; + }else{ + JSONdata = @"empty data"; + } + if (error) { + return; + } + NSDictionary *fullResponse = @{ + @"id": responseID, + @"data": JSONdata, + @"status": status + }; + JSONdata = [self mapToJson:fullResponse withError:error]; + [AppsflyerSdkPlugin.callbackChannel invokeMethod:@"callListener" arguments:JSONdata]; +} + +- (NSString*) getStatusAsString:(AFSDKDeepLinkResultStatus)value{ + switch (value) { + case AFSDKDeepLinkResultStatusFound: + return @"FOUND"; + case AFSDKDeepLinkResultStatusNotFound: + return @"NOT_FOUND"; + default: + return @"ERROR"; + + } +} + + + +@end diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m new file mode 100644 index 0000000..3a82781 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/AppsflyerSdkPlugin.m @@ -0,0 +1,1013 @@ +#import "AppsflyerSdkPlugin.h" +#import "AppsFlyerStreamHandler.h" +#import + +#ifdef ENABLE_PURCHASE_CONNECTOR +#import "appsflyer_sdk/appsflyer_sdk-Swift.h" +#endif +typedef void (*bypassDidFinishLaunchingWithOption)(id, SEL, NSInteger); +typedef void (*bypassDisableAdvertisingIdentifier)(id, SEL, BOOL); +typedef void (*bypassWaitForATTUserAuthorization)(id, SEL, NSTimeInterval); + + +@implementation AppsflyerSdkPlugin { + FlutterEventChannel *_eventChannel; + AppsFlyerStreamHandler *_streamHandler; + +} +static NSMutableArray* _callbackById; +static FlutterMethodChannel* _callbackChannel; +static FlutterMethodChannel* _methodChannel; +static BOOL _gcdCallback = false; +static BOOL _oaoaCallback = false; +static BOOL _udpCallback = false; +static BOOL _isPushNotificationEnabled = false; +static BOOL _isSandboxEnabled = false; +static BOOL _isSKADEnabled = false; + + ++ (FlutterMethodChannel*)callbackChannel{ + return _callbackChannel; +} + ++ (FlutterMethodChannel*)methodChannel{ + return _methodChannel; +} + ++ (BOOL)gcdCallback{ + return _gcdCallback; +} + ++ (BOOL)oaoaCallback{ + return _oaoaCallback; +} + ++ (BOOL)udpCallback{ + return _udpCallback; +} + +- (instancetype)initWithMessenger:(nonnull NSObject *)messenger { + self = [super init]; + if (self) { + _streamHandler = [[AppsFlyerStreamHandler alloc] init]; + _callbackChannel = [FlutterMethodChannel methodChannelWithName:afCallbacksMethodChannel binaryMessenger:messenger]; + _eventChannel = [FlutterEventChannel eventChannelWithName:afEventChannel binaryMessenger:messenger]; + _methodChannel = [FlutterMethodChannel methodChannelWithName:afMethodChannel binaryMessenger:messenger]; + } + return self; +} + ++ (void)registerWithRegistrar:(NSObject*)registrar { +#ifdef ENABLE_PURCHASE_CONNECTOR + [PurchaseConnectorPlugin registerWithRegistrar:registrar]; +#endif + id messenger = [registrar messenger]; + FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:afMethodChannel binaryMessenger:messenger]; + FlutterMethodChannel *callbackChannel = [FlutterMethodChannel methodChannelWithName:afCallbacksMethodChannel binaryMessenger:messenger]; + AppsflyerSdkPlugin *instance = [[AppsflyerSdkPlugin alloc] initWithMessenger:messenger]; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addMethodCallDelegate:instance channel:callbackChannel]; + [registrar addApplicationDelegate:instance]; +#if __has_include() + if (@available(iOS 13.0, *)) { + [registrar addSceneDelegate:instance]; + } +#endif + +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + + if([@"initSdk" isEqualToString:call.method]){ + [self initSdkWithCall:call result:result]; + }else if([@"getSDKVersion" isEqualToString:call.method]){ + [self getSDKVersion:result]; + }else if([@"startSDK" isEqualToString:call.method]){ + [self startSDK:call result:result]; + }else if([@"startSDKwithHandler" isEqualToString:call.method]){ + [self startSDKwithHandler:call result:result]; + } else if([@"logEvent" isEqualToString:call.method]){ + [self logEventWithCall:call result:result]; + }else if([@"waitForCustomerUserId" isEqualToString:call.method]){ + [self waitForCustomerId:call result:result]; + }else if([@"setUserEmails" isEqualToString:call.method]){ + [self setUserEmails:call result:result]; + }else if([@"updateServerUninstallToken" isEqualToString:call.method]){ + [self updateServerUninstallToken:call result:result]; + }else if([@"enableUninstallTracking" isEqualToString:call.method]){ + // + }else if([@"enableLocationCollection" isEqualToString:call.method]){ + // + }else if([@"stop" isEqualToString:call.method]){ + [self stop:call result:result]; + }else if([@"setIsUpdate" isEqualToString:call.method]){ + // + }else if([@"setCustomerUserId" isEqualToString:call.method]){ + [self setCustomerUserId:call result:result]; + }else if([@"setCustomerIdAndLogSession" isEqualToString:call.method]){ + [self setCustomerUserId:call result:result]; + }else if([@"setCurrencyCode" isEqualToString:call.method ]){ + [self setCurrencyCode:call result:result]; + }else if([@"setMinTimeBetweenSessions" isEqualToString:call.method]){ + [self setMinTimeBetweenSessions:call result:result]; + }else if([@"getHostPrefix" isEqualToString:call.method]){ + [self getHostPrefix:result]; + }else if([@"getHostName" isEqualToString:call.method]){ + [self getHostName:result]; + }else if([@"setHost" isEqualToString:call.method]){ + [self setHost:call result:result]; + }else if([@"setAdditionalData" isEqualToString:call.method]){ + [self setAdditionalData:call result:result]; + }else if([@"validateAndLogInAppIosPurchase" isEqualToString:call.method]){ + [self validateAndLogInAppPurchase:call result:result]; + }else if([@"validateAndLogInAppPurchaseV2" isEqualToString:call.method]){ + [self validateAndLogInAppPurchaseV2:call result:result]; + }else if([@"getAppsFlyerUID" isEqualToString:call.method]){ + [self getAppsFlyerUID:result]; + }else if([@"setSharingFilter" isEqualToString:call.method]){ + [self setSharingFilter:call result:result]; + }else if([@"setSharingFilterForAllPartners" isEqualToString:call.method]){ + [self setSharingFilterForAllPartners:result]; + }else if([@"generateInviteLink" isEqualToString:call.method]){ + [self generateInviteLink:call result:result]; + }else if([@"setAppInviteOneLinkID" isEqualToString:call.method]){ + [self setAppInviteOneLinkID:call result:result]; + }else if([@"logCrossPromotionImpression" isEqualToString:call.method]){ + [self logCrossPromotionImpression:call result:result]; + }else if([@"logCrossPromotionAndOpenStore" isEqualToString:call.method]){ + [self logCrossPromotionAndOpenStore:call result:result]; + }else if([@"startListening" isEqualToString:call.method]){ + [self startListening:call result:result]; + }else if([@"setOneLinkCustomDomain" isEqualToString:call.method]){ + [self setOneLinkCustomDomain:call result:result]; + }else if([@"setPushNotification" isEqualToString:call.method]){ + [self setPushNotification:call result:result]; + }else if([@"sendPushNotificationData" isEqualToString:call.method]){ + [self sendPushNotificationData:call result:result]; + }else if([@"useReceiptValidationSandbox" isEqualToString:call.method]){ + [self useReceiptValidationSandbox:call result:result]; + }else if([@"enableFacebookDeferredApplinks" isEqualToString:call.method]){ + [self enableFacebookDeferredApplinks:call result:result]; + }else if([@"anonymizeUser" isEqualToString:call.method]){ + [self anonymizeUser:call result:result]; + }else if([@"disableSKAdNetwork" isEqualToString:call.method]){ + [self disableSKAdNetwork:call result:result]; + }else if([@"setCurrentDeviceLanguage" isEqualToString:call.method]){ + [self setCurrentDeviceLanguage:call result:result]; + }else if([@"setSharingFilterForPartners" isEqualToString:call.method]){ + [self setSharingFilterForPartners:call result:result]; + }else if([@"setDisableAdvertisingIdentifiers" isEqualToString:call.method]){ + [self setDisableAdvertisingIdentifiers:call result:result]; + }else if([@"setPartnerData" isEqualToString:call.method]){ + [self setPartnerData:call result:result]; + }else if([@"setResolveDeepLinkURLs" isEqualToString:call.method]){ + [self setResolveDeepLinkURLs:call result:result]; + }else if([@"addPushNotificationDeepLinkPath" isEqualToString:call.method]){ + [self addPushNotificationDeepLinkPath:call result:result]; + }else if([@"enableTCFDataCollection" isEqualToString:call.method]){ + [self enableTCFDataCollection:call result:result]; + }else if([@"setConsentData" isEqualToString:call.method]){ + [self setConsentData:call result:result]; + }else if([@"setConsentDataV2" isEqualToString:call.method]){ + [self setConsentDataV2:call result:result]; + }else if([@"logAdRevenue" isEqualToString:call.method]){ + [self logAdRevenue:call result:result]; + } + else{ + result(FlutterMethodNotImplemented); + } +} + +-(void)startSDKwithHandler:(FlutterMethodCall*)call result:(FlutterResult)result { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; + + [[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"}]; + } else if (dictionary) { + [_methodChannel invokeMethod:@"onSuccess" arguments:dictionary]; + } else { + NSString *genericErrorMsg = @"SDK started without error or success data"; + [_methodChannel invokeMethod:@"onError" arguments:@{@"errorCode": @(0), @"errorMessage": genericErrorMsg}]; + } + result(nil); + }); + }]; +} + +- (void)startSDK:(FlutterMethodCall*)call result:(FlutterResult)result { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; + [[AppsFlyerLib shared] start]; + result(nil); +} + +- (void)setConsentData:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary* consentDict = call.arguments[@"consentData"]; + + BOOL isUserSubjectToGDPR = [consentDict[@"isUserSubjectToGDPR"] boolValue]; + BOOL hasConsentForDataUsage = [consentDict[@"hasConsentForDataUsage"] boolValue]; + BOOL hasConsentForAdsPersonalization = [consentDict[@"hasConsentForAdsPersonalization"] boolValue]; + + AppsFlyerConsent *consentData; + if(isUserSubjectToGDPR){ + consentData = [[AppsFlyerConsent alloc] initForGDPRUserWithHasConsentForDataUsage:hasConsentForDataUsage + hasConsentForAdsPersonalization:hasConsentForAdsPersonalization]; + }else{ + consentData = [[AppsFlyerConsent alloc] initWithNonGDPRUser]; + } + + [[AppsFlyerLib shared] setConsentData:consentData]; + result(nil); +} + +- (void)setConsentDataV2:(FlutterMethodCall*)call result:(FlutterResult)result { + @try { + // Extract the parameters directly from the arguments + NSNumber *isUserSubjectToGDPR = call.arguments[@"isUserSubjectToGDPR"]; + if ([isUserSubjectToGDPR isKindOfClass:[NSNull class]]) { + isUserSubjectToGDPR = nil; + } + + NSNumber *consentForDataUsage = call.arguments[@"consentForDataUsage"]; + if ([consentForDataUsage isKindOfClass:[NSNull class]]) { + consentForDataUsage = nil; + } + + NSNumber *consentForAdsPersonalization = call.arguments[@"consentForAdsPersonalization"]; + if ([consentForAdsPersonalization isKindOfClass:[NSNull class]]) { + consentForAdsPersonalization = nil; + } + + NSNumber *hasConsentForAdStorage = call.arguments[@"hasConsentForAdStorage"]; + if ([hasConsentForAdStorage isKindOfClass:[NSNull class]]) { + hasConsentForAdStorage = nil; + } + + // Create the consent object + AppsFlyerConsent *consentData = [[AppsFlyerConsent alloc] initWithIsUserSubjectToGDPR:isUserSubjectToGDPR + hasConsentForDataUsage:consentForDataUsage + hasConsentForAdsPersonalization:consentForAdsPersonalization + hasConsentForAdStorage:hasConsentForAdStorage]; + + // Set the consent data using AppsFlyer SDK + [[AppsFlyerLib shared] setConsentData:consentData]; + result(nil); + } + @catch (NSException *exception) { + NSLog(@"AppsFlyer: Error setting consent data v2: %@", exception.reason); + result([FlutterError errorWithCode:@"CONSENT_ERROR" + message:[NSString stringWithFormat:@"Failed to set consent data v2: %@", exception.reason] + details:nil]); + } +} + +- (void)logAdRevenue:(FlutterMethodCall*)call result:(FlutterResult)result { + @try { + NSString *monetizationNetwork = [self requireNonNullArgumentWithCall:call result:result argumentName:@"monetizationNetwork" errorCode:@"NULL_MONETIZATION_NETWORK"]; + if (monetizationNetwork == nil) return; + + NSString *currencyIso4217Code = [self requireNonNullArgumentWithCall:call result:result argumentName:@"currencyIso4217Code" errorCode:@"NULL_CURRENCY_CODE"]; + if (currencyIso4217Code == nil) return; + + NSNumber *revenueValue = [self requireNonNullArgumentWithCall:call result:result argumentName:@"revenue" errorCode:@"NULL_REVENUE"]; + if (revenueValue == nil) return; + + NSString *mediationNetworkString = [self requireNonNullArgumentWithCall:call result:result argumentName:@"mediationNetwork" errorCode:@"NULL_MEDIATION_NETWORK"]; + if (mediationNetworkString == nil) return; + + // Fetching the actual mediationNetwork Enum + AppsFlyerAdRevenueMediationNetworkType mediationNetwork = [self getEnumValueFromString:mediationNetworkString]; + if (mediationNetwork == -1) { //mediation network not found. + result([FlutterError errorWithCode:@"INVALID_MEDIATION_NETWORK" + message:@"The provided mediation network is not supported." + details:nil]); + return; + } + + NSDictionary *additionalParameters = call.arguments[@"additionalParameters"]; + if ([additionalParameters isEqual:[NSNull null]]) { + additionalParameters = nil; // Set to nil to avoid sending NSNull to the SDK which cannot be proseesed. + } + + AFAdRevenueData *adRevenueData = [[AFAdRevenueData alloc] + initWithMonetizationNetwork:monetizationNetwork + mediationNetwork:mediationNetwork + currencyIso4217Code:currencyIso4217Code + eventRevenue:revenueValue]; + + [[AppsFlyerLib shared] logAdRevenue:adRevenueData additionalParameters:additionalParameters]; + + } @catch (NSException *exception) { + result([FlutterError errorWithCode:@"UNEXPECTED_ERROR" + message:[NSString stringWithFormat:@"[logAdRevenue]: An error occurred retrieving method arguments: %@", exception.reason] + details:nil]); + NSLog(@"AppsFlyer, Exception occurred in [logAdRevenue]: %@", exception.reason); + } + +} + +- (AppsFlyerAdRevenueMediationNetworkType)getEnumValueFromString:(NSString *)mediationNetworkString { + NSDictionary *stringToEnumMap = @{ + @"google_admob": @(AppsFlyerAdRevenueMediationNetworkTypeGoogleAdMob), + @"ironsource": @(AppsFlyerAdRevenueMediationNetworkTypeIronSource), + @"applovin_max": @(AppsFlyerAdRevenueMediationNetworkTypeApplovinMax), + @"fyber": @(AppsFlyerAdRevenueMediationNetworkTypeFyber), + @"appodeal": @(AppsFlyerAdRevenueMediationNetworkTypeAppodeal), + @"admost": @(AppsFlyerAdRevenueMediationNetworkTypeAdmost), + @"topon": @(AppsFlyerAdRevenueMediationNetworkTypeTopon), + @"tradplus": @(AppsFlyerAdRevenueMediationNetworkTypeTradplus), + @"yandex": @(AppsFlyerAdRevenueMediationNetworkTypeYandex), + @"chartboost": @(AppsFlyerAdRevenueMediationNetworkTypeChartBoost), + @"unity": @(AppsFlyerAdRevenueMediationNetworkTypeUnity), + @"topon_pte": @(AppsFlyerAdRevenueMediationNetworkTypeToponPte), + @"custom_mediation": @(AppsFlyerAdRevenueMediationNetworkTypeCustom), + @"direct_monetization_network": @(AppsFlyerAdRevenueMediationNetworkTypeDirectMonetization) + }; + + NSNumber *enumValueNumber = stringToEnumMap[mediationNetworkString]; + if (enumValueNumber) { + return (AppsFlyerAdRevenueMediationNetworkType)[enumValueNumber integerValue]; + } else { + return -1; + } +} + +- (id)requireNonNullArgumentWithCall:(FlutterMethodCall*)call result:(FlutterResult)result argumentName:(NSString *)argumentName errorCode:(NSString *)errorCode { + id value = call.arguments[argumentName]; + if (value == nil) { + result([FlutterError + errorWithCode:errorCode + message:[NSString stringWithFormat:@"%@ must not be null", argumentName] + details:nil]); + NSLog(@"AppsFlyer, %@ must not be null", argumentName); + } + return value; +} + +- (void)enableTCFDataCollection:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL shouldCollect = [call.arguments[@"shouldCollect"] boolValue]; + [[AppsFlyerLib shared] enableTCFDataCollection:shouldCollect]; + result(nil); +} + +- (void)addPushNotificationDeepLinkPath:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSArray* deeplinkPath = call.arguments; + if(deeplinkPath != nil){ + [[AppsFlyerLib shared] addPushNotificationDeepLinkPath:deeplinkPath]; + } + result(nil); +} + +- (void)setResolveDeepLinkURLs:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSArray* urlsArr = call.arguments; + if(urlsArr != nil){ + [[AppsFlyerLib shared] setResolveDeepLinkURLs:urlsArr]; + } + result(nil); +} + +- (void)setPartnerData:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* partnerId = call.arguments[@"partnerId"]; + NSDictionary* partnersData = call.arguments[@"partnersData"]; + if(partnersData == [NSNull null]){ + partnersData = nil; + }; + [[AppsFlyerLib shared] setPartnerDataWithPartnerId:partnerId partnerInfo:partnersData]; + result(nil); +} + +- (void)setDisableAdvertisingIdentifiers:(FlutterMethodCall*)call result:(FlutterResult)result{ + id isAdvertiserIdEnabled = call.arguments; + if ([isAdvertiserIdEnabled isKindOfClass:[NSNumber class]]) { + BOOL _isAdvertiserIdEnabled = [isAdvertiserIdEnabled boolValue]; + [[AppsFlyerLib shared] setDisableAdvertisingIdentifier: _isAdvertiserIdEnabled]; + } + result(nil); +} + +- (void)setSharingFilterForPartners:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSArray* partners = call.arguments; + [[AppsFlyerLib shared] setSharingFilterForPartners: partners]; + result(nil); +} + +- (void)setCurrentDeviceLanguage:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* language = call.arguments; + [[AppsFlyerLib shared] setCurrentDeviceLanguage: language]; + result(nil); +} + +- (void)disableSKAdNetwork:(FlutterMethodCall*)call result:(FlutterResult)result{ + id isSKADEnabled = call.arguments; + if ([isSKADEnabled isKindOfClass:[NSNumber class]]) { + _isSKADEnabled = [(NSNumber*)isSKADEnabled boolValue]; + [AppsFlyerLib shared].disableSKAdNetwork = _isSKADEnabled; + } + result(nil); +} + +- (void)useReceiptValidationSandbox:(FlutterMethodCall*)call result:(FlutterResult)result{ + id isSandboxEnabled = call.arguments; + if ([isSandboxEnabled isKindOfClass:[NSNumber class]]) { + _isSandboxEnabled = [(NSNumber*)isSandboxEnabled boolValue]; + [AppsFlyerLib shared].useReceiptValidationSandbox = _isSandboxEnabled; + } + result(nil); +} + +- (void)enableFacebookDeferredApplinks:(FlutterMethodCall*)call result:(FlutterResult)result{ + id isFacebookDeferredApplinksEnabled = call.arguments[@"isFacebookDeferredApplinksEnabled"]; + if ([isFacebookDeferredApplinksEnabled isKindOfClass:[NSNumber class]]) { + if([(NSNumber*)isFacebookDeferredApplinksEnabled boolValue]){ + [[AppsFlyerLib shared] enableFacebookDeferredApplinksWithClass:NSClassFromString(@"FBSDKAppLinkUtility")]; + } + } + result(nil); +} + +- (void)anonymizeUser:(FlutterMethodCall*)call result:(FlutterResult)result { + id shouldAnonymize = call.arguments[@"shouldAnonymize"]; + if ([shouldAnonymize isKindOfClass:[NSNumber class]]) { + [AppsFlyerLib shared].anonymizeUser = [(NSNumber*)shouldAnonymize boolValue]; + } + result(nil); +} + +- (void)setPushNotification:(FlutterMethodCall*)call result:(FlutterResult)result{ + id isPushNotificationEnabled = call.arguments; + if ([isPushNotificationEnabled isKindOfClass:[NSNumber class]]) { + _isPushNotificationEnabled = [(NSNumber*)isPushNotificationEnabled boolValue]; + } + result(nil); +} + +- (void)sendPushNotificationData:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSDictionary* userInfo = call.arguments; + [[AppsFlyerLib shared] handlePushNotification:userInfo]; + result(nil); +} + +- (void)setOneLinkCustomDomain:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSArray* brandDomains = call.arguments; + [[AppsFlyerLib shared] setOneLinkCustomDomains:brandDomains]; + result(nil); +} + +- (void)startListening:(FlutterMethodCall*)call result:(FlutterResult)result{ + // Prepare callback dictionary + if (_callbackById == nil) _callbackById = [NSMutableArray array]; + + NSString* callbackId = call.arguments; + if ([callbackId isEqualToString:afGCDCallback]){ + _gcdCallback = true; + } + if ([callbackId isEqualToString:afOAOACallback]){ + _oaoaCallback = true; + } + if ([callbackId isEqualToString:afUDPCallback]){ + _udpCallback = true; + } + [_callbackById addObject:callbackId]; +} + +- (void)generateInviteLink:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* customerID = call.arguments[@"customerID"]; + NSString* referrerImageUrl = call.arguments[@"referrerImageUrl"]; + NSString* brandDomain = call.arguments[@"brandDomain"]; + NSString* baseDeeplink = call.arguments[@"baseDeeplink"]; + NSString* referrerName = call.arguments[@"referrerName"]; + NSString* channel = call.arguments[@"channel"]; + NSString* campaign = call.arguments[@"campaign"]; + NSDictionary* customParams = call.arguments[@"customParams"]; + + //Explicitly setting the values of the parameters to be nil in case they are initially received as . + if (customerID == [NSNull null]) { + customerID = nil; + } + if (referrerImageUrl == [NSNull null]) { + referrerImageUrl = nil; + } + if (brandDomain == [NSNull null]) { + brandDomain = nil; + } + if (baseDeeplink == [NSNull null]) { + baseDeeplink = nil; + } + if (referrerName == [NSNull null]) { + referrerName = nil; + } + if (channel == [NSNull null]) { + channel = nil; + } + if (campaign == [NSNull null]) { + campaign = nil; + } + if(customParams == [NSNull null]){ + customParams = nil; + }; + + [AppsFlyerShareInviteHelper generateInviteUrlWithLinkGenerator:^AppsFlyerLinkGenerator * _Nonnull(AppsFlyerLinkGenerator * _Nonnull generator) { + [generator setChannel:channel]; + [generator setCampaign:campaign]; + [generator setBrandDomain:brandDomain]; + [generator setBaseDeeplink:baseDeeplink]; + [generator setReferrerName:referrerName]; + [generator setReferrerImageURL:referrerImageUrl]; + [generator setReferrerCustomerId:customerID]; + [generator addParameters:customParams]; + + return generator; + } completionHandler:^(NSURL * _Nullable url) { + NSString * resultURL = url.absoluteString; + NSDictionary* resultURLObject; + if(resultURL != nil){ + resultURLObject = @{ + @"userInviteURL": resultURL + }; + if([_callbackById containsObject:afGenerateInviteLinkSuccess]){ + [_streamHandler sendResponseToFlutter:afGenerateInviteLinkSuccess status:afSuccess data:resultURLObject]; + } + }else{ + resultURLObject = @{ + @"error": @"The URL wasn't generated!" + }; + if([_callbackById containsObject:afGenerateInviteLinkFailure]){ + [_streamHandler sendResponseToFlutter:afGenerateInviteLinkFailure status:afFailure data:resultURLObject]; + } + } + }]; + + result(nil); +} + + + + +- (void)setAppInviteOneLinkID:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* oneLinkID = call.arguments[@"oneLinkID"]; + [AppsFlyerLib shared].appInviteOneLinkID = oneLinkID; + if([_callbackById containsObject:@"setAppInviteOneLinkIDCallback"]){ + NSDictionary* message = @{ + @"status": afSuccess + }; + [_streamHandler sendResponseToFlutter:afAppInviteOneLinkID status:afSuccess data:message]; + } + result(nil); +} + +- (void)logCrossPromotionImpression:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* appId = call.arguments[@"appId"]; + NSString* campaign = call.arguments[@"campaign"]; + NSDictionary* parameters = call.arguments[@"data"]; + + [AppsFlyerCrossPromotionHelper logCrossPromoteImpression:appId campaign:campaign parameters:parameters]; +} + +- (void)logCrossPromotionAndOpenStore:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* campaign = call.arguments[@"campaign"]; + NSDictionary* customParams = call.arguments[@"params"]; + + [AppsFlyerShareInviteHelper generateInviteUrlWithLinkGenerator:^AppsFlyerLinkGenerator * _Nonnull(AppsFlyerLinkGenerator * _Nonnull generator) { + if (campaign != nil && ![campaign isEqualToString:@""]) { + [generator setCampaign:campaign]; + } + if (![customParams isKindOfClass:[NSNull class]]) { + [generator addParameters:customParams]; + } + + return generator; + } completionHandler: ^(NSURL * _Nullable url) { + NSString *appLink = url.absoluteString; + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:appLink] options:@{} completionHandler:^(BOOL success) { + }]; + } else { + // Fallback on earlier versions + } + }]; +} + +- (void)setSharingFilter:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSArray* filters = call.arguments; + [[AppsFlyerLib shared] setSharingFilter:filters]; + result(nil); +} + +- (void)setSharingFilterForAllPartners:(FlutterResult)result{ + [[AppsFlyerLib shared] setSharingFilterForAllPartners]; + result(nil); +} + +- (void)getAppsFlyerUID:(FlutterResult)result{ + result([[AppsFlyerLib shared] getAppsFlyerUID]); +} + +- (void)getHostPrefix:(FlutterResult)result{ + result([[AppsFlyerLib shared] hostPrefix]); +} + +- (void)getHostName:(FlutterResult)result{ + result([[AppsFlyerLib shared] host]); +} + +- (void)getSDKVersion:(FlutterResult)result{ + result([[AppsFlyerLib shared] getSDKVersion]); +} + +- (void)setHost:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* hostName = call.arguments[@"hostName"]; + NSString* hostPrefix = call.arguments[@"hostPrefix"]; + [[AppsFlyerLib shared] setHost:hostName withHostPrefix:hostPrefix]; + result(nil); +} + +- (void)validateAndLogInAppPurchase:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* productIdentifier = call.arguments[@"productIdentifier"]; + NSString* price = call.arguments[@"price"]; + NSString* currency = call.arguments[@"currency"]; + NSString* transactionId = call.arguments[@"transactionId"]; + NSDictionary* additionalParameters = call.arguments[@"additionalParameters"]; + + [[AppsFlyerLib shared] validateAndLogInAppPurchase:productIdentifier price:price currency:currency transactionId:transactionId additionalParameters:additionalParameters + success:^(NSDictionary *response) { + NSLog(@"AppsFlyer Debug: validateAndLogInAppIosPurchase Success!"); + [self onValidateSuccess:response]; + } + failure:^(NSError *error, id reponse) { + NSLog(@"AppsFlyer Debug: validateAndLogInAppIosPurchase failed with Error: %@", error); + [self onValidateFail:error]; + }]; + + result(nil); +} + +- (void)validateAndLogInAppPurchaseV2:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary* purchaseDetailsMap = call.arguments[@"purchaseDetails"]; + NSDictionary* additionalParameters = call.arguments[@"additionalParameters"]; + + if (purchaseDetailsMap == nil) { + result([FlutterError errorWithCode:@"INVALID_ARGUMENTS" + message:@"Purchase details cannot be null" + details:nil]); + return; + } + + NSString* purchaseTypeString = purchaseDetailsMap[@"purchaseType"]; + NSString* transactionId = purchaseDetailsMap[@"purchaseToken"]; // purchaseToken maps to transactionId on iOS + NSString* productId = purchaseDetailsMap[@"productId"]; + + if (purchaseTypeString == nil || transactionId == nil || productId == nil) { + result([FlutterError errorWithCode:@"INVALID_ARGUMENTS" + message:@"Purchase details must contain purchaseType, purchaseToken, and productId" + details:nil]); + return; + } + + // Map Dart enum to iOS AFSDKPurchaseType + AFSDKPurchaseType purchaseType = [purchaseTypeString isEqualToString:@"subscription"] + ? AFSDKPurchaseTypeSubscription + : AFSDKPurchaseTypeOneTimePurchase; + + AFSDKPurchaseDetails *purchaseDetails = [[AFSDKPurchaseDetails alloc] initWithProductId:productId + transactionId:transactionId + purchaseType:purchaseType]; + + // Handle NSNull for additionalParameters + NSDictionary* purchaseAdditionalDetails = [additionalParameters isEqual:[NSNull null]] ? nil : additionalParameters; + + [[AppsFlyerLib shared] validateAndLogInAppPurchase:purchaseDetails + purchaseAdditionalDetails:purchaseAdditionalDetails + completion:^(NSDictionary * _Nullable response, NSError * _Nullable error) { + if (error) { + NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 failed: %@", error.localizedDescription); + result([FlutterError errorWithCode:@"VALIDATION_ERROR" + message:error.localizedDescription ?: @"Purchase validation failed" + details:@{ + @"error_code": @(error.code), + @"error_domain": error.domain ?: @"Unknown" + }]); + return; + } + + NSLog(@"AppsFlyer Debug: validateAndLogInAppPurchaseV2 Success!"); + result(response); + }]; +} + +- (void)onValidateSuccess: (NSDictionary*) data{ + [_streamHandler sendResponseToFlutter:afValidatePurchase status:afSuccess data:data]; +} + +-(void)onValidateFail:(NSError*)error{ + NSDictionary* errorObject = @{ + @"error": @"error" + }; + if(error != nil){ + errorObject = @{ + @"error": error.description + }; + } + + [_streamHandler sendResponseToFlutter:afValidatePurchase status:afFailure data:errorObject]; + [self performSelectorOnMainThread:@selector(handleCallback:) withObject:@[errorObject,afValidatePurchaseChannel] waitUntilDone:NO]; +} + +- (void)setAdditionalData:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSDictionary* data = call.arguments[@"customData"]; + [[AppsFlyerLib shared] setAdditionalData:data]; + result(nil); +} + +- (void)setCustomerUserId:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* userId = call.arguments[@"id"]; + [[AppsFlyerLib shared] setCustomerUserID:userId]; + result(nil); +} + +- (void)setCurrencyCode:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* currencyCode = call.arguments[@"currencyCode"]; + [[AppsFlyerLib shared] setCurrencyCode:currencyCode]; + result(nil); +} + +- (void)stop:(FlutterMethodCall*)call result:(FlutterResult)result{ + BOOL stop = [[call.arguments objectForKey:@"isStopped"] boolValue]; + [AppsFlyerLib shared].isStopped = stop; + result(nil); +} + +- (void)updateServerUninstallToken:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* deviceToken = call.arguments[@"token"]; + deviceToken = [deviceToken stringByReplacingOccurrencesOfString:@" " withString:@""]; + NSMutableData *deviceTokenData= [[NSMutableData alloc] init]; + unsigned char whole_byte; + char byte_chars[3] = {'\0','\0','\0'}; + int i; + for (i=0; i < [deviceToken length]/2; i++) { + byte_chars[0] = [deviceToken characterAtIndex:i*2]; + byte_chars[1] = [deviceToken characterAtIndex:i*2+1]; + whole_byte = strtol(byte_chars, NULL, 16); + [deviceTokenData appendBytes:&whole_byte length:1]; + } + [[AppsFlyerLib shared] registerUninstall:deviceTokenData]; + result(nil); +} + +- (void)waitForCustomerId:(FlutterMethodCall*)call result:(FlutterResult)result{ + result(nil); +} + +- (void)setUserEmails:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSMutableArray *emails = call.arguments[@"emails"]; + NSArray *emaillsArray = [emails copy]; + NSNumber* cryptTypeInt = (id)call.arguments[@"cryptType"]; + + EmailCryptType cryptType = EmailCryptTypeNone; + if(1 == [cryptTypeInt doubleValue]){ + cryptType = EmailCryptTypeSHA256; + } + + [[AppsFlyerLib shared] setUserEmails:emaillsArray withCryptType:cryptType]; + result(nil); +} + +- (void)initSdkWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString* devKey = nil; + NSString* appId = nil; + NSString* appInviteOneLink = nil; + BOOL manualStart = NO; + BOOL disableCollectASA = NO; + BOOL disableAdvertisingIdentifier = NO; + NSTimeInterval timeToWaitForATTUserAuthorization = 0; + BOOL isDebug = NO; + BOOL isConversionData = NO; + BOOL isUDP = NO; + + id isDebugValue = nil; + id isConversionDataValue = nil; + id isUDPValue = nil; + id isDisableCollectASA = nil; + id isDisableAdvertisingIdentifier = nil; + id isManualStart = nil; + + devKey = call.arguments[afDevKey]; + appId = call.arguments[afAppId]; + timeToWaitForATTUserAuthorization = [(id)call.arguments[afTimeToWaitForATTUserAuthorization] doubleValue]; + + isManualStart = call.arguments[afManualStart]; + if([isManualStart isKindOfClass:[NSNumber class]]){ + manualStart = [(NSNumber*)isManualStart boolValue]; + [self setIsManualStart:manualStart]; + } + + + isDebugValue = call.arguments[afIsDebug]; + if ([isDebugValue isKindOfClass:[NSNumber class]]) { + // isDebug is a boolean that will come through as an NSNumber + isDebug = [(NSNumber*)isDebugValue boolValue]; + } + + [AppsFlyerLib shared].appleAppID = appId; + [AppsFlyerLib shared].appsFlyerDevKey = devKey; + [AppsFlyerLib shared].isDebug = isDebug; + + isConversionDataValue = call.arguments[afConversionData]; + if ([isConversionDataValue isKindOfClass:[NSNumber class]]) { + isConversionData = [(NSNumber*)isConversionDataValue boolValue]; + } + if (isConversionData == YES) { + [[AppsFlyerLib shared] setDelegate:_streamHandler]; + } + + isUDPValue = call.arguments[afUDL]; + if ([isUDPValue isKindOfClass:[NSNumber class]]) { + isUDP = [(NSNumber*)isUDPValue boolValue]; + if(isUDP == YES){ + [AppsFlyerLib shared].deepLinkDelegate = _streamHandler; + } + } + + appInviteOneLink = call.arguments[afInviteOneLink]; + if (appInviteOneLink != nil && appInviteOneLink != [NSNull null]) { + [AppsFlyerLib shared].appInviteOneLinkID = appInviteOneLink; + } + + isDisableCollectASA = call.arguments[afDisableCollectASA]; + if ([isDisableCollectASA isKindOfClass:[NSNumber class]]) { + // isDebug is a boolean that will come through as an NSNumber + disableCollectASA = [(NSNumber*)isDisableCollectASA boolValue]; + } + isDisableAdvertisingIdentifier = call.arguments[afDisableAdvertisingIdentifier]; + if ([isDisableAdvertisingIdentifier isKindOfClass:[NSNumber class]]) { + // isDebug is a boolean that will come through as an NSNumber + disableAdvertisingIdentifier = [(NSNumber*)isDisableAdvertisingIdentifier boolValue]; + } + + + [AppsFlyerLib shared].disableCollectASA = disableCollectASA; + + SEL DisableAdvertisingSel = NSSelectorFromString(@"setDisableAdvertisingIdentifier:"); + id AppsFlyer = [AppsFlyerLib shared]; + if ([AppsFlyer respondsToSelector:DisableAdvertisingSel] && disableAdvertisingIdentifier) { + bypassDisableAdvertisingIdentifier msgSend = (bypassDisableAdvertisingIdentifier)objc_msgSend; + msgSend(AppsFlyer, DisableAdvertisingSel, disableAdvertisingIdentifier); + } + + [[AppsFlyerLib shared] setPluginInfoWith:AFSDKPluginFlutter pluginVersion:kAppsFlyerPluginVersion additionalParams:nil]; + + + // SEL WaitForATTSel = NSSelectorFromString(@"waitForATTUserAuthorizationWithTimeoutInterval:"); + + // if ([AppsFlyer respondsToSelector:WaitForATTSel] && timeToWaitForATTUserAuthorization != 0) { + // bypassWaitForATTUserAuthorization msgSend = (bypassWaitForATTUserAuthorization)objc_msgSend; + // msgSend(AppsFlyer, WaitForATTSel, timeToWaitForATTUserAuthorization); + // } + + if (timeToWaitForATTUserAuthorization != 0) { + [[AppsFlyerLib shared] waitForATTUserAuthorizationWithTimeoutInterval:timeToWaitForATTUserAuthorization]; + } + + if (manualStart == NO){ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; + [[AppsFlyerLib shared] start]; + } + + //post notification for the deep link object that the bridge is set and he can handle deep link + [AppsFlyerAttribution shared].isBridgeReady = YES; + [[NSNotificationCenter defaultCenter] postNotificationName:AF_BRIDGE_SET object:self]; + + + result(@{@"status": @"OK"}); +} + +-(void)logEventWithCall:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSString *eventName = call.arguments[afEventName]; + NSDictionary *eventValues = call.arguments[afEventValues]; + + // Explicitily setting the values to be nil if call.arguments[afEventValues] returns . + if (eventValues == [NSNull null]) { + eventValues = nil; + } + + [[AppsFlyerLib shared] logEvent:eventName withValues:eventValues]; + //TODO: Add callback handler + result(@YES); +} + +- (void)setMinTimeBetweenSessions:(FlutterMethodCall*)call result:(FlutterResult)result{ + NSInteger seconds = [(id)call.arguments[@"seconds"] integerValue]; + [AppsFlyerLib shared].minTimeBetweenSessions = seconds; + result(nil); +} + +- (void)appDidBecomeActive { + [[AppsFlyerLib shared] start]; + NSLog(@"App Did Become Active"); +} + + ++ (FlutterViewController*) getViewController{ + UIWindow *window = nil; + if (@available(iOS 13.0, *)) { + for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive) { + window = scene.windows.firstObject; + break; + } + } + } + if (window == nil) { + window = [[[UIApplication sharedApplication] delegate] window]; + } + UIViewController *topMostViewControllerObj = window.rootViewController; + FlutterViewController *flutterViewController = (FlutterViewController *)topMostViewControllerObj; + return flutterViewController; +} + +-(void) handleCallback:(NSArray *) objArray{ + NSDictionary* message = [objArray objectAtIndex:0]; + //NSString* channel = [objArray objectAtIndex:1]; + + NSError *error; + NSData *dataFromDict = [NSJSONSerialization dataWithJSONObject:message + options:NSJSONWritingPrettyPrinted + error:&error]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"af-events" object:dataFromDict]; + //if(!error){ + //[flutterViewController sendOnChannel:channel message:dataFromDict binaryReply:^(NSData * _Nullable reply) { + // + //}]; + //} +} + +# pragma mark - handle deep links +// Deep linking +// Open URI-scheme for iOS 9 and above +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *) options { + [[AppsFlyerAttribution shared] handleOpenUrl:url options:options]; + + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. + return NO; + +} +// Open URI-scheme for iOS 8 and below +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation { + [[AppsFlyerAttribution shared] handleOpenUrl:url sourceApplication:sourceApplication annotation:annotation]; + + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. + return NO; + +} +// Open Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { + [[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:restorationHandler]; + + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. + return NO; +} + +#if __has_include() +#pragma mark - FlutterSceneLifeCycleDelegate + +// UIScene-based URI-scheme deep links (iOS 13+, Flutter 3.41+ UIScene migration) +- (BOOL)scene:(UIScene*)scene openURLContexts:(NSSet*)URLContexts API_AVAILABLE(ios(13.0)) { + for (UIOpenURLContext *context in URLContexts) { + NSDictionary *opts = @{}; + if (context.options.sourceApplication) { + opts = @{UIApplicationOpenURLOptionsSourceApplicationKey: context.options.sourceApplication}; + } + [[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:opts]; + } + return NO; +} + +// Cold-start deep links delivered via UISceneConnectionOptions (iOS 13+) +// Handles both URI-scheme links (URLContexts) and Universal Links (userActivities) +- (BOOL)scene:(UIScene*)scene + willConnectToSession:(UISceneSession*)session + options:(UISceneConnectionOptions*)connectionOptions API_AVAILABLE(ios(13.0)) { + for (UIOpenURLContext *context in connectionOptions.URLContexts) { + NSDictionary *opts = @{}; + if (context.options.sourceApplication) { + opts = @{UIApplicationOpenURLOptionsSourceApplicationKey: context.options.sourceApplication}; + } + [[AppsFlyerAttribution shared] handleOpenUrl:context.URL options:opts]; + } + for (NSUserActivity *activity in connectionOptions.userActivities) { + if ([activity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + [[AppsFlyerAttribution shared] continueUserActivity:activity restorationHandler:nil]; + } + } + return NO; +} + +// UIScene-based Universal Links (iOS 13+) +- (BOOL)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity API_AVAILABLE(ios(13.0)) { + [[AppsFlyerAttribution shared] continueUserActivity:userActivity restorationHandler:nil]; + return NO; +} +#endif // __has_include() + + +@end diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h new file mode 100644 index 0000000..e6bed52 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerAttribution.h @@ -0,0 +1,30 @@ +// +// AppsFlyerAttribution.h +// Pods +// +// Created by Amit Kremer on 11/02/2021. +// + +#ifndef AppsFlyerAttribution_h +#define AppsFlyerAttribution_h +#endif /* AppsFlyerAttribution_h */ + +#import + +@interface AppsFlyerAttribution : NSObject +@property NSUserActivity*_Nullable userActivity; +@property (nonatomic, copy) void (^ _Nullable restorationHandler)(NSArray *_Nullable ); +@property NSURL * _Nullable url; +@property NSDictionary * _Nullable options; +@property NSString* _Nullable sourceApplication; +@property id _Nullable annotation; +@property BOOL isBridgeReady; + ++ (AppsFlyerAttribution *_Nullable)shared; +- (void) continueUserActivity: (NSUserActivity*_Nullable) userActivity restorationHandler: (void (^_Nullable)(NSArray * _Nullable))restorationHandler; +- (void) handleOpenUrl:(NSURL*_Nullable)url options:(NSDictionary*_Nullable) options; +- (void) handleOpenUrl: (NSURL *_Nullable)url sourceApplication:(NSString*_Nullable)sourceApplication annotation:(id _Nullable )annotation; + +@end + +static NSString * _Nullable const AF_BRIDGE_SET = @"bridge is set"; diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h new file mode 100644 index 0000000..69eee59 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsFlyerStreamHandler.h @@ -0,0 +1,24 @@ +// +// AppsFlyerStreamHandler.h +// appsflyer_sdk +// +// Created by Shahar Cohen on 05/09/2019. +// + +#import +#import + +// I will change it to seperate file with #defines +#import "AppsflyerSdkPlugin.h" +#import "AppsFlyerAttribution.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface AppsFlyerStreamHandler: NSObject + +- (void)sendResponseToFlutter:(NSString *)responseID status:(NSString *)status data:(NSDictionary *)data; +- (NSString*) getStatusAsString:(AFSDKDeepLinkResultStatus)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h new file mode 100644 index 0000000..213d0a8 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/AppsflyerSdkPlugin.h @@ -0,0 +1,65 @@ +#import +#import "AppsFlyerAttribution.h" +#if __has_include() // from Pod +#import +#else +#import "AppsFlyerLib.h" +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +@interface AppsflyerSdkPlugin: NSObject +#else +@interface AppsflyerSdkPlugin: NSObject +#endif + +@property (readwrite, nonatomic) BOOL isManualStart; + ++ (FlutterMethodChannel*)callbackChannel; ++ (BOOL)gcdCallback; ++ (BOOL)oaoaCallback; ++ (BOOL)udpCallback; + +@end + +// Appsflyer JS objects +#define kAppsFlyerPluginVersion @"6.18.0" +#define afDevKey @"afDevKey" +#define afAppId @"afAppId" +#define afIsDebug @"isDebug" +#define afManualStart @"manualStart" +#define afTimeToWaitForATTUserAuthorization @"timeToWaitForATTUserAuthorization" +#define afEventName @"eventName" +#define afEventValues @"eventValues" +#define afConversionData @"GCD" +#define afUDL @"UDL" +#define afInviteOneLink @"appInviteOneLink" +#define afDisableCollectASA @"disableCollectASA" +#define afDisableAdvertisingIdentifier @"disableAdvertisingIdentifier" + +// Appsflyer native objects +#define afOnInstallConversionData @"onInstallConversionData" +#define afSuccess @"success" +#define afFailure @"failure" +#define afOnAttributionFailure @"onAttributionFailure" +#define afValidatePurchase @"validatePurchase" +#define afOnAppOpenAttribution @"onAppOpenAttribution" +#define afOnDeepLinking @"onDeepLinking" +#define afOnInstallConversionFailure @"onInstallConversionFailure" +#define afOnInstallConversionDataLoaded @"onInstallConversionDataLoaded" +#define afGCDCallback @"onInstallConversionData" +#define afOAOACallback @"onAppOpenAttribution" +#define afUDPCallback @"onDeepLinking" +#define afGenerateInviteLinkSuccess @"generateInviteLinkSuccess" +#define afGenerateInviteLinkFailure @"generateInviteLinkFailure" +#define afAppInviteOneLinkID @"setAppInviteOneLinkIDCallback" + +// Stream Channels +#define afMethodChannel @"af-api" +#define afCallbacksMethodChannel @"callbacks" +#define afEventChannel @"af-events" +#define afValidatePurchaseChannel @"af-validate-purchase" + diff --git a/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h new file mode 100644 index 0000000..7cc93a2 --- /dev/null +++ b/ios/appsflyer_sdk/Sources/appsflyer_sdk/include/appsflyer_sdk/FlutterAppDelegate+AppsFlyerStreamHandler.h @@ -0,0 +1,16 @@ +// +// FlutterAppDelegate+AppsFlyerStreamHandler.h +// appsflyer_sdk +// +// Created by Shahar Cohen on 05/09/2019. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FlutterAppDelegate () + +@end + +NS_ASSUME_NONNULL_END