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

.testTarget(
name: "mParticle-Google-Analytics-Firebase-GA4-Swift-Tests",
dependencies: ["mParticle-Google-Analytics-Firebase-GA4"],
path: "mParticle-Google-Analytics-Firebase-GA4Tests/Swift"
),

.testTarget(
name: "mParticle-Google-Analytics-Firebase-GA4-Objc-Tests",
dependencies: ["mParticle-Google-Analytics-Firebase-GA4"],
path: "mParticle-Google-Analytics-Firebase-GA4Tests/Objc",
resources: [.process("GoogleService-Info.plist")]
)
]
)
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;

- (nullable NSNumber *)resolvedConsentForMappingKey:(NSString * _Nonnull)mappingKey
defaultKey:(NSString * _Nonnull)defaultKey
gdprConsents:(NSDictionary<NSString *, MPGDPRConsent *> * _Nonnull)gdprConsents
mapping:(NSDictionary<NSString *, NSString *> * _Nullable)mapping;

- (nullable NSArray<NSDictionary *>*)mappingForKey:(NSString* _Nonnull)key;

- (nonnull NSDictionary*)convertToKeyValuePairs: (NSArray<NSDictionary *> * _Nonnull)mappings;

@end

static NSString * _Nonnull const kMPFIRGA4ExternalUserIdentityType = @"externalUserIdentityType";
Expand Down
38 changes: 25 additions & 13 deletions mParticle-Google-Analytics-Firebase-GA4/MPKitFirebaseGA4Analytics.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@

static NSString* (^customNameStandardization)(NSString* name) = nil;

@implementation NSString(PRIVATE)

- (NSNumber*)isGranted {
if ([self isEqualToString:@"Granted"]) {
return @(YES);
} else if ([self isEqualToString:@"Denied"]) {
return @(NO);
}
return nil;
}

@end

@interface MPKitFirebaseGA4Analytics () <MPKitProtocol> {
BOOL forwardRequestsServerSide;
}
Expand Down Expand Up @@ -89,6 +102,11 @@ - (MPKitExecStatus *)execStatus:(MPKitReturnCode)returnCode {

#pragma mark MPKitInstanceProtocol methods
- (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configuration {
MParticleUser *currentUser = [[[MParticle sharedInstance] identity] currentUser];
return [self didFinishLaunchingWithConfiguration:configuration withConsentState:currentUser.consentState];
}

- (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configuration withConsentState: (MPConsentState *)consentState {
_configuration = configuration;

if ([FIRApp defaultApp] == nil) {
Expand All @@ -100,8 +118,7 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu
}

[self updateInstanceIDIntegration];

[self updateConsent];
[self updateConsent: consentState];

_started = YES;

Expand Down Expand Up @@ -382,21 +399,21 @@ - (void)logUserAttributes:(NSDictionary<NSString *, id> *)userAttributes {
}

- (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state {
[self updateConsent];
[self updateConsent: state];

return [self execStatus:MPKitReturnCodeSuccess];
}

- (void)updateConsent {
- (void)updateConsent:(MPConsentState *)consentState {
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 *> *gdprConsents = currentUser.consentState.gdprConsentState;

NSDictionary<NSString *, MPGDPRConsent *> *gdprConsents = consentState.gdprConsentState;

NSNumber *adStorage = [self resolvedConsentForMappingKey:kMPFIRGA4AdStorageKey
defaultKey:kMPFIRGA4DefaultAdStorageKey
Expand Down Expand Up @@ -684,16 +701,11 @@ - (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<NSDictionary *>*)mappingForKey:(NSString*)key {
NSString *mappingJson = _configuration[@"consentMappingSDK"];
NSString *mappingJson = _configuration[key];
if (![mappingJson isKindOfClass:[NSString class]]) {
return nil;
}
Expand Down
22 changes: 0 additions & 22 deletions mParticle-Google-Analytics-Firebase-GA4Tests/Info.plist

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,22 @@ @interface mParticle_Firebase_AnalyticsTests : XCTestCase
@implementation mParticle_Firebase_AnalyticsTests

- (void)setUp {
NSString *bundlePath = [[NSBundle bundleForClass:[self class]] resourcePath];
NSString *filePath = [bundlePath stringByAppendingPathComponent:@"GoogleService-Info.plist"];
[super setUp];

// 1. Start with the test bundle
NSBundle *testBundle = [NSBundle bundleForClass:[self class]];

// 2. Locate the auto-generated resource bundle for this test target
NSURL *resourceBundleURL = [testBundle URLForResource:@"mParticle-Google-Analytics-Firebase-GA4_mParticle-Google-Analytics-Firebase-GA4-Objc-Tests"
withExtension:@"bundle"];
NSBundle *resourceBundle = [NSBundle bundleWithURL:resourceBundleURL];
NSAssert(resourceBundle != nil, @"Resource bundle not found");

// 3. Fetch the plist inside that resource bundle
NSString *filePath = [resourceBundle pathForResource:@"GoogleService-Info" ofType:@"plist"];
NSAssert(filePath != nil, @"GoogleService-Info.plist not found in resource bundle");

// 4. Configure Firebase
FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:filePath];
[FIRApp configureWithOptions:options];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// 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!

// MARK: - Lifecycle

override func setUpWithError() throws {
try super.setUpWithError()
kit = MPKitFirebaseGA4Analytics()
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)
}
}