Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 96 additions & 62 deletions mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,13 @@ @implementation MPKitFirebaseGA4Analytics
static NSString *const instanceIdIntegrationKey = @"app_instance_id";
static NSString *const invalidFirebaseKey = @"invalid_ga4_key";

// Consent Mapping Keys
static NSString *const kMPFIRGA4AdStorageKey = @"ad_storage";
static NSString *const kMPFIRGA4AdUserDataKey = @"ad_user_data";
static NSString *const kMPFIRGA4AdPersonalizationKey = @"ad_personalization";
static NSString *const kMPFIRGA4AnalyticsStorageKey = @"analytics_storage";

// Default Consent Keys (from mParticle UI)
static NSString *const kMPFIRGA4DefaultAdStorageKey = @"defaultAdStorageConsentSDK";
static NSString *const kMPFIRGA4DefaultAdUserDataKey = @"defaultAdUserDataConsentSDK";
static NSString *const kMPFIRGA4DefaultAdPersonalizationKey = @"defaultAdPersonalizationConsentSDK";
Expand Down Expand Up @@ -385,76 +388,51 @@ - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state {
}

- (void)updateConsent {
FIRConsentStatus adStorageStatus;
FIRConsentStatus adUserDataStatus;
FIRConsentStatus analyticsStorageStatus;
FIRConsentStatus adPersonalizationStatus;

// Default Consent States
if (self.configuration[kMPFIRGA4DefaultAdStorageKey]) {
if ([self.configuration[kMPFIRGA4DefaultAdStorageKey] isEqual: @"Granted"]) {
adStorageStatus = FIRConsentStatusGranted;
} else if ([self.configuration[kMPFIRGA4DefaultAdStorageKey] isEqual: @"Denied"]) {
adStorageStatus = FIRConsentStatusDenied;
}
}
if (self.configuration[kMPFIRGA4DefaultAdUserDataKey]) {
if ([self.configuration[kMPFIRGA4DefaultAdUserDataKey] isEqual: @"Granted"]) {
adUserDataStatus = FIRConsentStatusGranted;
} else if ([self.configuration[kMPFIRGA4DefaultAdUserDataKey] isEqual: @"Denied"]) {
adUserDataStatus = FIRConsentStatusDenied;
}
}
if (self.configuration[kMPFIRGA4DefaultAnalyticsStorageKey]) {
if ([self.configuration[kMPFIRGA4DefaultAnalyticsStorageKey] isEqual: @"Granted"]) {
analyticsStorageStatus = FIRConsentStatusGranted;
} else if ([self.configuration[kMPFIRGA4DefaultAnalyticsStorageKey] isEqual: @"Denied"]) {
analyticsStorageStatus = FIRConsentStatusDenied;
}
}
if (self.configuration[kMPFIRGA4DefaultAdPersonalizationKey]) {
if ([self.configuration[kMPFIRGA4DefaultAdPersonalizationKey] isEqual: @"Granted"]) {
adPersonalizationStatus = FIRConsentStatusGranted;
} else if ([self.configuration[kMPFIRGA4DefaultAdPersonalizationKey] isEqual: @"Denied"]) {
adPersonalizationStatus = FIRConsentStatusDenied;
}
NSArray<NSDictionary *> *mappings = [self mappingForKey: @"consentMappingSDK"];
NSDictionary<NSString *, NSString *> *mappingsConfig;
if (mappings != nil) {
mappingsConfig = [self convertToKeyValuePairs: mappings];
}

MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser];
NSDictionary<NSString *, MPGDPRConsent *> *userConsentMap = currentUser.consentState.gdprConsentState;

// Update from mParticle consent
if (self.configuration[kMPFIRGA4AdStorageKey] && userConsentMap[self.configuration[kMPFIRGA4AdStorageKey]]) {
MPGDPRConsent *consent = userConsentMap[self.configuration[kMPFIRGA4AdStorageKey]];
adStorageStatus = consent.consented ? FIRConsentStatusGranted : FIRConsentStatusDenied;
MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser];
NSDictionary<NSString *, MPGDPRConsent *> *gdprConsents = currentUser.consentState.gdprConsentState;

NSNumber *adStorage = [self resolvedConsentForMappingKey:kMPFIRGA4AdStorageKey
defaultKey:kMPFIRGA4DefaultAdStorageKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSNumber *adUserData = [self resolvedConsentForMappingKey:kMPFIRGA4AdUserDataKey
defaultKey:kMPFIRGA4DefaultAdUserDataKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSNumber *analyticsStorage = [self resolvedConsentForMappingKey:kMPFIRGA4AnalyticsStorageKey
defaultKey:kMPFIRGA4DefaultAnalyticsStorageKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSNumber *adPersonalization = [self resolvedConsentForMappingKey:kMPFIRGA4AdPersonalizationKey
defaultKey:kMPFIRGA4DefaultAdPersonalizationKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSMutableDictionary *uploadDict = [NSMutableDictionary dictionary];

if (adStorage != nil) {
uploadDict[FIRConsentTypeAdStorage] = adStorage.boolValue ? FIRConsentStatusGranted : FIRConsentStatusDenied;
}
if (self.configuration[kMPFIRGA4AdUserDataKey] && userConsentMap[self.configuration[kMPFIRGA4AdUserDataKey]]) {
MPGDPRConsent *consent = userConsentMap[self.configuration[kMPFIRGA4AdUserDataKey]];
adUserDataStatus = consent.consented ? FIRConsentStatusGranted : FIRConsentStatusDenied;
if (adUserData != nil) {
uploadDict[FIRConsentTypeAdUserData] = adUserData.boolValue ? FIRConsentStatusGranted : FIRConsentStatusDenied;
}
if (self.configuration[kMPFIRGA4AnalyticsStorageKey] && userConsentMap[self.configuration[kMPFIRGA4AnalyticsStorageKey]]) {
MPGDPRConsent *consent = userConsentMap[self.configuration[kMPFIRGA4AnalyticsStorageKey]];
analyticsStorageStatus = consent.consented ? FIRConsentStatusGranted : FIRConsentStatusDenied;
if (analyticsStorage != nil) {
uploadDict[FIRConsentTypeAnalyticsStorage] = analyticsStorage.boolValue ? FIRConsentStatusGranted : FIRConsentStatusDenied;
}
if (self.configuration[kMPFIRGA4AdPersonalizationKey] && userConsentMap[self.configuration[kMPFIRGA4AdPersonalizationKey]]) {
MPGDPRConsent *consent = userConsentMap[self.configuration[kMPFIRGA4AdPersonalizationKey]];
adPersonalizationStatus = consent.consented ? FIRConsentStatusGranted : FIRConsentStatusDenied;
if (adPersonalization != nil) {
uploadDict[FIRConsentTypeAdPersonalization] = adPersonalization.boolValue ? FIRConsentStatusGranted : FIRConsentStatusDenied;
}

// Construct a dictionary of consents
NSMutableDictionary *uploadDict = [[NSMutableDictionary alloc] init];
if (adStorageStatus) {
uploadDict[FIRConsentTypeAdStorage] = adStorageStatus;
}
if (adUserDataStatus) {
uploadDict[FIRConsentTypeAdUserData] = adUserDataStatus;
}
if (analyticsStorageStatus) {
uploadDict[FIRConsentTypeAnalyticsStorage] = analyticsStorageStatus;
}
if (adPersonalizationStatus) {
uploadDict[FIRConsentTypeAdPersonalization] = adPersonalizationStatus;
}

// Update consent state with FIRAnalytics
[FIRAnalytics setConsent:uploadDict];
Expand Down Expand Up @@ -688,4 +666,60 @@ - (void)limitDictionary:(NSMutableDictionary *)dictionary maxCount:(int)maxCount
}
}

#pragma mark - Helpers

- (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey
defaultKey:(NSString *)defaultKey
gdprConsents:(NSDictionary<NSString *, MPGDPRConsent *> *)gdprConsents
mapping:(NSDictionary<NSString *, NSString*> *) 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];
if ([value isEqualToString:@"Granted"]) {
return @(YES);
} else if ([value isEqualToString:@"Denied"]) {
return @(NO);
}
return nil;
}

- (NSArray<NSDictionary *>*)mappingForKey:(NSString*)key {
NSString *mappingJson = _configuration[@"consentMappingSDK"];
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<NSDictionary *>*) 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;
}

@end