diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..2b510da --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "AppsFlyerLib", + "repositoryURL": "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", + "state": { + "branch": null, + "revision": "1741d025d5bdd8a64c42854ba0fcfd7f768e4594", + "version": "6.17.5" + } + }, + { + "package": "mParticle-Apple-SDK", + "repositoryURL": "https://github.com/mParticle/mparticle-apple-sdk", + "state": { + "branch": null, + "revision": "e629ee514f760bd14f195ac34370afba4c816baa", + "version": "8.38.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index e215bf2..0f0fb55 100644 --- a/Package.swift +++ b/Package.swift @@ -40,6 +40,12 @@ let package = Package( ], path: "SPM/mParticle-AppsFlyer-NoLocation", resources: [.process("PrivacyInfo.xcprivacy")] - ) + ), + + .testTarget( + name: "mParticle-AppsFlyer-Swift-Tests", + dependencies: ["mParticle-AppsFlyer"], + path: "mParticle_AppsFlyerTests" + ), ] ) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 1350e87..8032f70 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -21,9 +21,32 @@ NSString *const afAppsFlyerIdIntegrationKey = @"appsflyer_id_integration_setting"; NSString *const kMPKAFCustomerUserId = @"af_customer_user_id"; +// Consent Mapping Keys +NSString *const kMPAFAdStorageKey = @"ad_storage"; +NSString *const kMPAFAdUserDataKey = @"ad_user_data"; +NSString *const kMPAFAdPersonalizationKey = @"ad_personalization"; + +// Default Consent Keys (from mParticle UI) +NSString *const kMPAFDefaultAdStorageKey = @"defaultAdStorageConsent"; +NSString *const kMPAFDefaultAdUserDataKey = @"defaultAdUserDataConsent"; +NSString *const kMPAFDefaultAdPersonalizationKey = @"defaultAdPersonalizationConsent"; + static AppsFlyerLib *appsFlyerTracker = nil; static id temporaryDelegate = nil; +@implementation NSString(PRIVATE) + +- (NSNumber*)isGranted { + if ([self isEqualToString:@"Granted"]) { + return @(YES); + } else if ([self isEqualToString:@"Denied"]) { + return @(NO); + } + return nil; +} + +@end + @interface MPKitAppsFlyer() @end @@ -88,6 +111,8 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu appsFlyerTracker.deepLinkDelegate = self; _configuration = configuration; + + [self updateConsent]; [appsFlyerTracker waitForATTUserAuthorizationWithTimeoutInterval:60]; [self start]; @@ -438,8 +463,110 @@ - (MPKitAPI *)kitApi { return _kitApi; } +- (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { + [self updateConsent]; + return [[MPKitExecStatus alloc] initWithSDKCode:@(MPKitInstanceAppsFlyer) + returnCode:MPKitReturnCodeSuccess]; +} + +- (void)updateConsent { + NSArray *mappings = [self mappingForKey: @"consentMapping"]; + NSDictionary *mappingsConfig; + if (mappings != nil) { + mappingsConfig = [self convertToKeyValuePairs: mappings]; + } + + BOOL isUserSubjectToGDPR = NO; + + NSString *gdprValue = _configuration[@"gdprApplies"]; + if ([gdprValue isKindOfClass:[NSString class]]) { + isUserSubjectToGDPR = [gdprValue boolValue]; + } + + MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; + NSDictionary *gdprConsents = currentUser.consentState.gdprConsentState; + + if (gdprConsents.count > 0) { + isUserSubjectToGDPR = YES; + } + + NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey + defaultKey:kMPAFDefaultAdUserDataKey + gdprConsents:gdprConsents + mapping:mappingsConfig]; + + NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey + defaultKey:kMPAFDefaultAdPersonalizationKey + gdprConsents:gdprConsents + mapping:mappingsConfig]; + + NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey + defaultKey:kMPAFDefaultAdStorageKey + gdprConsents:gdprConsents + mapping:mappingsConfig]; + + + AppsFlyerConsent *consentObj = [[AppsFlyerConsent alloc] + initWithIsUserSubjectToGDPR:@(isUserSubjectToGDPR) + hasConsentForDataUsage:isUserSubjectToGDPR ? dataUsage : nil + hasConsentForAdsPersonalization:isUserSubjectToGDPR ? personalization : nil + hasConsentForAdStorage:isUserSubjectToGDPR ? storage : nil]; + + // Update consent state with AppsFlyer + [appsFlyerTracker setConsentData:consentObj]; +} + #pragma helper methods +- (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey + defaultKey:(NSString *)defaultKey + gdprConsents:(NSDictionary *)gdprConsents + mapping:(NSDictionary *) mapping { + + // Prefer mParticle Consent if available + NSString *purpose = mapping[mappingKey]; + if (purpose) { + MPGDPRConsent *consent = gdprConsents[purpose]; + if (consent) { + return @(consent.consented); + } + } + + // Fallback to configuration defaults + NSString *value = self->_configuration[defaultKey]; + return [value isGranted]; +} + +- (NSArray*)mappingForKey:(NSString*)key { + NSString *mappingJson = _configuration[key]; + if (![mappingJson isKindOfClass:[NSString class]]) { + return nil; + } + + NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSArray *result = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error) { + NSLog(@"Failed to parse consent mapping JSON: %@", error.localizedDescription); + return nil; + } + + return result; +} + +- (NSDictionary*)convertToKeyValuePairs: (NSArray*) mappings { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (NSDictionary *entry in mappings) { + NSString *value = entry[@"value"]; + NSString *purpose = [entry[@"map"] lowercaseString]; + if (value && purpose) { + dict[value] = purpose; + } + } + return dict; +} + - (FilteredMParticleUser *)currentUser { return [[self kitApi] getCurrentUserWithKit:self]; } diff --git a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h index 4c6e2a0..4fc51e0 100644 --- a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h +++ b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h @@ -20,10 +20,22 @@ extern NSString * _Nonnull const MPKitAppsFlyerErrorDomain; @property (nonatomic, strong, nonnull) NSDictionary *configuration; @property (nonatomic, unsafe_unretained, readonly) BOOL started; @property (nonatomic, strong, nullable) MPKitAPI *kitApi; +@property (nonatomic, strong, nullable) id providerKitInstance; + (void)setDelegate:(id _Nonnull)delegate; + (NSNumber * _Nonnull)computeProductQuantity:(nullable MPCommerceEvent *)event; + (NSString * _Nullable)generateProductIdList:(nullable MPCommerceEvent *)event; + +- (nullable NSNumber *)resolvedConsentForMappingKey:(NSString * _Nonnull)mappingKey + defaultKey:(NSString * _Nonnull)defaultKey + gdprConsents:(NSDictionary * _Nonnull)gdprConsents + mapping:(NSDictionary * _Nullable)mapping; + +- (nullable NSArray*)mappingForKey:(NSString* _Nonnull)key; + +- (nonnull NSDictionary*)convertToKeyValuePairs: (NSArray * _Nonnull)mappings; + +- (nonnull MPKitExecStatus *)routeCommerceEvent:(nonnull MPCommerceEvent *)commerceEvent; @end extern NSString * _Nonnull const MPKitAppsFlyerAttributionResultKey __deprecated_msg("Use MPKitAppsFlyerConversionResultKey instead."); diff --git a/mParticle_AppsFlyerTests/AppsFlyerLibMock.swift b/mParticle_AppsFlyerTests/AppsFlyerLibMock.swift new file mode 100644 index 0000000..28625e7 --- /dev/null +++ b/mParticle_AppsFlyerTests/AppsFlyerLibMock.swift @@ -0,0 +1,20 @@ +// +// AppsFlyerLibMock.swift +// mParticle-AppsFlyer +// +// Created by Nick Dimitrakas on 9/16/25. +// + +import AppsFlyerLib + +class AppsFlyerLibMock: AppsFlyerLib { + var logEventCalled = false + var logEventEventName: String? + var logEventValues: [AnyHashable : Any]? + + override func logEvent(_ eventName: String, withValues values: [AnyHashable : Any]?) { + logEventCalled = true + logEventEventName = eventName + logEventValues = values + } +} diff --git a/mParticle_AppsFlyerTests/Info.plist b/mParticle_AppsFlyerTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/mParticle_AppsFlyerTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift b/mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift new file mode 100644 index 0000000..ad1a459 --- /dev/null +++ b/mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift @@ -0,0 +1,223 @@ +// +// MPKitAppsFlyerTests.swift +// mParticle-AppsFlyer +// +// Created by Nick Dimitrakas on 9/16/25. +// + +import XCTest +@testable import mParticle_AppsFlyer +import AppsFlyerLib + +extension MPCommerceEvent { + static func mock(action: MPCommerceEventAction = .purchase, products: [MPProduct] = []) -> MPCommerceEvent { + let event = MPCommerceEvent(action: .purchase)! + event.addProducts(products) + return event + } +} + +final class MPKitAppsFlyerTests: XCTestCase { + var kit: MPKitAppsFlyer! + var mock: AppsFlyerLibMock! + static let product1 = MPProduct(name: "foo", sku: "foo-sku", quantity: 3, price: 50) + static let product2 = MPProduct(name: "foo2", sku: "foo-sku-2", quantity: 2, price: 50) + static let product3 = MPProduct(name: "foo3", sku: "foo-sku-,3", quantity: 2, price: 50) + + let fakeProducts: [MPProduct] = [ + MPKitAppsFlyerTests.product1, + MPKitAppsFlyerTests.product2, + MPKitAppsFlyerTests.product3, + ] + + // MARK: - Lifecycle + + override func setUp() { + super.setUp() + kit = MPKitAppsFlyer() + kit.configuration = [:] + mock = AppsFlyerLibMock() + } + + // MARK: - convertToKeyValuePairs + + func test_convertToKeyValuePairs_createsLowercasedMapping() { + let mappings: [[String: String]] = [ + ["value": "ad_storage", "map": "Advertising"], + ["value": "analytics_storage", "map": "Analytics"] + ] + + let result = kit.convert(toKeyValuePairs: mappings) + XCTAssertEqual(result["ad_storage"] as! String, "advertising") + XCTAssertEqual(result["analytics_storage"] as! String, "analytics") + } + + // MARK: - mappingForKey + + func test_mappingForKey_withValidJSON_returnsArray() { + let jsonString = """ + [ + { "value": "ad_storage", "map": "Advertising" }, + { "value": "analytics_storage", "map": "Analytics" } + ] + """ + kit.configuration["consentMappingSDK"] = jsonString + + let result = kit.mapping(forKey: "consentMappingSDK") + XCTAssertNotNil(result) + XCTAssertEqual(result!.count, 2) + } + + func test_mappingForKey_withInvalidJSON_returnsNil() { + kit.configuration["consentMappingSDK"] = "{ not valid json }" + let result = kit.mapping(forKey: "consentMappingSDK") + XCTAssertNil(result) + } + + // MARK: - resolvedConsentForMappingKey + + func test_resolvedConsentForMappingKey_withGDPRMapping_returnsTrue() { + let consent = MPGDPRConsent() + consent.consented = true + let gdprConsents = ["advertising": consent] + + let mapping = ["ad_storage": "advertising"] + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: gdprConsents, + mapping: mapping + ) + XCTAssertEqual(result, true) + } + + func test_resolvedConsentForMappingKey_withGDPRMapping_returnsFalse() { + let consent = MPGDPRConsent() + consent.consented = false + let gdprConsents = ["advertising": consent] + + let mapping = ["ad_storage": "advertising"] + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: gdprConsents, + mapping: mapping + ) + XCTAssertEqual(result, false) + } + + func test_resolvedConsentForMappingKey_withDefaultValue_returnsFalse() { + kit.configuration["defaultAdStorageConsentSDK"] = "Denied" + + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: [:], + mapping: [:] + ) + XCTAssertEqual(result, false) + } + + func test_resolvedConsentForMappingKey_withNoMappingOrDefault_returnsNil() { + let result = kit.resolvedConsent( + forMappingKey: "ad_storage", + defaultKey: "defaultAdStorageConsentSDK", + gdprConsents: [:], + mapping: [:] + ) + XCTAssertNil(result) + } + + func testComputeQuantityWithNoEvent() { + XCTAssertEqual(MPKitAppsFlyer.computeProductQuantity(nil).intValue, 1); + } + + func testComputeQuantityWithNoProducts() { + let event = MPCommerceEvent(action: .purchase) + XCTAssertEqual(MPKitAppsFlyer.computeProductQuantity(event).intValue, 1); + } + + func testComputeQuantityWithProductWithNoQuantity() { + let event = MPCommerceEvent(action: .purchase) + event!.addProducts([.init(name: "foo", sku: "bar", quantity: 0, price: 50)]) + + XCTAssertEqual(MPKitAppsFlyer.computeProductQuantity(event).intValue, 1); + } + + func testComputeQuantityWithProductWithMultipleQuantities() { + let event = MPCommerceEvent(action: .purchase) + event!.addProducts([ + MPKitAppsFlyerTests.product1, + MPKitAppsFlyerTests.product2 + ]) + + XCTAssertEqual(MPKitAppsFlyer.computeProductQuantity(event).intValue, 5); + } + + func testGenerateSkuStringNoEvent() { + XCTAssertNil(MPKitAppsFlyer.generateProductIdList(nil)); + } + + func testGenerateSkuStringNoProducts() { + let event = MPCommerceEvent.mock() + XCTAssertNil(MPKitAppsFlyer.generateProductIdList(event)); + } + + func testGenerateSkuStringSingleProduct() { + let event = MPCommerceEvent.mock(products: [ + MPKitAppsFlyerTests.product1 + ]) + XCTAssertEqual(MPKitAppsFlyer.generateProductIdList(event), "foo-sku") + } + + func testGenerateSkuStringMultipleProducts() { + let event = MPCommerceEvent.mock(products: [ + MPKitAppsFlyerTests.product1, + MPKitAppsFlyerTests.product2 + ]) + XCTAssertEqual(MPKitAppsFlyer.generateProductIdList(event), "foo-sku,foo-sku-2") + } + + func testGenerateSkuStringEmbeddedCommas() { + let event = MPCommerceEvent.mock(products: fakeProducts) + XCTAssertEqual(MPKitAppsFlyer.generateProductIdList(event), "foo-sku,foo-sku-2,foo-sku-%2C3") + } + + func testRouteCommerce() { + let event = MPCommerceEvent.mock(products: fakeProducts) + event.customAttributes = ["test": "Malarkey"] + + let af = MPKitAppsFlyer() + + af.providerKitInstance = mock + af.routeCommerceEvent(event) + + checkLogEventParams() + + XCTAssertEqual(mock.logEventValues!["test"] as! String, "Malarkey") + } + + func testRouteCommerceNilCustomAttributes() { + let event = MPCommerceEvent.mock(products: fakeProducts) + event.customAttributes = nil + + let af = MPKitAppsFlyer() + + af.providerKitInstance = mock + af.routeCommerceEvent(event) + + checkLogEventParams() + } + + func checkLogEventParams() { + XCTAssertTrue(mock.logEventCalled) + XCTAssertEqual(mock.logEventEventName, AFEventPurchase) + XCTAssertEqual(mock.logEventValues!["af_customer_user_id"] as! String , "0") + XCTAssertEqual(mock.logEventValues!["af_quantity"] as! NSNumber, 7) + XCTAssertEqual(mock.logEventValues!["af_content_id"] as! String, "foo-sku,foo-sku-2,foo-sku-%2C3") + } + + +} diff --git a/mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m b/mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m deleted file mode 100644 index ec8e241..0000000 --- a/mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m +++ /dev/null @@ -1,160 +0,0 @@ -// -// mParticle_AppsFlyerTests.m -// mParticle_AppsFlyerTests -// -// Created by Sam Dozor on 1/30/18. -// Copyright © 2018 mParticle. All rights reserved. -// - -#import -#import "mParticle_AppsFlyer.h" -#import -#import - -NSString *const afAppleAppId = @"appleAppId"; -NSString *const afDevKey = @"devKey"; - -@interface MPKitAppsFlyer () - -- (void)setProviderKitInstance:(id)tracker; -- (nonnull MPKitExecStatus *)routeCommerceEvent:(nonnull MPCommerceEvent *)commerceEvent; - -@end - -@interface mParticle_AppsFlyerTests : XCTestCase - -@end - -@implementation mParticle_AppsFlyerTests - -- (void)setUp { - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; -} - -- (void)testComputeQuantityWithNoEvent { - XCTAssertEqual(1, [[MPKitAppsFlyer computeProductQuantity:nil] intValue]); -} -- (void)testComputeQuantityWithNoProducts { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - XCTAssertEqual(1, [[MPKitAppsFlyer computeProductQuantity:event] intValue]); -} - -- (void)testComputeQuantityWithProductWithNoQuantity { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"bar" quantity:@0 price:@50]; - [event addProduct:product]; - XCTAssertEqual(1, [[MPKitAppsFlyer computeProductQuantity:event] intValue]); -} - -- (void)testComputeQuantityWithProductWithMultipleQuantities { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"bar" quantity:@3 price:@50]; - MPProduct *product2 = [[MPProduct alloc] initWithName:@"foo2" sku:@"bar2" quantity:@2 price:@50]; - [event addProduct:product]; - [event addProduct:product2]; - XCTAssertEqual(5, [[MPKitAppsFlyer computeProductQuantity:event] intValue]); -} - -- (void)testGenerateSkuStringNoEvent { - MPCommerceEvent *event = nil; - XCTAssertNil([MPKitAppsFlyer generateProductIdList:event]); -} - -- (void)testGenerateSkuStringNoProducts { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - XCTAssertNil([MPKitAppsFlyer generateProductIdList:event]); -} - -- (void)testGenerateSkuStringSingleProduct { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"foo-sku" quantity:@3 price:@50]; - [event addProduct:product]; - XCTAssertEqualObjects(@"foo-sku",[MPKitAppsFlyer generateProductIdList:event]); -} - -- (void)testGenerateSkuStringMultipleProducts { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"foo-sku" quantity:@3 price:@50]; - MPProduct *product2 = [[MPProduct alloc] initWithName:@"foo2" sku:@"foo-sku-2" quantity:@2 price:@50]; - [event addProduct:product]; - [event addProduct:product2]; - XCTAssertEqualObjects(@"foo-sku,foo-sku-2",[MPKitAppsFlyer generateProductIdList:event]); -} - -- (void)testGenerateSkuStringEmbeddedCommas { - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"foo-sku" quantity:@3 price:@50]; - MPProduct *product2 = [[MPProduct alloc] initWithName:@"foo2" sku:@"foo-sku-2" quantity:@2 price:@50]; - MPProduct *product3 = [[MPProduct alloc] initWithName:@"foo3" sku:@"foo-sku-,3" quantity:@2 price:@50]; - [event addProduct:product]; - [event addProduct:product2]; - [event addProduct:product3]; - XCTAssertEqualObjects(@"foo-sku,foo-sku-2,foo-sku-%2C3",[MPKitAppsFlyer generateProductIdList:event]); -} - -- (void)testRouteCommerce{ - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - event.customAttributes = @{ - @"test": @"Malarkey" - }; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"foo-sku" quantity:@3 price:@50]; - MPProduct *product2 = [[MPProduct alloc] initWithName:@"foo2" sku:@"foo-sku-2" quantity:@2 price:@50]; - MPProduct *product3 = [[MPProduct alloc] initWithName:@"foo3" sku:@"foo-sku-,3" quantity:@2 price:@50]; - [event addProduct:product]; - [event addProduct:product2]; - [event addProduct:product3]; - - NSDictionary *resultValues = @{ - @"af_customer_user_id" : @"2", - @"af_quantity" : @7, - @"test": @"Malarkey", - @"af_content_id" : @"foo-sku,foo-sku-2,foo-sku-%2C3" - }; - - MPKitAppsFlyer *testClient = [[MPKitAppsFlyer alloc] init]; - id mockTracker = OCMPartialMock([AppsFlyerLib shared]); - [[mockTracker expect] logEvent:AFEventPurchase withValues:resultValues]; - - testClient.providerKitInstance = mockTracker; - - [testClient routeCommerceEvent: event]; - - [mockTracker verify]; - [mockTracker stopMocking]; -} - -- (void)testRouteCommerceNilCustomAttributes{ - MPCommerceEvent *event = [[MPCommerceEvent alloc] initWithAction:MPCommerceEventActionPurchase]; - event.customAttributes = nil; - MPProduct *product = [[MPProduct alloc] initWithName:@"foo" sku:@"foo-sku" quantity:@3 price:@50]; - MPProduct *product2 = [[MPProduct alloc] initWithName:@"foo2" sku:@"foo-sku-2" quantity:@2 price:@50]; - MPProduct *product3 = [[MPProduct alloc] initWithName:@"foo3" sku:@"foo-sku-,3" quantity:@2 price:@50]; - [event addProduct:product]; - [event addProduct:product2]; - [event addProduct:product3]; - - NSDictionary *resultValues = @{ - @"af_customer_user_id" : @"2", - @"af_quantity" : @7, - @"af_content_id" : @"foo-sku,foo-sku-2,foo-sku-%2C3" - }; - - MPKitAppsFlyer *testClient = [[MPKitAppsFlyer alloc] init]; - id mockTracker = OCMPartialMock([AppsFlyerLib shared]); - [[mockTracker expect] logEvent:AFEventPurchase withValues:resultValues]; - - testClient.providerKitInstance = mockTracker; - - [testClient routeCommerceEvent: event]; - - [mockTracker verify]; - [mockTracker stopMocking]; -} - -@end