From aa6bb0c7d86a7e58bc2b2524b74acd940fe76941 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Fri, 5 Sep 2025 13:52:42 -0400 Subject: [PATCH 01/20] feat: support consent mapping --- Package.resolved | 25 ++++++ Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 87 ++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 Package.resolved 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/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 1350e87..aa4cedc 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -21,6 +21,16 @@ 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 = @"defaultAdStorageConsentSDK"; +NSString *const kMPAFDefaultAdUserDataKey = @"defaultAdUserDataConsentSDK"; +NSString *const kMPAFDefaultAdPersonalizationKey = @"defaultAdPersonalizationConsentSDK"; + static AppsFlyerLib *appsFlyerTracker = nil; static id temporaryDelegate = nil; @@ -88,6 +98,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu appsFlyerTracker.deepLinkDelegate = self; _configuration = configuration; + [self updateConsent]; [appsFlyerTracker waitForATTUserAuthorizationWithTimeoutInterval:60]; [self start]; @@ -438,6 +449,82 @@ - (MPKitAPI *)kitApi { return _kitApi; } +- (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { + [self updateConsent]; + return [[MPKitExecStatus alloc] initWithSDKCode:@(MPKitInstanceAppsFlyer) + returnCode:MPKitReturnCodeSuccess]; +} + +- (void)updateConsent { + BOOL isUserSubjectToGDPR = NO; + NSNumber *dataUsage = nil; + NSNumber *personalization = nil; + NSNumber *storage = nil; + + // Defaults Consent States + NSString *defaultUserData = self->_configuration[kMPAFDefaultAdUserDataKey]; + if ([defaultUserData isEqualToString:@"Granted"]) { + dataUsage = @(YES); + } else if ([defaultUserData isEqualToString:@"Denied"]) { + dataUsage = @(NO); + } + + NSString *defaultPersonalization = self->_configuration[kMPAFDefaultAdPersonalizationKey]; + if ([defaultPersonalization isEqualToString:@"Granted"]) { + personalization = @(YES); + } else if ([defaultPersonalization isEqualToString:@"Denied"]) { + personalization = @(NO); + } + + NSString *defaultStorage = self->_configuration[kMPAFDefaultAdStorageKey]; + if ([defaultStorage isEqualToString:@"Granted"]) { + storage = @(YES); + } else if ([defaultStorage isEqualToString:@"Denied"]) { + storage = @(NO); + } + + // Update from mParticle Consent + MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; + NSDictionary *gdprConsents = currentUser.consentState.gdprConsentState; + + if (gdprConsents.count > 0) { + isUserSubjectToGDPR = YES; + } + + if (self->_configuration[kMPAFAdUserDataKey] && gdprConsents[self->_configuration[kMPAFAdUserDataKey]]) { + MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdUserDataKey]]; + dataUsage = consent.consented ? @(YES) : @(NO); + } + + if (self->_configuration[kMPAFAdPersonalizationKey] && gdprConsents[self->_configuration[kMPAFAdPersonalizationKey]]) { + MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdPersonalizationKey]]; + personalization = consent.consented ? @(YES) : @(NO); + } + + if (self->_configuration[kMPAFAdStorageKey] && gdprConsents[self->_configuration[kMPAFAdStorageKey]]) { + MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdStorageKey]]; + storage = consent.consented ? @(YES) : @(NO); + } + + AppsFlyerConsent *consentObj = nil; + if (isUserSubjectToGDPR) { + consentObj = [[AppsFlyerConsent alloc] + initWithIsUserSubjectToGDPR:@(YES) + hasConsentForDataUsage:dataUsage + hasConsentForAdsPersonalization:personalization + hasConsentForAdStorage:storage]; + } else { + consentObj = [[AppsFlyerConsent alloc] + initWithIsUserSubjectToGDPR:@(NO) + hasConsentForDataUsage:nil + hasConsentForAdsPersonalization:nil + hasConsentForAdStorage:nil]; + } + + // Update consent state with AppsFlyer + [appsFlyerTracker setConsentData:consentObj]; +} + #pragma helper methods - (FilteredMParticleUser *)currentUser { From 3152bb0582e11fa5d2e3990ad557e93c2367c6b5 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Fri, 5 Sep 2025 13:58:45 -0400 Subject: [PATCH 02/20] extract logic into seperate methods --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 71 ++++++++------------ 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index aa4cedc..3597cdf 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -457,55 +457,17 @@ - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { - (void)updateConsent { BOOL isUserSubjectToGDPR = NO; - NSNumber *dataUsage = nil; - NSNumber *personalization = nil; - NSNumber *storage = nil; - - // Defaults Consent States - NSString *defaultUserData = self->_configuration[kMPAFDefaultAdUserDataKey]; - if ([defaultUserData isEqualToString:@"Granted"]) { - dataUsage = @(YES); - } else if ([defaultUserData isEqualToString:@"Denied"]) { - dataUsage = @(NO); - } - - NSString *defaultPersonalization = self->_configuration[kMPAFDefaultAdPersonalizationKey]; - if ([defaultPersonalization isEqualToString:@"Granted"]) { - personalization = @(YES); - } else if ([defaultPersonalization isEqualToString:@"Denied"]) { - personalization = @(NO); - } - - NSString *defaultStorage = self->_configuration[kMPAFDefaultAdStorageKey]; - if ([defaultStorage isEqualToString:@"Granted"]) { - storage = @(YES); - } else if ([defaultStorage isEqualToString:@"Denied"]) { - storage = @(NO); - } - - // Update from mParticle Consent MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser]; NSDictionary *gdprConsents = currentUser.consentState.gdprConsentState; if (gdprConsents.count > 0) { isUserSubjectToGDPR = YES; } - - if (self->_configuration[kMPAFAdUserDataKey] && gdprConsents[self->_configuration[kMPAFAdUserDataKey]]) { - MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdUserDataKey]]; - dataUsage = consent.consented ? @(YES) : @(NO); - } - - if (self->_configuration[kMPAFAdPersonalizationKey] && gdprConsents[self->_configuration[kMPAFAdPersonalizationKey]]) { - MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdPersonalizationKey]]; - personalization = consent.consented ? @(YES) : @(NO); - } - - if (self->_configuration[kMPAFAdStorageKey] && gdprConsents[self->_configuration[kMPAFAdStorageKey]]) { - MPGDPRConsent *consent = gdprConsents[self->_configuration[kMPAFAdStorageKey]]; - storage = consent.consented ? @(YES) : @(NO); - } - + + NSNumber *dataUsage = [self mp_resolvedConsentForKey:kMPAFAdUserDataKey gdprConsents:gdprConsents]; + NSNumber *personalization = [self mp_resolvedConsentForKey:kMPAFAdPersonalizationKey gdprConsents:gdprConsents]; + NSNumber *storage = [self mp_resolvedConsentForKey:kMPAFAdStorageKey gdprConsents:gdprConsents]; + AppsFlyerConsent *consentObj = nil; if (isUserSubjectToGDPR) { consentObj = [[AppsFlyerConsent alloc] @@ -527,6 +489,29 @@ - (void)updateConsent { #pragma helper methods +- (NSNumber * _Nullable)mp_valueForConsentKey:(NSString *)key { + NSString *value = self->_configuration[key]; + if ([value isEqualToString:@"Granted"]) { + return @(YES); + } else if ([value isEqualToString:@"Denied"]) { + return @(NO); + } + return nil; +} + +- (NSNumber * _Nullable)mp_resolvedConsentForKey:(NSString *)key + gdprConsents:(NSDictionary *)gdprConsents { + // Defaults Consent States + NSNumber *value = [self mp_valueForConsentKey:key]; + + // Update from mParticle Consent + if (self->_configuration[key] && gdprConsents[self->_configuration[key]]) { + MPGDPRConsent *consent = gdprConsents[self->_configuration[key]]; + value = consent.consented ? @(YES) : @(NO); + } + return value; +} + - (FilteredMParticleUser *)currentUser { return [[self kitApi] getCurrentUserWithKit:self]; } From 10ce38e8c2d18344b9975a8443d358a12e31ab61 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Fri, 5 Sep 2025 13:59:34 -0400 Subject: [PATCH 03/20] adjust method naming --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 3597cdf..16397d0 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -464,9 +464,9 @@ - (void)updateConsent { isUserSubjectToGDPR = YES; } - NSNumber *dataUsage = [self mp_resolvedConsentForKey:kMPAFAdUserDataKey gdprConsents:gdprConsents]; - NSNumber *personalization = [self mp_resolvedConsentForKey:kMPAFAdPersonalizationKey gdprConsents:gdprConsents]; - NSNumber *storage = [self mp_resolvedConsentForKey:kMPAFAdStorageKey gdprConsents:gdprConsents]; + NSNumber *dataUsage = [self resolvedConsentForKey:kMPAFAdUserDataKey gdprConsents:gdprConsents]; + NSNumber *personalization = [self resolvedConsentForKey:kMPAFAdPersonalizationKey gdprConsents:gdprConsents]; + NSNumber *storage = [self resolvedConsentForKey:kMPAFAdStorageKey gdprConsents:gdprConsents]; AppsFlyerConsent *consentObj = nil; if (isUserSubjectToGDPR) { @@ -489,7 +489,7 @@ - (void)updateConsent { #pragma helper methods -- (NSNumber * _Nullable)mp_valueForConsentKey:(NSString *)key { +- (NSNumber * _Nullable)valueForConsentKey:(NSString *)key { NSString *value = self->_configuration[key]; if ([value isEqualToString:@"Granted"]) { return @(YES); @@ -499,10 +499,10 @@ - (NSNumber * _Nullable)mp_valueForConsentKey:(NSString *)key { return nil; } -- (NSNumber * _Nullable)mp_resolvedConsentForKey:(NSString *)key +- (NSNumber * _Nullable)resolvedConsentForKey:(NSString *)key gdprConsents:(NSDictionary *)gdprConsents { // Defaults Consent States - NSNumber *value = [self mp_valueForConsentKey:key]; + NSNumber *value = [self valueForConsentKey:key]; // Update from mParticle Consent if (self->_configuration[key] && gdprConsents[self->_configuration[key]]) { From 78a9e6a318d5a2bb4553976956c461cb51cfbabf Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Mon, 8 Sep 2025 15:28:44 -0400 Subject: [PATCH 04/20] Correct mismatching key logic and code improvement --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 73 ++++++++++---------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 16397d0..473865a 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -27,9 +27,9 @@ NSString *const kMPAFAdPersonalizationKey = @"ad_personalization"; // Default Consent Keys (from mParticle UI) -NSString *const kMPAFDefaultAdStorageKey = @"defaultAdStorageConsentSDK"; -NSString *const kMPAFDefaultAdUserDataKey = @"defaultAdUserDataConsentSDK"; -NSString *const kMPAFDefaultAdPersonalizationKey = @"defaultAdPersonalizationConsentSDK"; +NSString *const kMPAFDefaultAdStorageKey = @"defaultAdStorageConsent"; +NSString *const kMPAFDefaultAdUserDataKey = @"defaultAdUserDataConsent"; +NSString *const kMPAFDefaultAdPersonalizationKey = @"defaultAdPersonalizationConsent"; static AppsFlyerLib *appsFlyerTracker = nil; static id temporaryDelegate = nil; @@ -457,6 +457,12 @@ - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { - (void)updateConsent { 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; @@ -464,24 +470,24 @@ - (void)updateConsent { isUserSubjectToGDPR = YES; } - NSNumber *dataUsage = [self resolvedConsentForKey:kMPAFAdUserDataKey gdprConsents:gdprConsents]; - NSNumber *personalization = [self resolvedConsentForKey:kMPAFAdPersonalizationKey gdprConsents:gdprConsents]; - NSNumber *storage = [self resolvedConsentForKey:kMPAFAdStorageKey gdprConsents:gdprConsents]; - - AppsFlyerConsent *consentObj = nil; - if (isUserSubjectToGDPR) { - consentObj = [[AppsFlyerConsent alloc] - initWithIsUserSubjectToGDPR:@(YES) - hasConsentForDataUsage:dataUsage - hasConsentForAdsPersonalization:personalization - hasConsentForAdStorage:storage]; - } else { - consentObj = [[AppsFlyerConsent alloc] - initWithIsUserSubjectToGDPR:@(NO) - hasConsentForDataUsage:nil - hasConsentForAdsPersonalization:nil - hasConsentForAdStorage:nil]; - } + NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey + defaultKey:kMPAFDefaultAdUserDataKey + gdprConsents:gdprConsents]; + + NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey + defaultKey:kMPAFDefaultAdPersonalizationKey + gdprConsents:gdprConsents]; + + NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey + defaultKey:kMPAFDefaultAdStorageKey + gdprConsents:gdprConsents]; + + + 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]; @@ -489,8 +495,17 @@ - (void)updateConsent { #pragma helper methods -- (NSNumber * _Nullable)valueForConsentKey:(NSString *)key { - NSString *value = self->_configuration[key]; +- (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey + defaultKey:(NSString *)defaultKey + gdprConsents:(NSDictionary *)gdprConsents { + // Prefer mParticle Consent if available + MPGDPRConsent *consent = gdprConsents[mappingKey]; + if (consent) { + return consent.consented ? @(YES) : @(NO); + } + + // Fallback to configuration defaults + NSString *value = self->_configuration[defaultKey]; if ([value isEqualToString:@"Granted"]) { return @(YES); } else if ([value isEqualToString:@"Denied"]) { @@ -499,18 +514,6 @@ - (NSNumber * _Nullable)valueForConsentKey:(NSString *)key { return nil; } -- (NSNumber * _Nullable)resolvedConsentForKey:(NSString *)key - gdprConsents:(NSDictionary *)gdprConsents { - // Defaults Consent States - NSNumber *value = [self valueForConsentKey:key]; - - // Update from mParticle Consent - if (self->_configuration[key] && gdprConsents[self->_configuration[key]]) { - MPGDPRConsent *consent = gdprConsents[self->_configuration[key]]; - value = consent.consented ? @(YES) : @(NO); - } - return value; -} - (FilteredMParticleUser *)currentUser { return [[self kitApi] getCurrentUserWithKit:self]; From 9dd76a86dc300464dad234749ef7b9c514683d81 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 9 Sep 2025 15:30:21 -0400 Subject: [PATCH 05/20] Add JSON parsing --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 28 +++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 473865a..fe38ece 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -498,10 +498,30 @@ - (void)updateConsent { - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey defaultKey:(NSString *)defaultKey gdprConsents:(NSDictionary *)gdprConsents { - // Prefer mParticle Consent if available - MPGDPRConsent *consent = gdprConsents[mappingKey]; - if (consent) { - return consent.consented ? @(YES) : @(NO); + // Parse the consentMapping JSON string + NSString *mappingJson = self->_configuration[@"consentMapping"]; + if ([mappingJson isKindOfClass:[NSString class]]) { + NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; + if (jsonData) { + NSError *error = nil; + NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if (!error && [mappings isKindOfClass:[NSArray class]]) { + for (NSDictionary *entry in mappings) { + if ([entry isKindOfClass:[NSDictionary class]]) { + NSString *value = entry[@"value"]; + NSString *purpose = [entry[@"map"] lowercaseString]; + + // Prefer mParticle Consent if available + if ([value isEqualToString:mappingKey] && purpose) { + MPGDPRConsent *consent = gdprConsents[purpose]; + if (consent) { + return consent.consented ? @(YES) : @(NO); + } + } + } + } + } + } } // Fallback to configuration defaults From 30bb46c993788ff169d083d14ae06c75d2b35465 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 9 Sep 2025 16:05:07 -0400 Subject: [PATCH 06/20] clean up json parsing code block --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 26 +++++++------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index fe38ece..86a0cd9 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -502,23 +502,15 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey NSString *mappingJson = self->_configuration[@"consentMapping"]; if ([mappingJson isKindOfClass:[NSString class]]) { NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; - if (jsonData) { - NSError *error = nil; - NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (!error && [mappings isKindOfClass:[NSArray class]]) { - for (NSDictionary *entry in mappings) { - if ([entry isKindOfClass:[NSDictionary class]]) { - NSString *value = entry[@"value"]; - NSString *purpose = [entry[@"map"] lowercaseString]; - - // Prefer mParticle Consent if available - if ([value isEqualToString:mappingKey] && purpose) { - MPGDPRConsent *consent = gdprConsents[purpose]; - if (consent) { - return consent.consented ? @(YES) : @(NO); - } - } - } + NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; + + for (NSDictionary *entry in mappings) { + if ([entry[@"value"] isEqualToString:mappingKey]) { + // Prefer mParticle Consent if available + NSString *purpose = [entry[@"map"] lowercaseString]; + MPGDPRConsent *consent = gdprConsents[purpose]; + if (consent) { + return consent.consented ? @(YES) : @(NO); } } } From 693049d732e572376c605bba809ee4cc8e208cb2 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 9 Sep 2025 16:47:30 -0400 Subject: [PATCH 07/20] cached mappings removing repetitive parsing --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 41 +++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 86a0cd9..9c0a56d 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -35,6 +35,7 @@ static id temporaryDelegate = nil; @interface MPKitAppsFlyer() +@property (nonatomic, strong) NSDictionary *consentMappingDict; @end @implementation MPKitAppsFlyer @@ -98,6 +99,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu appsFlyerTracker.deepLinkDelegate = self; _configuration = configuration; + [self parseConsentMappings]; [self updateConsent]; [appsFlyerTracker waitForATTUserAuthorizationWithTimeoutInterval:60]; [self start]; @@ -498,21 +500,13 @@ - (void)updateConsent { - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey defaultKey:(NSString *)defaultKey gdprConsents:(NSDictionary *)gdprConsents { - // Parse the consentMapping JSON string - NSString *mappingJson = self->_configuration[@"consentMapping"]; - if ([mappingJson isKindOfClass:[NSString class]]) { - NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; - NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; - for (NSDictionary *entry in mappings) { - if ([entry[@"value"] isEqualToString:mappingKey]) { - // Prefer mParticle Consent if available - NSString *purpose = [entry[@"map"] lowercaseString]; - MPGDPRConsent *consent = gdprConsents[purpose]; - if (consent) { - return consent.consented ? @(YES) : @(NO); - } - } + // Prefer mParticle Consent if available + NSString *purpose = self.consentMappingDict[mappingKey]; + if (purpose) { + MPGDPRConsent *consent = gdprConsents[purpose]; + if (consent) { + return consent.consented ? @(YES) : @(NO); } } @@ -526,6 +520,25 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey return nil; } +- (void)parseConsentMappings { + NSString *mappingJson = _configuration[@"consentMapping"]; + if ([mappingJson isKindOfClass:[NSString class]]) { + NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; + NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; + + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (NSDictionary *entry in mappings) { + NSString *value = entry[@"value"]; + NSString *purpose = [entry[@"map"] lowercaseString]; + if (value && purpose) { + dict[value] = purpose; + } + } + self.consentMappingDict = [dict copy]; + } +} + + - (FilteredMParticleUser *)currentUser { return [[self kitApi] getCurrentUserWithKit:self]; From f5161cb8dd3e58acac5eb9ec6890aefd42889d68 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 9 Sep 2025 17:02:31 -0400 Subject: [PATCH 08/20] remove ternary --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 9c0a56d..7d4d9d1 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -506,7 +506,7 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey if (purpose) { MPGDPRConsent *consent = gdprConsents[purpose]; if (consent) { - return consent.consented ? @(YES) : @(NO); + return @(consent.consented); } } From 3e29d5c7ef551b69842770c2c692bd2d50280825 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Wed, 10 Sep 2025 15:33:58 -0400 Subject: [PATCH 09/20] Extract separate methods related to mapping Co-Authored-By: denischilik <225006499+denischilik@users.noreply.github.com> --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 50 ++++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 7d4d9d1..1e0448b 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -35,7 +35,6 @@ static id temporaryDelegate = nil; @interface MPKitAppsFlyer() -@property (nonatomic, strong) NSDictionary *consentMappingDict; @end @implementation MPKitAppsFlyer @@ -99,7 +98,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu appsFlyerTracker.deepLinkDelegate = self; _configuration = configuration; - [self parseConsentMappings]; + [self updateConsent]; [appsFlyerTracker waitForATTUserAuthorizationWithTimeoutInterval:60]; [self start]; @@ -458,6 +457,12 @@ - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state { } - (void)updateConsent { + NSArray *mappings = [self mappingForKey: @"consentMapping"]; + NSDictionary *mappingsConfig; + if (mappings != nil) { + mappingsConfig = [self convertToKeyValuePairs: mappings]; + } + BOOL isUserSubjectToGDPR = NO; NSString *gdprValue = _configuration[@"gdprApplies"]; @@ -474,15 +479,18 @@ - (void)updateConsent { NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey defaultKey:kMPAFDefaultAdUserDataKey - gdprConsents:gdprConsents]; + gdprConsents:gdprConsents + mappingsConfig:mappingsConfig]; NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey defaultKey:kMPAFDefaultAdPersonalizationKey - gdprConsents:gdprConsents]; + gdprConsents:gdprConsents + mappingsConfig:mappingsConfig]; NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey defaultKey:kMPAFDefaultAdStorageKey - gdprConsents:gdprConsents]; + gdprConsents:gdprConsents + mappingsConfig:mappingsConfig]; AppsFlyerConsent *consentObj = [[AppsFlyerConsent alloc] @@ -499,10 +507,11 @@ - (void)updateConsent { - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey defaultKey:(NSString *)defaultKey - gdprConsents:(NSDictionary *)gdprConsents { + gdprConsents:(NSDictionary *)gdprConsents + mappingsConfig:(NSDictionary *) mapping { // Prefer mParticle Consent if available - NSString *purpose = self.consentMappingDict[mappingKey]; + NSString *purpose = mapping[mappingKey]; if (purpose) { MPGDPRConsent *consent = gdprConsents[purpose]; if (consent) { @@ -520,25 +529,26 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey return nil; } -- (void)parseConsentMappings { +- (NSArray*)mappingForKey:(NSString*)key { NSString *mappingJson = _configuration[@"consentMapping"]; if ([mappingJson isKindOfClass:[NSString class]]) { NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; - NSArray *mappings = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; - - NSMutableDictionary *dict = [NSMutableDictionary dictionary]; - for (NSDictionary *entry in mappings) { - NSString *value = entry[@"value"]; - NSString *purpose = [entry[@"map"] lowercaseString]; - if (value && purpose) { - dict[value] = purpose; - } - } - self.consentMappingDict = [dict copy]; + return [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; } + return nil; } - +- (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]; From c74982f1977943bd99a4590206c0e285f762b134 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Thu, 11 Sep 2025 09:59:33 -0400 Subject: [PATCH 10/20] Keep naming consistent and add error handling --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 1e0448b..525f7df 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -480,17 +480,17 @@ - (void)updateConsent { NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey defaultKey:kMPAFDefaultAdUserDataKey gdprConsents:gdprConsents - mappingsConfig:mappingsConfig]; + mapping:mappingsConfig]; NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey defaultKey:kMPAFDefaultAdPersonalizationKey gdprConsents:gdprConsents - mappingsConfig:mappingsConfig]; + mapping:mappingsConfig]; NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey defaultKey:kMPAFDefaultAdStorageKey gdprConsents:gdprConsents - mappingsConfig:mappingsConfig]; + mapping:mappingsConfig]; AppsFlyerConsent *consentObj = [[AppsFlyerConsent alloc] @@ -508,7 +508,7 @@ - (void)updateConsent { - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey defaultKey:(NSString *)defaultKey gdprConsents:(NSDictionary *)gdprConsents - mappingsConfig:(NSDictionary *) mapping { + mapping:(NSDictionary *) mapping { // Prefer mParticle Consent if available NSString *purpose = mapping[mappingKey]; @@ -531,11 +531,20 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey - (NSArray*)mappingForKey:(NSString*)key { NSString *mappingJson = _configuration[@"consentMapping"]; - if ([mappingJson isKindOfClass:[NSString class]]) { - NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding]; - return [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; + if (![mappingJson isKindOfClass:[NSString class]]) { + return nil; } - 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 { From 0302716b845ed18a6a2d0f3ab63fc2fedbc1ed16 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Thu, 11 Sep 2025 09:59:53 -0400 Subject: [PATCH 11/20] spacing --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 525f7df..cf33c78 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -508,7 +508,7 @@ - (void)updateConsent { - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey defaultKey:(NSString *)defaultKey gdprConsents:(NSDictionary *)gdprConsents - mapping:(NSDictionary *) mapping { + mapping:(NSDictionary *) mapping { // Prefer mParticle Consent if available NSString *purpose = mapping[mappingKey]; From 0017769b6df907e8e96e8d1361acf649961f9b01 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Thu, 11 Sep 2025 10:00:26 -0400 Subject: [PATCH 12/20] more spacing --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index cf33c78..a9a0df1 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -480,17 +480,17 @@ - (void)updateConsent { NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey defaultKey:kMPAFDefaultAdUserDataKey gdprConsents:gdprConsents - mapping:mappingsConfig]; + mapping:mappingsConfig]; NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey defaultKey:kMPAFDefaultAdPersonalizationKey gdprConsents:gdprConsents - mapping:mappingsConfig]; + mapping:mappingsConfig]; NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey defaultKey:kMPAFDefaultAdStorageKey gdprConsents:gdprConsents - mapping:mappingsConfig]; + mapping:mappingsConfig]; AppsFlyerConsent *consentObj = [[AppsFlyerConsent alloc] From 957ae0d144f901158e57832a69fe73269889d39a Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 10:29:56 -0400 Subject: [PATCH 13/20] fix usage of key parameter --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index a9a0df1..940203d 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -530,7 +530,7 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey } - (NSArray*)mappingForKey:(NSString*)key { - NSString *mappingJson = _configuration[@"consentMapping"]; + NSString *mappingJson = _configuration[key]; if (![mappingJson isKindOfClass:[NSString class]]) { return nil; } From 23c0c2c3e95196fdf988355d202a939d1a276681 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 10:30:31 -0400 Subject: [PATCH 14/20] Create file structure for tests in swift and objc --- Package.swift | 12 ++ .../{ => Objc}/Info.plist | 0 .../{ => Objc}/mParticle_AppsFlyerTests.m | 118 +++++++++--------- .../Swift/mParticle_AppsFlyerSwiftTests.swift | 13 ++ 4 files changed, 84 insertions(+), 59 deletions(-) rename mParticle_AppsFlyerTests/{ => Objc}/Info.plist (100%) rename mParticle_AppsFlyerTests/{ => Objc}/mParticle_AppsFlyerTests.m (62%) create mode 100644 mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift diff --git a/Package.swift b/Package.swift index e215bf2..4513a0f 100644 --- a/Package.swift +++ b/Package.swift @@ -40,6 +40,18 @@ let package = Package( ], path: "SPM/mParticle-AppsFlyer-NoLocation", resources: [.process("PrivacyInfo.xcprivacy")] + ), + + .testTarget( + name: "mParticle-AppsFlyer-Swift-Tests", + dependencies: ["mParticle-AppsFlyer"], + path: "mParticle_AppsFlyerTests/Swift" + ), + + .testTarget( + name: "mParticle-AppsFlyer-Objc-Tests", + dependencies: ["mParticle-AppsFlyer"], + path: "mParticle_AppsFlyerTests/Objc", ) ] ) diff --git a/mParticle_AppsFlyerTests/Info.plist b/mParticle_AppsFlyerTests/Objc/Info.plist similarity index 100% rename from mParticle_AppsFlyerTests/Info.plist rename to mParticle_AppsFlyerTests/Objc/Info.plist diff --git a/mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m b/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m similarity index 62% rename from mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m rename to mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m index ec8e241..ed75580 100644 --- a/mParticle_AppsFlyerTests/mParticle_AppsFlyerTests.m +++ b/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m @@ -9,7 +9,7 @@ #import #import "mParticle_AppsFlyer.h" #import -#import +//#import NSString *const afAppleAppId = @"appleAppId"; NSString *const afDevKey = @"devKey"; @@ -98,63 +98,63 @@ - (void)testGenerateSkuStringEmbeddedCommas { 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]; -} +//- (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 diff --git a/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift b/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift new file mode 100644 index 0000000..38eb9d9 --- /dev/null +++ b/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift @@ -0,0 +1,13 @@ +// +// mParticle_AppsFlyerSwiftTests.swift +// mParticle-AppsFlyer +// +// Created by Nick Dimitrakas on 9/16/25. +// + +import XCTest +import mParticle_AppsFlyer + +final class mParticle_AppsFlyerSwiftTests { + +} From b608b21a0ce786c7145b53c4d1c0aa7d38174723 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 10:31:46 -0400 Subject: [PATCH 15/20] add NSString extension --- Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m index 940203d..8032f70 100644 --- a/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m +++ b/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m @@ -34,6 +34,19 @@ 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 @@ -521,12 +534,7 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey // Fallback to configuration defaults NSString *value = self->_configuration[defaultKey]; - if ([value isEqualToString:@"Granted"]) { - return @(YES); - } else if ([value isEqualToString:@"Denied"]) { - return @(NO); - } - return nil; + return [value isGranted]; } - (NSArray*)mappingForKey:(NSString*)key { From ca3dc575b5194d1680bcba5815743864bbc90278 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 10:34:34 -0400 Subject: [PATCH 16/20] add swift tests --- .../include/MPKitAppsFlyer.h | 9 ++ .../Swift/mParticle_AppsFlyerSwiftTests.swift | 108 +++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h index 4c6e2a0..665036f 100644 --- a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h +++ b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h @@ -24,6 +24,15 @@ extern NSString * _Nonnull const MPKitAppsFlyerErrorDomain; + (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; @end extern NSString * _Nonnull const MPKitAppsFlyerAttributionResultKey __deprecated_msg("Use MPKitAppsFlyerConversionResultKey instead."); diff --git a/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift b/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift index 38eb9d9..6151a14 100644 --- a/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift +++ b/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift @@ -6,8 +6,112 @@ // import XCTest -import mParticle_AppsFlyer +@testable import mParticle_AppsFlyer -final class mParticle_AppsFlyerSwiftTests { +final class mParticle_AppsFlyerSwiftTests: XCTestCase { + var kit: MPKitAppsFlyer! + // MARK: - Lifecycle + + override func setUpWithError() throws { + try super.setUpWithError() + kit = MPKitAppsFlyer() + kit.configuration = [:] + } + + override func tearDownWithError() throws { + kit = nil + try super.tearDownWithError() + } + + // 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) + } } From 2dc05d24407e70c545ce1ed9873fc79d3ff9fa88 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 11:29:22 -0400 Subject: [PATCH 17/20] fixed XCMock dependency in SPM --- Package.resolved | 9 +++++++++ Package.swift | 8 +++++++- mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m | 8 ++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2b510da..1061d31 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "revision": "e629ee514f760bd14f195ac34370afba4c816baa", "version": "8.38.0" } + }, + { + "package": "OCMock", + "repositoryURL": "https://github.com/erikdoe/ocmock.git", + "state": { + "branch": null, + "revision": "afd2c6924e8a36cb872bc475248b978f743c6050", + "version": null + } } ] }, diff --git a/Package.swift b/Package.swift index 4513a0f..cf7124e 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,9 @@ let package = Package( .package(name: "AppsFlyerLib", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", .upToNextMajor(from: "6.14.3")), + .package(name: "OCMock", + url: "https://github.com/erikdoe/ocmock.git", + .revision("afd2c6924e8a36cb872bc475248b978f743c6050")) ], targets: [ .target( @@ -50,7 +53,10 @@ let package = Package( .testTarget( name: "mParticle-AppsFlyer-Objc-Tests", - dependencies: ["mParticle-AppsFlyer"], + dependencies: [ + "mParticle-AppsFlyer", + .product(name: "OCMock", package: "OCMock") + ], path: "mParticle_AppsFlyerTests/Objc", ) ] diff --git a/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m b/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m index ed75580..342a9c5 100644 --- a/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m +++ b/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m @@ -6,10 +6,10 @@ // Copyright © 2018 mParticle. All rights reserved. // -#import -#import "mParticle_AppsFlyer.h" -#import -//#import +@import mParticle_Apple_SDK; +@import mParticle_AppsFlyer; +@import XCTest; +@import OCMock; NSString *const afAppleAppId = @"appleAppId"; NSString *const afDevKey = @"devKey"; From 63cc22638a505c5efa90bc52568f36648f8f9ef1 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 16:26:57 -0400 Subject: [PATCH 18/20] remove unused plist --- mParticle_AppsFlyerTests/Objc/Info.plist | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 mParticle_AppsFlyerTests/Objc/Info.plist diff --git a/mParticle_AppsFlyerTests/Objc/Info.plist b/mParticle_AppsFlyerTests/Objc/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/mParticle_AppsFlyerTests/Objc/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 - - From 0fb479da24f10b6872a07e9b4bae5709ecdeecc3 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 16:41:27 -0400 Subject: [PATCH 19/20] move objc tests to swift - Created mocks - exposed methods and properties to swift Co-Authored-By: denischilik <225006499+denischilik@users.noreply.github.com> --- Package.resolved | 9 - Package.swift | 14 +- .../include/MPKitAppsFlyer.h | 3 + .../AppsFlyerLibMock.swift | 20 ++ .../Objc/mParticle_AppsFlyerTests.m | 160 ------------- .../Swift/mParticle_AppsFlyerSwiftTests.swift | 117 --------- .../mParticle_AppsFlyerSwiftTests.swift | 223 ++++++++++++++++++ 7 files changed, 247 insertions(+), 299 deletions(-) create mode 100644 mParticle_AppsFlyerTests/AppsFlyerLibMock.swift delete mode 100644 mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m delete mode 100644 mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift create mode 100644 mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift diff --git a/Package.resolved b/Package.resolved index 1061d31..2b510da 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,15 +18,6 @@ "revision": "e629ee514f760bd14f195ac34370afba4c816baa", "version": "8.38.0" } - }, - { - "package": "OCMock", - "repositoryURL": "https://github.com/erikdoe/ocmock.git", - "state": { - "branch": null, - "revision": "afd2c6924e8a36cb872bc475248b978f743c6050", - "version": null - } } ] }, diff --git a/Package.swift b/Package.swift index cf7124e..0f0fb55 100644 --- a/Package.swift +++ b/Package.swift @@ -22,9 +22,6 @@ let package = Package( .package(name: "AppsFlyerLib", url: "https://github.com/AppsFlyerSDK/AppsFlyerFramework-Static", .upToNextMajor(from: "6.14.3")), - .package(name: "OCMock", - url: "https://github.com/erikdoe/ocmock.git", - .revision("afd2c6924e8a36cb872bc475248b978f743c6050")) ], targets: [ .target( @@ -48,16 +45,7 @@ let package = Package( .testTarget( name: "mParticle-AppsFlyer-Swift-Tests", dependencies: ["mParticle-AppsFlyer"], - path: "mParticle_AppsFlyerTests/Swift" + path: "mParticle_AppsFlyerTests" ), - - .testTarget( - name: "mParticle-AppsFlyer-Objc-Tests", - dependencies: [ - "mParticle-AppsFlyer", - .product(name: "OCMock", package: "OCMock") - ], - path: "mParticle_AppsFlyerTests/Objc", - ) ] ) diff --git a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h index 665036f..4fc51e0 100644 --- a/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h +++ b/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h @@ -20,6 +20,7 @@ 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; @@ -33,6 +34,8 @@ extern NSString * _Nonnull const MPKitAppsFlyerErrorDomain; - (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/Objc/mParticle_AppsFlyerTests.m b/mParticle_AppsFlyerTests/Objc/mParticle_AppsFlyerTests.m deleted file mode 100644 index 342a9c5..0000000 --- a/mParticle_AppsFlyerTests/Objc/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 mParticle_Apple_SDK; -@import mParticle_AppsFlyer; -@import XCTest; -@import OCMock; - -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 diff --git a/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift b/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift deleted file mode 100644 index 6151a14..0000000 --- a/mParticle_AppsFlyerTests/Swift/mParticle_AppsFlyerSwiftTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// mParticle_AppsFlyerSwiftTests.swift -// mParticle-AppsFlyer -// -// Created by Nick Dimitrakas on 9/16/25. -// - -import XCTest -@testable import mParticle_AppsFlyer - -final class mParticle_AppsFlyerSwiftTests: XCTestCase { - var kit: MPKitAppsFlyer! - - // MARK: - Lifecycle - - override func setUpWithError() throws { - try super.setUpWithError() - kit = MPKitAppsFlyer() - kit.configuration = [:] - } - - override func tearDownWithError() throws { - kit = nil - try super.tearDownWithError() - } - - // 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) - } -} diff --git a/mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift b/mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift new file mode 100644 index 0000000..516d123 --- /dev/null +++ b/mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift @@ -0,0 +1,223 @@ +// +// mParticle_AppsFlyerSwiftTests.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") + } + + +} From 9aa589e3d05b482ebcc41ede1ca483f4984a942a Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Tue, 16 Sep 2025 16:49:22 -0400 Subject: [PATCH 20/20] adjust file name --- ...icle_AppsFlyerSwiftTests.swift => MPKitAppsFlyerTests.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename mParticle_AppsFlyerTests/{mParticle_AppsFlyerSwiftTests.swift => MPKitAppsFlyerTests.swift} (99%) diff --git a/mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift b/mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift similarity index 99% rename from mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift rename to mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift index 516d123..ad1a459 100644 --- a/mParticle_AppsFlyerTests/mParticle_AppsFlyerSwiftTests.swift +++ b/mParticle_AppsFlyerTests/MPKitAppsFlyerTests.swift @@ -1,5 +1,5 @@ // -// mParticle_AppsFlyerSwiftTests.swift +// MPKitAppsFlyerTests.swift // mParticle-AppsFlyer // // Created by Nick Dimitrakas on 9/16/25.