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
25 changes: 25 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ let package = Package(
],
path: "SPM/mParticle-AppsFlyer-NoLocation",
resources: [.process("PrivacyInfo.xcprivacy")]
)
),

.testTarget(
name: "mParticle-AppsFlyer-Swift-Tests",
dependencies: ["mParticle-AppsFlyer"],
path: "mParticle_AppsFlyerTests"
),
]
)
127 changes: 127 additions & 0 deletions Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,32 @@
NSString *const afAppsFlyerIdIntegrationKey = @"appsflyer_id_integration_setting";
NSString *const kMPKAFCustomerUserId = @"af_customer_user_id";

// Consent Mapping Keys
NSString *const kMPAFAdStorageKey = @"ad_storage";
NSString *const kMPAFAdUserDataKey = @"ad_user_data";
NSString *const kMPAFAdPersonalizationKey = @"ad_personalization";

// Default Consent Keys (from mParticle UI)
NSString *const kMPAFDefaultAdStorageKey = @"defaultAdStorageConsent";
NSString *const kMPAFDefaultAdUserDataKey = @"defaultAdUserDataConsent";
NSString *const kMPAFDefaultAdPersonalizationKey = @"defaultAdPersonalizationConsent";

static AppsFlyerLib *appsFlyerTracker = nil;
static id<AppsFlyerLibDelegate> 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() <AppsFlyerLibDelegate, AppsFlyerDeepLinkDelegate>
@end

Expand Down Expand Up @@ -88,6 +111,8 @@ - (MPKitExecStatus *)didFinishLaunchingWithConfiguration:(NSDictionary *)configu
appsFlyerTracker.deepLinkDelegate = self;

_configuration = configuration;

[self updateConsent];
[appsFlyerTracker waitForATTUserAuthorizationWithTimeoutInterval:60];
[self start];

Expand Down Expand Up @@ -438,8 +463,110 @@ - (MPKitAPI *)kitApi {
return _kitApi;
}

- (MPKitExecStatus *)setConsentState:(nullable MPConsentState *)state {
[self updateConsent];
return [[MPKitExecStatus alloc] initWithSDKCode:@(MPKitInstanceAppsFlyer)
returnCode:MPKitReturnCodeSuccess];
}

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

BOOL isUserSubjectToGDPR = NO;

NSString *gdprValue = _configuration[@"gdprApplies"];
if ([gdprValue isKindOfClass:[NSString class]]) {
isUserSubjectToGDPR = [gdprValue boolValue];
}

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

if (gdprConsents.count > 0) {
isUserSubjectToGDPR = YES;
}

NSNumber *dataUsage = [self resolvedConsentForMappingKey:kMPAFAdUserDataKey
defaultKey:kMPAFDefaultAdUserDataKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSNumber *personalization = [self resolvedConsentForMappingKey:kMPAFAdPersonalizationKey
defaultKey:kMPAFDefaultAdPersonalizationKey
gdprConsents:gdprConsents
mapping:mappingsConfig];

NSNumber *storage = [self resolvedConsentForMappingKey:kMPAFAdStorageKey
defaultKey:kMPAFDefaultAdStorageKey
gdprConsents:gdprConsents
mapping:mappingsConfig];


AppsFlyerConsent *consentObj = [[AppsFlyerConsent alloc]
initWithIsUserSubjectToGDPR:@(isUserSubjectToGDPR)
hasConsentForDataUsage:isUserSubjectToGDPR ? dataUsage : nil
hasConsentForAdsPersonalization:isUserSubjectToGDPR ? personalization : nil
hasConsentForAdStorage:isUserSubjectToGDPR ? storage : nil];

// Update consent state with AppsFlyer
[appsFlyerTracker setConsentData:consentObj];
}

#pragma helper methods

- (NSNumber * _Nullable)resolvedConsentForMappingKey:(NSString *)mappingKey
defaultKey:(NSString *)defaultKey
gdprConsents:(NSDictionary<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];
return [value isGranted];
}

- (NSArray<NSDictionary *>*)mappingForKey:(NSString*)key {
NSString *mappingJson = _configuration[key];
if (![mappingJson isKindOfClass:[NSString class]]) {
return nil;
}

NSData *jsonData = [mappingJson dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
NSArray *result = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];

if (error) {
NSLog(@"Failed to parse consent mapping JSON: %@", error.localizedDescription);
return nil;
}

return result;
}

- (NSDictionary*)convertToKeyValuePairs: (NSArray<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;
}

- (FilteredMParticleUser *)currentUser {
return [[self kitApi] getCurrentUserWithKit:self];
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,22 @@ extern NSString * _Nonnull const MPKitAppsFlyerErrorDomain;
@property (nonatomic, strong, nonnull) NSDictionary *configuration;
@property (nonatomic, unsafe_unretained, readonly) BOOL started;
@property (nonatomic, strong, nullable) MPKitAPI *kitApi;
@property (nonatomic, strong, nullable) id providerKitInstance;

+ (void)setDelegate:(id _Nonnull)delegate;
+ (NSNumber * _Nonnull)computeProductQuantity:(nullable MPCommerceEvent *)event;
+ (NSString * _Nullable)generateProductIdList:(nullable MPCommerceEvent *)event;

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

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

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

- (nonnull MPKitExecStatus *)routeCommerceEvent:(nonnull MPCommerceEvent *)commerceEvent;
@end

extern NSString * _Nonnull const MPKitAppsFlyerAttributionResultKey __deprecated_msg("Use MPKitAppsFlyerConversionResultKey instead.");
20 changes: 20 additions & 0 deletions mParticle_AppsFlyerTests/AppsFlyerLibMock.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
22 changes: 0 additions & 22 deletions mParticle_AppsFlyerTests/Info.plist

This file was deleted.

Loading