Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,11 @@ let package = Package(
exclude: ["Info.plist", "dummy.swift"],
resources: [.process("PrivacyInfo.xcprivacy")],
publicHeadersPath: "."),

.testTarget(
name: "mParticle-Google-Analytics-Firebase-GA4SwiftTests",
dependencies: ["mParticle-Google-Analytics-Firebase-GA4"],
path: "mParticle-Google-Analytics-Firebase-GA4Tests/Swift"
)
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
+ (void)setCustomNameStandardization:(NSString * _Nonnull (^_Nullable)(NSString * _Nonnull name))standardization;
+ (NSString * _Nonnull (^_Nullable)(NSString * _Nonnull name))customNameStandardization;

- (nonnull NSDictionary<NSString *, NSString *> *)consentDictionaryForCurrentUser;
- (NSNumber * _Nullable)resolvedConsentForPurpose:(NSString * _Nullable)purpose
gdprConsents:(NSDictionary<NSString *, MPGDPRConsent *> * _Nonnull)gdprConsents;
- (NSNumber * _Nullable)resolvedConsentFromDefault:(NSString * _Nonnull)defaultKey;
- (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString * _Nonnull)mappingKey
defaultKey:(NSString * _Nonnull)defaultKey
gdprConsents:(NSDictionary<NSString *, MPGDPRConsent *> * _Nonnull)gdprConsents
mapping:(NSDictionary<NSString *, NSString*> * _Nonnull)mapping;

@end

static NSString * _Nonnull const kMPFIRGA4ExternalUserIdentityType = @"externalUserIdentityType";
Expand Down
51 changes: 31 additions & 20 deletions mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m
Original file line number Diff line number Diff line change
Expand Up @@ -387,36 +387,35 @@ - (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state {
return [self execStatus:MPKitReturnCodeSuccess];
}

- (void)updateConsent {
NSArray<NSDictionary *> *mappings = [self mappingForKey: @"consentMappingSDK"];
- (NSDictionary<NSString *, NSString *> *)consentDictionaryForCurrentUser {
NSArray<NSDictionary *> *mappings = [self mappingForKey:@"consentMappingSDK"];
NSDictionary<NSString *, NSString *> *mappingsConfig;
if (mappings != nil) {
mappingsConfig = [self convertToKeyValuePairs: mappings];
mappingsConfig = [self convertToKeyValuePairs:mappings];
}



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

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

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

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

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

NSMutableDictionary *uploadDict = [NSMutableDictionary dictionary];

Expand All @@ -432,12 +431,16 @@ - (void)updateConsent {
if (adPersonalization != nil) {
uploadDict[FIRConsentTypeAdPersonalization] = adPersonalization.boolValue ? FIRConsentStatusGranted : FIRConsentStatusDenied;
}


// Update consent state with FIRAnalytics
[FIRAnalytics setConsent:uploadDict];

return uploadDict;
}

- (void)updateConsent {
NSDictionary *consentDict = [self consentDictionaryForCurrentUser];
[FIRAnalytics setConsent:consentDict];
}


- (NSString *)getEventNameForCommerceEvent:(MPCommerceEvent *)commerceEvent parameters:(NSDictionary<NSString *, id> *)parameters {
switch (commerceEvent.action) {
case MPCommerceEventActionAddToCart:
Expand Down Expand Up @@ -673,16 +676,24 @@ - (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey
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);
}
NSNumber *consent = [self resolvedConsentForPurpose:purpose gdprConsents:gdprConsents];
if (consent) {
return consent;
}
return [self resolvedConsentFromDefault:defaultKey];
}

- (NSNumber * _Nullable)resolvedConsentForPurpose:(NSString *)purpose
gdprConsents:(NSDictionary<NSString *, MPGDPRConsent *> *)gdprConsents {
MPGDPRConsent *consent = gdprConsents[purpose];
if (consent) {
return @(consent.consented);
}
return nil;
}

// Fallback to configuration defaults
- (NSNumber * _Nullable)resolvedConsentFromDefault:(NSString *)defaultKey {
NSString *value = self->_configuration[defaultKey];
if ([value isEqualToString:@"Granted"]) {
return @(YES);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// MPKitFirebaseGA4SwiftTests.swift
// mParticle-Google-Analytics-Firebase-GA4
//
// Created by Nick Dimitrakas on 9/12/25.
//

import XCTest
@testable import mParticle_Google_Analytics_Firebase_GA4

final class MPKitFirebaseGA4AnalyticsTests: XCTestCase {
var kit: MPKitFirebaseGA4Analytics!

override func setUp() {
super.setUp()
kit = MPKitFirebaseGA4Analytics()
kit.configuration = [:]
}

override func tearDown() {
kit = nil
super.tearDown()
}

// MARK: - resolvedConsentForPurpose

func testResolvedConsentForPurpose_whenConsentGranted_returnsYES() {
let consent = MPGDPRConsent()
consent.consented = true
let gdprConsents = ["analytics": consent]

let result = kit.resolvedConsent(forPurpose: "analytics", gdprConsents: gdprConsents)

XCTAssertEqual(result, true)
}

func testResolvedConsentForPurpose_whenConsentDenied_returnsNO() {
let consent = MPGDPRConsent()
consent.consented = false
let gdprConsents = ["ads": consent]

let result = kit.resolvedConsent(forPurpose: "ads", gdprConsents: gdprConsents)

XCTAssertEqual(result, false)
}

func testResolvedConsentForPurpose_whenConsentMissing_returnsNil() {
let gdprConsents: [String: MPGDPRConsent] = [:]

let result = kit.resolvedConsent(forPurpose: "ads", gdprConsents: gdprConsents)

XCTAssertNil(result)
}

// MARK: - resolvedConsentFromDefault

func testResolvedConsentFromDefault_whenGranted_returnsYES() {
kit.configuration = ["purpose_default": "Granted"]

let result = kit.resolvedConsent(fromDefault: "purpose_default")

XCTAssertEqual(result, true)
}

func testResolvedConsentFromDefault_whenDenied_returnsNO() {
kit.configuration = ["purpose_default": "Denied"]

let result = kit.resolvedConsent(fromDefault: "purpose_default")

XCTAssertEqual(result, false)
}

func testResolvedConsentFromDefault_whenMissing_returnsNil() {
kit.configuration = [:]

let result = kit.resolvedConsent(fromDefault: "purpose_default")

XCTAssertNil(result)
}

// MARK: - resolvedConsentForMappingKey

func testResolvedConsentForMappingKey_prefersGDPRConsentOverDefault() {
let consent = MPGDPRConsent()
consent.consented = true
let gdprConsents = ["analytics": consent]

let mapping = ["tracking": "analytics"]
kit.configuration = ["tracking_default": "Denied"]

let result = kit.resolvedConsent(forMappingKey: "tracking",
defaultKey: "tracking_default",
gdprConsents: gdprConsents,
mapping: mapping)

// Should return GDPR (true) instead of config (Denied/false)
XCTAssertEqual(result, true)
}

func testResolvedConsentForMappingKey_fallsBackToDefaultWhenNoGDPR() {
let gdprConsents: [String: MPGDPRConsent] = [:]
let mapping = ["tracking": "ads"]
kit.configuration = ["tracking_default": "Granted"]

let result = kit.resolvedConsent(forMappingKey: "tracking",
defaultKey: "tracking_default",
gdprConsents: gdprConsents,
mapping: mapping)

XCTAssertEqual(result, true)
}

func testResolvedConsentForMappingKey_returnsNilWhenNeitherFound() {
let gdprConsents: [String: MPGDPRConsent] = [:]
let mapping = ["tracking": "ads"]
kit.configuration = [:]

let result = kit.resolvedConsent(forMappingKey: "tracking",
defaultKey: "tracking_default",
gdprConsents: gdprConsents,
mapping: mapping)

XCTAssertNil(result)
}
}