Skip to content

Commit bab57c7

Browse files
fix: Mapping updates and general improvements
- Fixed attribute mapping configuration lookup (now correctly accesses nested 'as' config) - Added MPID to attributes passed to Rokt SDK - Improved code organization with constants for configuration keys - Updated documentation comments to modern Objective-C style - Fixed typo: "seperate" -> "separate" - Updated CI workflow to use Xcode 16.4 with iOS 18.5 simulator - Added tests for attribute mapping and MPID handling Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8a9fd83 commit bab57c7

4 files changed

Lines changed: 209 additions & 78 deletions

File tree

.github/workflows/pull-request.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
- name: Set up Xcode
3838
uses: maxim-lobanov/setup-xcode@v1
3939
with:
40+
<<<<<<< HEAD
4041
xcode-version: ${{ env.XCODE_VERSION }}
4142

4243
- name: Setup specified simulator
@@ -52,6 +53,15 @@ jobs:
5253

5354
- name: Run unit tests
5455
run: xcodebuild -project mParticle-Rokt.xcodeproj -scheme mParticle_RoktTests -destination 'id=${{ steps.simulator.outputs.udid }}' test
56+
=======
57+
xcode-version: 16.4
58+
- name: Run Tests
59+
run: >
60+
set -o pipefail &&
61+
xcodebuild test -project mParticle-Rokt.xcodeproj -scheme mParticle_RoktTests -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.5'
62+
| xcbeautify --renderer github-actions
63+
shell: bash
64+
>>>>>>> 2a99c1a (fix: Mapping updates and general improvements)
5565

5666
pr-notify:
5767
if: >

mParticle-Rokt/MPKitRokt.m

Lines changed: 82 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
#import "MPKitRokt.h"
22
#import <Rokt_Widget/Rokt_Widget-Swift.h>
33

4+
// Constants for kit configuration keys
5+
static NSString * const kMPKitConfigurationIdKey = @"id";
6+
static NSString * const kMPRemoteConfigKitConfigurationKey = @"as";
7+
static NSString * const kMPAttributeMappingSourceKey = @"map";
8+
static NSString * const kMPAttributeMappingDestinationKey = @"value";
9+
10+
// Rokt attribute keys
11+
static NSString * const kMPRoktAttributeKeySandbox = @"sandbox";
12+
13+
// Rokt kit constants
414
NSString * const kMPRemoteConfigKitHashesKey = @"hs";
515
NSString * const kMPRemoteConfigUserAttributeFilter = @"ua";
616
NSString * const MPKitRoktErrorDomain = @"com.mparticle.kits.rokt";
@@ -9,6 +19,10 @@
919
NSString * const kMPHashedEmailUserIdentityType = @"hashedEmailUserIdentityType";
1020
NSString * const kMPRoktEmbeddedViewClassName = @"MPRoktEmbeddedView";
1121
NSString * const kMPEventNameSelectPlacements = @"selectPlacements";
22+
NSString * const kMPRoktIdentityTypeEmailSha256 = @"emailsha256";
23+
NSString * const kMPRoktIdentityTypeMpid = @"mpid";
24+
25+
// Rokt kit identifier
1226
NSInteger const kMPRoktKitCode = 181;
1327

1428
static __weak MPKitRokt *roktKit = nil;
@@ -86,16 +100,16 @@ - (void)start {
86100
});
87101
}
88102

89-
/// \param identifier The name that should be displayed in the widget
90-
///
91-
/// \param attributes A string dictionary containing the parameters that should be displayed in the widget
92-
///
93-
/// \param embeddedViews A dictionary of RoktEmbeddedViews with their names
94-
///
95-
/// \param callbacks Object that contains all possible callbacks for selectPlacements
96-
///
97-
/// \param filteredUser The current user when this placement was requested. Filtered for the kit as per settings in the mParticle UI
98-
///
103+
/// Displays a Rokt ad placement with full configuration options.
104+
/// This method handles user identity synchronization, attribute mapping, and forwards the request to the Rokt SDK.
105+
/// Device identifiers (IDFA/IDFV) are automatically added if available.
106+
/// @param identifier The Rokt placement identifier configured in the Rokt dashboard
107+
/// @param attributes Dictionary of user attributes (email, firstName, etc.). Attributes will be mapped according to dashboard configuration.
108+
/// @param embeddedViews Optional dictionary mapping placement identifiers to embedded view containers for inline placements
109+
/// @param mpRoktConfig Optional Rokt configuration object (e.g., for dark mode or custom styling)
110+
/// @param callbacks Object that contains all possible callbacks for selectPlacements
111+
/// @param filteredUser The current user when this placement was requested. Filtered for the kit as per settings in the mParticle UI
112+
/// @return MPKitExecStatus indicating success or failure of the operation
99113
- (MPKitExecStatus *)executeWithIdentifier:(NSString * _Nullable)identifier
100114
attributes:(NSDictionary<NSString *, NSString *> * _Nonnull)attributes
101115
embeddedViews:(NSDictionary<NSString *, MPRoktEmbeddedView *> * _Nullable)embeddedViews
@@ -188,42 +202,41 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
188202
return safePlacements;
189203
}
190204

205+
/// Ensures the "sandbox" attribute is present in the attributes dictionary.
206+
/// If not already set by the caller, the sandbox value is automatically determined based on the current mParticle environment
207+
/// (MPEnvironmentDevelopment → "true", production → "false"). This tells Rokt whether to show test or production ads.
208+
/// @param attributes The input attributes dictionary to validate
209+
/// @return A dictionary with the sandbox attribute guaranteed to be present
191210
+ (NSDictionary<NSString *, NSString *> *)confirmSandboxAttribute:(NSDictionary<NSString *, NSString *> * _Nullable)attributes {
192211
NSMutableDictionary<NSString *, NSString *> *finalAttributes = attributes.mutableCopy;
193-
NSString *sandboxKey = @"sandbox";
194212

195213
// Determine the value of the sandbox attribute based off the current environment
196214
NSString *sandboxValue = ([[MParticle sharedInstance] environment] == MPEnvironmentDevelopment) ? @"true" : @"false";
197215

198216
if (finalAttributes != nil) {
199-
// Only set sandbox if it`s not set by the client
200-
if (![finalAttributes.allKeys containsObject:sandboxKey]) {
201-
finalAttributes[sandboxKey] = sandboxValue;
217+
// Only set sandbox if it's not set by the client
218+
if (![finalAttributes.allKeys containsObject:kMPRoktAttributeKeySandbox]) {
219+
finalAttributes[kMPRoktAttributeKeySandbox] = sandboxValue;
202220
}
203221
} else {
204-
finalAttributes = [[NSMutableDictionary alloc] initWithDictionary:@{sandboxKey: sandboxValue}];
222+
finalAttributes = [[NSMutableDictionary alloc] initWithDictionary:@{kMPRoktAttributeKeySandbox: sandboxValue}];
205223
}
206224

207-
[MPKitRokt MPLog:[NSString stringWithFormat:@"Sandbox value: %@", finalAttributes[sandboxKey]]];
225+
[MPKitRokt MPLog:[NSString stringWithFormat:@"Sandbox value: %@", finalAttributes[kMPRoktAttributeKeySandbox]]];
208226
return finalAttributes;
209227
}
210228

211229
+ (NSDictionary<NSString *, NSString *> * _Nonnull)prepareAttributes:(NSDictionary<NSString *, NSString *> * _Nonnull)attributes filteredUser:(FilteredMParticleUser * _Nullable)filteredUser performMapping:(BOOL)performMapping {
212230
if (filteredUser == nil && roktKit != nil) {
213231
filteredUser = [[[MPKitAPI alloc] init] getCurrentUserWithKit:roktKit];
214232
}
215-
NSDictionary<NSString *, NSString *> *mpAttributes = [self transformValuesToString:filteredUser.userAttributes];
233+
NSDictionary<NSString *, NSString *> *mappedAttributes = attributes;
216234
if (performMapping) {
217-
mpAttributes = [self mapAttributes:attributes filteredUser:filteredUser];
235+
mappedAttributes = [MPKitRokt mapAttributes:attributes filteredUser:filteredUser];
218236
}
219237

220238
NSMutableDictionary<NSString *, NSString *> *finalAtt = [[NSMutableDictionary alloc] init];
221-
[finalAtt addEntriesFromDictionary:mpAttributes];
222-
223-
// Add MPID to the attributes being passed to the Rokt SDK
224-
if (filteredUser.userId.stringValue != nil) {
225-
[finalAtt addEntriesFromDictionary:@{@"mpid": filteredUser.userId.stringValue}];
226-
}
239+
[finalAtt addEntriesFromDictionary:mappedAttributes];
227240

228241
// Add all known user identities to the attributes being passed to the Rokt SDK
229242
[self addIdentityAttributes:finalAtt filteredUser:filteredUser];
@@ -232,13 +245,12 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
232245
[self handleHashedEmail:finalAtt];
233246

234247
// The core SDK does not set sandbox on the user, but we must pass it to Rokt if provided
235-
NSString *sandboxKey = @"sandbox";
236-
if (attributes[sandboxKey] != nil) {
237-
[finalAtt addEntriesFromDictionary:@{sandboxKey: attributes[sandboxKey]}];
248+
if (attributes[kMPRoktAttributeKeySandbox] != nil) {
249+
[finalAtt addEntriesFromDictionary:@{kMPRoktAttributeKeySandbox: attributes[kMPRoktAttributeKeySandbox]}];
238250
}
239251

240252
[MPKitRokt MPLog:[NSString stringWithFormat:@"Attributes updated with mapped user Attributes and Identities: %@", finalAtt]];
241-
return [self confirmSandboxAttribute:finalAtt];
253+
return [MPKitRokt confirmSandboxAttribute:finalAtt];
242254
}
243255

244256
+ (NSDictionary<NSString *, NSString *> *)transformValuesToString:(NSDictionary<NSString *, id> * _Nullable)originalDictionary {
@@ -277,19 +289,18 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
277289
return transformedDictionary;
278290
}
279291

292+
/// Retrieves the attribute mapping configuration for the Rokt Kit from the mParticle dashboard settings.
293+
/// The mapping defines how attribute keys should be renamed before being sent to Rokt (e.g., "userEmail" → "email").
294+
/// @param attributes The input attributes dictionary
295+
/// @param filteredUser The current mParticle user
296+
/// @return A dictionary with mapped attributes according to dashboard configuration
280297
+ (NSDictionary<NSString *, NSString *> *)mapAttributes:(NSDictionary<NSString *, NSString *> * _Nullable)attributes filteredUser:(FilteredMParticleUser * _Nonnull)filteredUser {
281298
NSArray<NSDictionary<NSString *, NSString *> *> *attributeMap = nil;
282299

283300
// Get the kit configuration
284-
NSArray<NSDictionary *> *kitConfigs = [MParticle sharedInstance].kitContainer_PRIVATE.originalConfig.copy;
285-
NSDictionary *roktKitConfig;
286-
for (NSDictionary *kitConfig in kitConfigs) {
287-
if (kitConfig[@"id"] != nil && [kitConfig[@"id"] integerValue] == kMPRoktKitCode) {
288-
roktKitConfig = kitConfig;
289-
}
290-
}
301+
NSDictionary *roktKitConfig = [MPKitRokt getKitConfig];
291302

292-
// Return nil if no Rokt Kit configuration found
303+
// Return original attributes if no Rokt Kit configuration found
293304
if (!roktKitConfig) {
294305
return attributes;
295306
}
@@ -299,8 +310,9 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
299310
NSData *dataAttributeMap;
300311
// Rokt Kit is available though there may not be an attribute map
301312
attributeMap = @[];
302-
if (roktKitConfig[kMPPlacementAttributesMapping] != [NSNull null]) {
303-
strAttributeMap = [roktKitConfig[kMPPlacementAttributesMapping] stringByRemovingPercentEncoding];
313+
id configJSONString = roktKitConfig[kMPRemoteConfigKitConfigurationKey][kMPPlacementAttributesMapping];
314+
if (configJSONString != nil && configJSONString != [NSNull null]) {
315+
strAttributeMap = [configJSONString stringByRemovingPercentEncoding];
304316
dataAttributeMap = [strAttributeMap dataUsingEncoding:NSUTF8StringEncoding];
305317
}
306318

@@ -311,28 +323,29 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
311323
@try {
312324
attributeMap = [NSJSONSerialization JSONObjectWithData:dataAttributeMap options:kNilOptions error:&error];
313325
} @catch (NSException *exception) {
326+
[MPKitRokt MPLog:[NSString stringWithFormat:@"Exception parsing placement attribute map: %@", exception]];
314327
}
315328

316329
if (attributeMap && !error) {
317-
NSLog(@"%@", attributeMap);
330+
[MPKitRokt MPLog:[NSString stringWithFormat:@"Successfully parsed placement attribute map with %lu entries", (unsigned long)attributeMap.count]];
318331
} else {
319-
NSLog(@"%@", error);
332+
[MPKitRokt MPLog:[NSString stringWithFormat:@"Failed to parse placement attribute map: %@", error]];
320333
}
321334
}
322335

323336
if (attributeMap) {
324337
NSMutableDictionary *mappedAttributes = attributes.mutableCopy;
325338
for (NSDictionary<NSString *, NSString *> *map in attributeMap) {
326-
NSString *mapFrom = map[@"map"];
327-
NSString *mapTo = map[@"value"];
339+
NSString *mapFrom = map[kMPAttributeMappingSourceKey];
340+
NSString *mapTo = map[kMPAttributeMappingDestinationKey];
328341
if (mappedAttributes[mapFrom]) {
329342
NSString * value = mappedAttributes[mapFrom];
330343
[mappedAttributes removeObjectForKey:mapFrom];
331344
mappedAttributes[mapTo] = value;
332345
}
333346
}
334347
for (NSString *key in mappedAttributes) {
335-
if (![key isEqual:@"sandbox"]) {
348+
if (![key isEqual:kMPRoktAttributeKeySandbox]) {
336349
[[MParticle sharedInstance].identity.currentUser setUserAttribute:key value:mappedAttributes[key]];
337350
}
338351
}
@@ -344,7 +357,7 @@ - (RoktFrameworkType)mapMPWrapperSdkToRoktFrameworkType:(MPWrapperSdk)wrapperSdk
344357
}
345358
}
346359

347-
return [self transformValuesToString:mappedAttributes];
360+
return [MPKitRokt transformValuesToString:mappedAttributes];
348361
} else {
349362
return attributes;
350363
}
@@ -362,14 +375,17 @@ + (void)addIdentityAttributes:(NSMutableDictionary<NSString *, NSString *> * _Nu
362375
} else {
363376
attributes = identityAttributes;
364377
}
378+
379+
// Add MPID to the attributes being passed to the Rokt SDK
380+
attributes[kMPRoktIdentityTypeMpid] = filteredUser.userId.stringValue;
365381
}
366382

367383
+ (void)handleHashedEmail:(NSMutableDictionary<NSString *, NSString *> * _Nullable)attributes {
368384
NSString *emailKey = [MPKitRokt stringForIdentityType:MPIdentityEmail];
369-
NSString *hashedEmailValue = attributes[@"emailsha256"];
385+
NSString *hashedEmailValue = attributes[kMPRoktIdentityTypeEmailSha256];
370386

371387
// Remove email if hashed value set
372-
if (hashedEmailValue != nil) {
388+
if (emailKey != kMPRoktIdentityTypeEmailSha256 && hashedEmailValue != nil) {
373389
[attributes removeObjectForKey:emailKey];
374390
}
375391
}
@@ -397,7 +413,7 @@ + (NSString *)stringForIdentityType:(MPIdentity)identityType {
397413
NSNumber *hashedEmailIdentity = [MPKitRokt getRoktHashedEmailUserIdentityType];
398414

399415
if (hashedEmailIdentity && hashedEmailIdentity.unsignedIntValue == identityType) {
400-
return @"emailsha256";
416+
return kMPRoktIdentityTypeEmailSha256;
401417
}
402418

403419
NSDictionary<NSNumber *, NSString *> *identityStrings = @{@(MPIdentityCustomerId): @"customerid",
@@ -462,29 +478,39 @@ + (NSNumber *)identityTypeForString:(NSString *)identityString {
462478
return identityNumbers[identityString];
463479
}
464480

465-
+ (NSDictionary *)getKitConfig {
466-
// Get the kit configuration
481+
#pragma mark - Private Helper Methods
482+
483+
/// Retrieves the Rokt Kit configuration from the kit container.
484+
/// @return The Rokt Kit configuration dictionary, or nil if Rokt Kit is not configured.
485+
+ (NSDictionary * _Nullable)getKitConfig {
467486
NSArray<NSDictionary *> *kitConfigs = [MParticle sharedInstance].kitContainer_PRIVATE.originalConfig.copy;
468-
NSDictionary *roktKitConfig;
469487
for (NSDictionary *kitConfig in kitConfigs) {
470-
if (kitConfig[@"id"] != nil && [kitConfig[@"id"] integerValue] == kMPRoktKitCode) {
471-
roktKitConfig = kitConfig;
488+
if ([kitConfig[kMPKitConfigurationIdKey] integerValue] == kMPRoktKitCode) {
489+
return kitConfig;
472490
}
473491
}
474-
475-
return roktKitConfig;
492+
return nil;
476493
}
477494

478-
+ (NSNumber *)getRoktHashedEmailUserIdentityType {
495+
/// Retrieves the configured identity type to use for hashed email from the Rokt Kit configuration.
496+
/// The hashed email identity type is determined by dashboard settings and may vary (e.g., CustomerId, Other, etc.).
497+
/// @return The NSNumber representing the MPIdentity type for hashed email, or nil if not configured.
498+
+ (NSNumber * _Nullable)getRoktHashedEmailUserIdentityType {
479499
NSDictionary *roktKitConfig = [MPKitRokt getKitConfig];
480500

481501
// Get the string representing which identity to use and convert it to the key (NSNumber)
482-
NSString *hashedIdentityTypeString = roktKitConfig[kMPHashedEmailUserIdentityType];
502+
NSString *hashedIdentityTypeString = roktKitConfig[kMPRemoteConfigKitConfigurationKey][kMPHashedEmailUserIdentityType];
483503
NSNumber *hashedIdentityTypeNumber = [MPKitRokt identityTypeForString:hashedIdentityTypeString.lowercaseString];
484504

485505
return hashedIdentityTypeNumber;
486506
}
487507

508+
/// Notifies Rokt that a purchase from a placement offer has been finalized.
509+
/// Call this method to inform Rokt about the completion status of an offer purchase initiated from a placement.
510+
/// @param placementId The identifier of the placement where the offer was displayed
511+
/// @param catalogItemId The identifier of the catalog item that was purchased
512+
/// @param success Whether the purchase was successful (YES) or failed (NO)
513+
/// @return MPKitExecStatus indicating success or failure of the operation
488514
- (MPKitExecStatus *)purchaseFinalized:(NSString *)placementId catalogItemId:(NSString *)catalogItemId success:(NSNumber *)success {
489515
if (placementId != nil && catalogItemId != nil && success != nil) {
490516
if (@available(iOS 15.0, *)) {

0 commit comments

Comments
 (0)