From b982c4138f7c802392f94b577ddd34df7ac89dc2 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Tue, 14 Apr 2026 11:52:01 -0400 Subject: [PATCH 1/5] feat: add selectShoppableAds API and migrate to mParticle SDK 9.0 - Add selectShoppableAds() to JS/TS API, codegen spec, iOS native bridge, and Android (no-op) - Migrate iOS native code to mParticle Apple SDK 9.0: - MPRoktEventCallback removed, use onEvent: block pattern - All MPRokt* event classes renamed to Rokt* (from RoktContracts) - Event property .placementId renamed to .identifier - MPRoktConfig -> RoktConfig via RoktConfigBuilder - MPRoktEmbeddedView -> RoktEmbeddedView - Import paths updated for mParticle_Apple_SDK_ObjC module - Add new shoppable ads event types: CartItemInstantPurchaseInitiated, CartItemInstantPurchaseFailure, InstantPurchaseDismissal, CartItemDevicePay - Backwards-compatible RoktCallback emission for onLoad/onUnLoad/etc. - Bump version to 3.0.0, iOS deployment target to 15.6 - Add shoppable ads demo button to ExpoTestApp Co-Authored-By: Claude Opus 4.6 (1M context) --- ExpoTestApp/App.tsx | 33 ++++ ExpoTestApp/package.json | 5 +- .../mparticle/react/rokt/MPRoktModuleImpl.kt | 9 + .../com/mparticle/react/rokt/MPRoktModule.kt | 17 +- .../com/mparticle/react/rokt/MPRoktModule.kt | 9 + ios/RNMParticle/RNMPRokt.mm | 171 ++++++++---------- ios/RNMParticle/RNMParticle.mm | 42 ++--- ios/RNMParticle/RoktEventManager.h | 5 +- ios/RNMParticle/RoktEventManager.m | 104 +++++++---- ios/RNMParticle/RoktLayoutManager.m | 9 +- .../RoktNativeLayoutComponentView.h | 13 +- .../RoktNativeLayoutComponentView.mm | 4 +- js/codegenSpecs/rokt/NativeMPRokt.ts | 6 + js/rokt/rokt.ts | 8 + package.json | 6 +- react-native-mparticle.podspec | 7 +- 16 files changed, 280 insertions(+), 168 deletions(-) diff --git a/ExpoTestApp/App.tsx b/ExpoTestApp/App.tsx index 8c78abe..f4d776f 100644 --- a/ExpoTestApp/App.tsx +++ b/ExpoTestApp/App.tsx @@ -219,6 +219,32 @@ export default function App() { const handleRoktBottomSheet = () => handleRoktSelectPlacements('MSDKBottomSheetLayout'); + const handleRoktShoppableAds = () => { + const attributes = { + email: 'user@example.com', + firstname: 'John', + lastname: 'Doe', + confirmationref: 'ORDER-12345', + amount: '99.99', + currency: 'USD', + paymenttype: 'credit_card', + }; + + const config = MParticle.Rokt.createRoktConfig('system'); + + addLog('Rokt: Calling selectShoppableAds'); + + MParticle.Rokt.selectShoppableAds('ConfirmationPage', attributes, config) + .then((result: any) => { + addLog(`Rokt selectShoppableAds success: ${JSON.stringify(result)}`); + setStatus('Rokt: Shoppable Ads loaded'); + }) + .catch((error: any) => { + addLog(`Rokt selectShoppableAds error: ${JSON.stringify(error)}`); + setStatus(`Rokt error: ${error.message || 'Unknown error'}`); + }); + }; + return ( @@ -323,6 +349,13 @@ export default function App() { > Bottom Sheet + + + Shoppable Ads + {/* Rokt Embedded Placeholder */} diff --git a/ExpoTestApp/package.json b/ExpoTestApp/package.json index 130ea21..96a5a90 100644 --- a/ExpoTestApp/package.json +++ b/ExpoTestApp/package.json @@ -10,12 +10,13 @@ "prebuild": "expo prebuild --clean" }, "dependencies": { - "react-native-mparticle": "file:../react-native-mparticle-latest.tgz", "expo": "~54.0.25", + "expo-build-properties": "~1.0.10", "expo-dev-client": "~6.0.16", "expo-status-bar": "~3.0.8", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-mparticle": "file:.." }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt index 504fe71..c46983b 100644 --- a/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt +++ b/android/src/main/java/com/mparticle/react/rokt/MPRoktModuleImpl.kt @@ -16,6 +16,7 @@ import com.mparticle.MpRoktEventCallback import com.mparticle.RoktEvent import com.mparticle.UnloadReasons import com.mparticle.WrapperSdk +import com.mparticle.internal.Logger import com.mparticle.rokt.CacheConfig import com.mparticle.rokt.RoktConfig import kotlinx.coroutines.Job @@ -39,6 +40,14 @@ class MPRoktModuleImpl( fun getName(): String = MODULE_NAME + fun selectShoppableAds( + identifier: String, + attributes: ReadableMap?, + roktConfig: ReadableMap?, + ) { + Logger.warning("selectShoppableAds is not yet supported on Android") + } + fun purchaseFinalized( placementId: String, catalogItemId: String, diff --git a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt index 9cf7a61..87c537d 100644 --- a/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/newarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -7,17 +7,15 @@ import com.facebook.react.bridge.ReadableType import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.UIManagerHelper import com.mparticle.MParticle -import com.mparticle.WrapperSdk +import com.mparticle.internal.Logger import com.mparticle.react.NativeMPRoktSpec import com.mparticle.rokt.RoktEmbeddedView -import com.mparticle.internal.Logger import java.lang.ref.WeakReference import java.util.concurrent.CountDownLatch class MPRoktModule( private val reactContext: ReactApplicationContext, ) : NativeMPRoktSpec(reactContext) { - private val impl = MPRoktModuleImpl(reactContext) override fun getName(): String = impl.getName() @@ -52,6 +50,15 @@ class MPRoktModule( ) } + @ReactMethod + override fun selectShoppableAds( + identifier: String, + attributes: ReadableMap?, + roktConfig: ReadableMap?, + ) { + impl.selectShoppableAds(identifier, attributes, roktConfig) + } + @ReactMethod override fun purchaseFinalized( placementId: String, @@ -61,7 +68,6 @@ class MPRoktModule( impl.purchaseFinalized(placementId, catalogItemId, success) } - /** * Process placeholders from ReadableMap to a map of Widgets for use with Rokt. * This method handles the Fabric-specific view resolution. @@ -83,8 +89,9 @@ class MPRoktModule( // Get the tag value as an integer val reactTag = when { - placeholders.getType(key) == ReadableType.Number -> + placeholders.getType(key) == ReadableType.Number -> { placeholders.getDouble(key).toInt() + } else -> { Logger.warning("Invalid view tag for key: $key") diff --git a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt index 3d0c853..5093f72 100644 --- a/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt +++ b/android/src/oldarch/java/com/mparticle/react/rokt/MPRoktModule.kt @@ -48,6 +48,15 @@ class MPRoktModule( } } + @ReactMethod + override fun selectShoppableAds( + identifier: String, + attributes: ReadableMap?, + roktConfig: ReadableMap?, + ) { + impl.selectShoppableAds(identifier, attributes, roktConfig) + } + @ReactMethod override fun purchaseFinalized( placementId: String, diff --git a/ios/RNMParticle/RNMPRokt.mm b/ios/RNMParticle/RNMPRokt.mm index b7cc060..d45b970 100644 --- a/ios/RNMParticle/RNMPRokt.mm +++ b/ios/RNMParticle/RNMPRokt.mm @@ -1,18 +1,19 @@ #import "RNMPRokt.h" -#if defined(__has_include) && __has_include() +// SDK 9.0: ObjC headers moved to mParticle_Apple_SDK_ObjC module +#if defined(__has_include) && __has_include() + #import + #import +#elif defined(__has_include) && __has_include() #import #import -#elif defined(__has_include) && __has_include() - #import #else - #import + #import + #import #endif -#if defined(__has_include) && __has_include() - #import -#elif defined(__has_include) && __has_include() - #import -#else - #import "mParticle_Apple_SDK-Swift.h" +#if __has_include() + #import +#elif __has_include() + #import #endif #import #import @@ -81,6 +82,12 @@ - (void)setMethodQueue:(dispatch_queue_t)methodQueue // We always return the UI manager's method queue } +- (void)ensureEventManager { + if (self.eventManager == nil) { + self.eventManager = [RoktEventManager allocWithZone: nil]; + } +} + #ifdef RCT_NEW_ARCH_ENABLED // Extracts roktConfig fields into an NSDictionary, returning nil when the // TurboModule bridge passes a null C++ reference for an omitted optional param. @@ -112,7 +119,7 @@ - (void)setMethodQueue:(dispatch_queue_t)methodQueue return roktConfigDict; } -// New Architecture Implementation +// New Architecture Implementation — selectPlacements - (void)selectPlacements:(NSString *)identifer attributes:(NSDictionary *)attributes placeholders:(NSDictionary *)placeholders @@ -123,48 +130,22 @@ - (void)selectPlacements:(NSString *)identifer NSMutableDictionary *finalAttributes = [self convertToMutableDictionaryOfStrings:attributes]; NSDictionary *roktConfigDict = safeExtractRoktConfigDict(roktConfig); - MPRoktConfig *config = [self buildRoktConfigFromDict:roktConfigDict]; + RoktConfig *config = [self buildRoktConfigFromDict:roktConfigDict]; #else -// Old Architecture Implementation +// Old Architecture Implementation — selectPlacements RCT_EXPORT_METHOD(selectPlacements:(NSString *) identifer attributes:(NSDictionary *)attributes placeholders:(NSDictionary * _Nullable)placeholders roktConfig:(NSDictionary * _Nullable)roktConfig fontFilesMap:(NSDictionary * _Nullable)fontFilesMap) { _rokt_log(@"[mParticle-Rokt] Old Architecture Implementation"); NSMutableDictionary *finalAttributes = [self convertToMutableDictionaryOfStrings:attributes]; - MPRoktConfig *config = [self buildRoktConfigFromDict:roktConfig]; + RoktConfig *config = [self buildRoktConfigFromDict:roktConfig]; #endif _rokt_log(@"[mParticle-Rokt] selectPlacements called with identifier: %@, attributes count: %lu", identifer, (unsigned long)finalAttributes.count); [MParticle _setWrapperSdk_internal:MPWrapperSdkReactNative version:@""]; - // Create callback implementation - MPRoktEventCallback *callbacks = [[MPRoktEventCallback alloc] init]; + [self ensureEventManager]; __weak __typeof__(self) weakSelf = self; - callbacks.onLoad = ^{ - _rokt_log(@"[mParticle-Rokt] onLoad"); - [weakSelf.eventManager onRoktCallbackReceived:@"onLoad"]; - }; - - callbacks.onUnLoad = ^{ - _rokt_log(@"[mParticle-Rokt] onUnLoad"); - [weakSelf.eventManager onRoktCallbackReceived:@"onUnLoad"]; - }; - - callbacks.onShouldShowLoadingIndicator = ^{ - _rokt_log(@"[mParticle-Rokt] onShouldShowLoadingIndicator"); - [weakSelf.eventManager onRoktCallbackReceived:@"onShouldShowLoadingIndicator"]; - }; - - callbacks.onShouldHideLoadingIndicator = ^{ - _rokt_log(@"[mParticle-Rokt] onShouldHideLoadingIndicator"); - [weakSelf.eventManager onRoktCallbackReceived:@"onShouldHideLoadingIndicator"]; - }; - - callbacks.onEmbeddedSizeChange = ^(NSString *placementId, CGFloat height) { - _rokt_log(@"[mParticle-Rokt] onEmbeddedSizeChange"); - [weakSelf.eventManager onWidgetHeightChanges:height placement:placementId]; - }; - BOOL bridgeNil = (self.bridge == nil); BOOL uiManagerNil = (self.bridge.uiManager == nil); _rokt_log(@"[mParticle-Rokt] bridge %@, uiManager %@", bridgeNil ? @"nil" : @"non-nil", uiManagerNil ? @"nil" : @"non-nil"); @@ -180,10 +161,6 @@ - (void)selectPlacements:(NSString *)identifer NSMutableDictionary *nativePlaceholders = strongSelf ? [strongSelf getNativePlaceholders:placeholders viewRegistry:viewRegistry] : [NSMutableDictionary dictionary]; - if (strongSelf) { - [strongSelf subscribeViewEvents:identifer]; - } - id mpInstance = [MParticle sharedInstance]; id roktKit = mpInstance ? [mpInstance rokt] : nil; _rokt_log(@"[mParticle-Rokt] MParticle sharedInstance %@, rokt kit %@", mpInstance ? @"non-nil" : @"nil", roktKit ? @"non-nil" : @"nil"); @@ -192,11 +169,46 @@ - (void)selectPlacements:(NSString *)identifer attributes:finalAttributes embeddedViews:nativePlaceholders config:config - callbacks:callbacks]; + onEvent:^(RoktEvent * _Nonnull event) { + [weakSelf.eventManager onRoktEvents:event viewName:identifer]; + }]; }]; _rokt_log(@"[mParticle-Rokt] addUIBlock enqueued for identifier: %@", identifer); } +#ifdef RCT_NEW_ARCH_ENABLED +// New Architecture Implementation — selectShoppableAds +- (void)selectShoppableAds:(NSString *)identifier + attributes:(NSDictionary *)attributes + roktConfig:(JS::NativeMPRokt::RoktConfigType &)roktConfig +{ + _rokt_log(@"[mParticle-Rokt] selectShoppableAds New Architecture"); + NSMutableDictionary *finalAttributes = [self convertToMutableDictionaryOfStrings:attributes]; + NSDictionary *roktConfigDict = safeExtractRoktConfigDict(roktConfig); + RoktConfig *config = [self buildRoktConfigFromDict:roktConfigDict]; +#else +// Old Architecture Implementation — selectShoppableAds +RCT_EXPORT_METHOD(selectShoppableAds:(NSString *)identifier attributes:(NSDictionary *)attributes roktConfig:(NSDictionary * _Nullable)roktConfig) +{ + _rokt_log(@"[mParticle-Rokt] selectShoppableAds Old Architecture"); + NSMutableDictionary *finalAttributes = [self convertToMutableDictionaryOfStrings:attributes]; + RoktConfig *config = [self buildRoktConfigFromDict:roktConfig]; +#endif + + _rokt_log(@"[mParticle-Rokt] selectShoppableAds called with identifier: %@, attributes count: %lu", identifier, (unsigned long)finalAttributes.count); + + [MParticle _setWrapperSdk_internal:MPWrapperSdkReactNative version:@""]; + [self ensureEventManager]; + __weak __typeof__(self) weakSelf = self; + + [[[MParticle sharedInstance] rokt] selectShoppableAds:identifier + attributes:finalAttributes + config:config + onEvent:^(RoktEvent * _Nonnull event) { + [weakSelf.eventManager onRoktEvents:event viewName:identifier]; + }]; +} + RCT_EXPORT_METHOD(purchaseFinalized : (NSString *)placementId catalogItemId : ( NSString *)catalogItemId success : (BOOL)success) { [[[MParticle sharedInstance] rokt] purchaseFinalized:placementId @@ -209,48 +221,35 @@ - (NSMutableDictionary*)convertToMutableDictionaryOfStrings:(NSDictionary*)attri NSMutableDictionary *finalAttributes = [attributes mutableCopy]; NSArray *keysForNullValues = [finalAttributes allKeysForObject:[NSNull null]]; [finalAttributes removeObjectsForKeys:keysForNullValues]; - + NSSet *keys = [finalAttributes keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) { return ![obj isKindOfClass:[NSString class]]; }]; - + [finalAttributes removeObjectsForKeys:[keys allObjects]]; return finalAttributes; - -} -- (MPColorMode)stringToColorMode:(NSString*)colorString -{ - if ([colorString isEqualToString:@"light"]) { - return MPColorModeLight; - } - else if ([colorString isEqualToString:@"dark"]) { - return MPColorModeDark; - } - else { - return MPColorModeSystem; - } } -- (MPRoktConfig *)buildRoktConfigFromDict:(NSDictionary *)configMap { +- (RoktConfig *)buildRoktConfigFromDict:(NSDictionary *)configMap { _rokt_log(@"[mParticle-Rokt] buildRoktConfigFromDict: configMap %@", configMap == nil ? @"nil" : [NSString stringWithFormat:@"non-nil (%lu keys)", (unsigned long)configMap.count]); - MPRoktConfig *config = [[MPRoktConfig alloc] init]; + if (configMap == nil || configMap.count == 0) { + _rokt_log(@"[mParticle-Rokt] buildRoktConfigFromDict: returning nil"); + return nil; + } + + RoktConfigBuilder *builder = [[RoktConfigBuilder alloc] init]; BOOL isConfigEmpty = YES; NSString *colorModeString = configMap[@"colorMode"]; if (colorModeString && [colorModeString isKindOfClass:[NSString class]]) { - if (@available(iOS 12.0, *)) { - isConfigEmpty = NO; - if ([colorModeString isEqualToString:@"dark"]) { - if (@available(iOS 13.0, *)) { - config.colorMode = MPColorModeDark; - } - } else if ([colorModeString isEqualToString:@"light"]) { - config.colorMode = MPColorModeLight; - } else { - // default: "system" - config.colorMode = MPColorModeSystem; - } + isConfigEmpty = NO; + if ([colorModeString isEqualToString:@"dark"]) { + [builder colorMode:RoktColorModeDark]; + } else if ([colorModeString isEqualToString:@"light"]) { + [builder colorMode:RoktColorModeLight]; + } else { + [builder colorMode:RoktColorModeSystem]; } } @@ -262,23 +261,13 @@ - (MPRoktConfig *)buildRoktConfigFromDict:(NSDictionary *)config cacheDuration = @0; } NSDictionary *cacheAttributes = cacheConfigMap[@"cacheAttributes"]; - config.cacheAttributes = cacheAttributes; - config.cacheDuration = cacheDuration; + RoktCacheConfig *cacheConfig = [[RoktCacheConfig alloc] initWithCacheDuration:[cacheDuration longLongValue] + cacheAttributes:cacheAttributes ?: @{}]; + [builder cacheConfig:cacheConfig]; } _rokt_log(@"[mParticle-Rokt] buildRoktConfigFromDict: returning %@", isConfigEmpty ? @"nil" : @"config"); - return isConfigEmpty ? nil : config; -} - -- (void)subscribeViewEvents:(NSString* _Nonnull) viewName -{ - _rokt_log(@"[mParticle-Rokt] subscribeViewEvents for viewName: %@", viewName); - if (self.eventManager == nil) { - self.eventManager = [RoktEventManager allocWithZone: nil]; - } - [[[MParticle sharedInstance] rokt] events:viewName onEvent:^(MPRoktEvent * _Nonnull roktEvent) { - [self.eventManager onRoktEvents:roktEvent viewName:viewName]; - }]; + return isConfigEmpty ? nil : [builder build]; } - (NSMutableDictionary *)getNativePlaceholders:(NSDictionary *)placeholders viewRegistry:(NSDictionary *)viewRegistry @@ -295,8 +284,8 @@ - (NSMutableDictionary *)getNativePlaceholders:(NSDictionary *)placeholders view } nativePlaceholders[key] = wrapperView.roktEmbeddedView; #else - MPRoktEmbeddedView *view = viewRegistry[[placeholders objectForKey:key]]; - if (!view || ![view isKindOfClass:[MPRoktEmbeddedView class]]) { + RoktEmbeddedView *view = viewRegistry[[placeholders objectForKey:key]]; + if (!view || ![view isKindOfClass:[RoktEmbeddedView class]]) { RCTLogError(@"Cannot find RoktEmbeddedView with tag #%@", key); continue; } diff --git a/ios/RNMParticle/RNMParticle.mm b/ios/RNMParticle/RNMParticle.mm index 7580640..f2968d0 100644 --- a/ios/RNMParticle/RNMParticle.mm +++ b/ios/RNMParticle/RNMParticle.mm @@ -1,18 +1,12 @@ #import "RNMParticle.h" #import -#if defined(__has_include) && __has_include() +// SDK 9.0: ObjC headers moved to mParticle_Apple_SDK_ObjC module +#if defined(__has_include) && __has_include() + #import +#elif defined(__has_include) && __has_include() #import -#elif defined(__has_include) && __has_include() - #import #else - #import -#endif -#if defined(__has_include) && __has_include() - #import -#elif defined(__has_include) && __has_include() - #import -#else - #import "mParticle_Apple_SDK-Swift.h" + #import #endif #import @@ -44,8 +38,8 @@ + (void)load { RCT_EXPORT_METHOD(setLocation:(double)latitude longitude:(double)longitude) { - CLLocation *newLocation = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude]; - [MParticle sharedInstance].location = newLocation; + // Location support was removed in mParticle Apple SDK 9.0 + NSLog(@"[RNMParticle] setLocation is a no-op in mParticle SDK 9.0+"); } RCT_EXPORT_METHOD(setUploadInterval:(double)uploadInterval) @@ -204,8 +198,8 @@ + (void)load { [reactError setObject:[NSNumber numberWithLong:response.httpCode] forKey:@"httpCode"]; } - if ([NSNumber numberWithInt:response.code] != nil) { - [reactError setObject:[NSNumber numberWithInt:response.code] forKey:@"responseCode"]; + if ([NSNumber numberWithInteger:response.code] != nil) { + [reactError setObject:[NSNumber numberWithInteger:response.code] forKey:@"responseCode"]; } if (response.message != nil) { @@ -238,8 +232,8 @@ + (void)load { [reactError setObject:[NSNumber numberWithLong:response.httpCode] forKey:@"httpCode"]; } - if ([NSNumber numberWithInt:response.code] != nil) { - [reactError setObject:[NSNumber numberWithInt:response.code] forKey:@"responseCode"]; + if ([NSNumber numberWithInteger:response.code] != nil) { + [reactError setObject:[NSNumber numberWithInteger:response.code] forKey:@"responseCode"]; } if (response.message != nil) { @@ -272,8 +266,8 @@ + (void)load { [reactError setObject:[NSNumber numberWithLong:response.httpCode] forKey:@"httpCode"]; } - if ([NSNumber numberWithInt:response.code] != nil) { - [reactError setObject:[NSNumber numberWithInt:response.code] forKey:@"responseCode"]; + if ([NSNumber numberWithInteger:response.code] != nil) { + [reactError setObject:[NSNumber numberWithInteger:response.code] forKey:@"responseCode"]; } if (response.message != nil) { @@ -306,8 +300,8 @@ + (void)load { [reactError setObject:[NSNumber numberWithLong:response.httpCode] forKey:@"httpCode"]; } - if ([NSNumber numberWithInt:response.code] != nil) { - [reactError setObject:[NSNumber numberWithInt:response.code] forKey:@"responseCode"]; + if ([NSNumber numberWithInteger:response.code] != nil) { + [reactError setObject:[NSNumber numberWithInteger:response.code] forKey:@"responseCode"]; } if (response.message != nil) { @@ -684,8 +678,8 @@ - (void)performIdentityRequest:(NSDictionary *)identityRequest callback:(RCTResp if ([NSNumber numberWithLong:response.httpCode] != nil) { [reactError setObject:[NSNumber numberWithLong:response.httpCode] forKey:@"httpCode"]; } - if ([NSNumber numberWithInt:response.code] != nil) { - [reactError setObject:[NSNumber numberWithInt:response.code] forKey:@"responseCode"]; + if ([NSNumber numberWithInteger:response.code] != nil) { + [reactError setObject:[NSNumber numberWithInteger:response.code] forKey:@"responseCode"]; } if (response.message != nil) { [reactError setObject:response.message forKey:@"message"]; @@ -1165,7 +1159,7 @@ + (MPEvent *)MPEvent:(id)json { event.category = json[@"category"]; event.duration = json[@"duration"]; event.endTime = json[@"endTime"]; - event.info = json[@"info"]; + event.customAttributes = json[@"info"]; event.name = json[@"name"]; event.startTime = json[@"startTime"]; [event setType:(MPEventType)[json[@"type"] intValue]]; diff --git a/ios/RNMParticle/RoktEventManager.h b/ios/RNMParticle/RoktEventManager.h index 6c7bd90..af630ff 100644 --- a/ios/RNMParticle/RoktEventManager.h +++ b/ios/RNMParticle/RoktEventManager.h @@ -1,6 +1,7 @@ #import #import -#import + +@class RoktEvent; NS_ASSUME_NONNULL_BEGIN @@ -9,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)onWidgetHeightChanges:(CGFloat)widgetHeight placement:(NSString * _Nonnull)selectedPlacement; - (void)onFirstPositiveResponse; - (void)onRoktCallbackReceived:(NSString * _Nonnull)eventValue; -- (void)onRoktEvents:(MPRoktEvent * _Nonnull)event viewName:(NSString * _Nullable)viewName; +- (void)onRoktEvents:(RoktEvent * _Nonnull)event viewName:(NSString * _Nullable)viewName; @end diff --git a/ios/RNMParticle/RoktEventManager.m b/ios/RNMParticle/RoktEventManager.m index a9b211a..9870f56 100644 --- a/ios/RNMParticle/RoktEventManager.m +++ b/ios/RNMParticle/RoktEventManager.m @@ -1,5 +1,5 @@ #import "RoktEventManager.h" -#import +@import RoktContracts; #import static os_log_t _rokt_events_os_log(void) { @@ -78,7 +78,7 @@ - (void)onRoktCallbackReceived:(NSString*)eventValue } } -- (void)onRoktEvents:(MPRoktEvent * _Nonnull)event viewName:(NSString * _Nullable)viewName +- (void)onRoktEvents:(RoktEvent * _Nonnull)event viewName:(NSString * _Nullable)viewName { NSString *eventClass = event ? NSStringFromClass([event class]) : @"nil"; _rokt_events_log(@"[mParticle-Rokt] RoktEventManager onRoktEvents: %@ viewName: %@", eventClass, viewName ?: @"(nil)"); @@ -96,60 +96,92 @@ - (void)onRoktEvents:(MPRoktEvent * _Nonnull)event viewName:(NSString * _Nullabl NSDecimalNumber *quantity; NSDecimalNumber *totalPrice; NSDecimalNumber *unitPrice; - - if ([event isKindOfClass:[MPRoktShowLoadingIndicator class]]) { + NSString *error; + NSString *paymentProvider; + + if ([event isKindOfClass:[RoktShowLoadingIndicator class]]) { eventName = @"ShowLoadingIndicator"; - } else if ([event isKindOfClass:[MPRoktHideLoadingIndicator class]]) { + [self onRoktCallbackReceived:@"onShouldShowLoadingIndicator"]; + } else if ([event isKindOfClass:[RoktHideLoadingIndicator class]]) { eventName = @"HideLoadingIndicator"; - } else if ([event isKindOfClass:[MPRoktPlacementInteractive class]]) { - placementId = ((MPRoktPlacementInteractive *)event).placementId; + [self onRoktCallbackReceived:@"onShouldHideLoadingIndicator"]; + } else if ([event isKindOfClass:[RoktPlacementInteractive class]]) { + placementId = ((RoktPlacementInteractive *)event).identifier; eventName = @"PlacementInteractive"; - } else if ([event isKindOfClass:[MPRoktPlacementReady class]]) { - placementId = ((MPRoktPlacementReady *)event).placementId; + } else if ([event isKindOfClass:[RoktPlacementReady class]]) { + placementId = ((RoktPlacementReady *)event).identifier; eventName = @"PlacementReady"; - } else if ([event isKindOfClass:[MPRoktOfferEngagement class]]) { - placementId = ((MPRoktOfferEngagement *)event).placementId; + [self onRoktCallbackReceived:@"onLoad"]; + } else if ([event isKindOfClass:[RoktOfferEngagement class]]) { + placementId = ((RoktOfferEngagement *)event).identifier; eventName = @"OfferEngagement"; - } else if ([event isKindOfClass:[MPRoktPositiveEngagement class]]) { - placementId = ((MPRoktPositiveEngagement *)event).placementId; + } else if ([event isKindOfClass:[RoktPositiveEngagement class]]) { + placementId = ((RoktPositiveEngagement *)event).identifier; eventName = @"PositiveEngagement"; - } else if ([event isKindOfClass:[MPRoktPlacementClosed class]]) { - placementId = ((MPRoktPlacementClosed *)event).placementId; + } else if ([event isKindOfClass:[RoktPlacementClosed class]]) { + placementId = ((RoktPlacementClosed *)event).identifier; eventName = @"PlacementClosed"; - } else if ([event isKindOfClass:[MPRoktPlacementCompleted class]]) { - placementId = ((MPRoktPlacementCompleted *)event).placementId; + [self onRoktCallbackReceived:@"onUnLoad"]; + } else if ([event isKindOfClass:[RoktPlacementCompleted class]]) { + placementId = ((RoktPlacementCompleted *)event).identifier; eventName = @"PlacementCompleted"; - } else if ([event isKindOfClass:[MPRoktPlacementFailure class]]) { - placementId = ((MPRoktPlacementFailure *)event).placementId; + } else if ([event isKindOfClass:[RoktPlacementFailure class]]) { + placementId = ((RoktPlacementFailure *)event).identifier; eventName = @"PlacementFailure"; - } else if ([event isKindOfClass:[MPRoktFirstPositiveEngagement class]]) { - placementId = ((MPRoktFirstPositiveEngagement *)event).placementId; + } else if ([event isKindOfClass:[RoktFirstPositiveEngagement class]]) { + placementId = ((RoktFirstPositiveEngagement *)event).identifier; eventName = @"FirstPositiveEngagement"; - } else if ([event isKindOfClass:[MPRoktInitComplete class]]) { + } else if ([event isKindOfClass:[RoktInitComplete class]]) { eventName = @"InitComplete"; - status = ((MPRoktInitComplete *)event).success ? @"true" : @"false"; - } else if ([event isKindOfClass:[MPRoktOpenUrl class]]) { + status = ((RoktInitComplete *)event).success ? @"true" : @"false"; + } else if ([event isKindOfClass:[RoktOpenUrl class]]) { eventName = @"OpenUrl"; - placementId = ((MPRoktOpenUrl *)event).placementId; - url = ((MPRoktOpenUrl *)event).url; - } else if ([event isKindOfClass:[MPRoktCartItemInstantPurchase class]]) { - MPRoktCartItemInstantPurchase *cartEvent = (MPRoktCartItemInstantPurchase *)event; + placementId = ((RoktOpenUrl *)event).identifier; + url = ((RoktOpenUrl *)event).url; + } else if ([event isKindOfClass:[RoktEmbeddedSizeChanged class]]) { + RoktEmbeddedSizeChanged *sizeEvent = (RoktEmbeddedSizeChanged *)event; + placementId = sizeEvent.identifier; + eventName = @"EmbeddedSizeChanged"; + [self onWidgetHeightChanges:sizeEvent.updatedHeight placement:sizeEvent.identifier]; + } else if ([event isKindOfClass:[RoktCartItemInstantPurchase class]]) { + RoktCartItemInstantPurchase *cartEvent = (RoktCartItemInstantPurchase *)event; eventName = @"CartItemInstantPurchase"; - // Required properties - placementId = cartEvent.placementId; + placementId = cartEvent.identifier; cartItemId = cartEvent.cartItemId; catalogItemId = cartEvent.catalogItemId; currency = cartEvent.currency; providerData = cartEvent.providerData; - // Optional properties linkedProductId = cartEvent.linkedProductId; - // Overridden description property itemDescription = cartEvent.description; - // Decimal properties quantity = cartEvent.quantity; totalPrice = cartEvent.totalPrice; unitPrice = cartEvent.unitPrice; + } else if ([event isKindOfClass:[RoktCartItemInstantPurchaseInitiated class]]) { + RoktCartItemInstantPurchaseInitiated *initiatedEvent = (RoktCartItemInstantPurchaseInitiated *)event; + eventName = @"CartItemInstantPurchaseInitiated"; + placementId = initiatedEvent.identifier; + catalogItemId = initiatedEvent.catalogItemId; + cartItemId = initiatedEvent.cartItemId; + } else if ([event isKindOfClass:[RoktCartItemInstantPurchaseFailure class]]) { + RoktCartItemInstantPurchaseFailure *failureEvent = (RoktCartItemInstantPurchaseFailure *)event; + eventName = @"CartItemInstantPurchaseFailure"; + placementId = failureEvent.identifier; + catalogItemId = failureEvent.catalogItemId; + cartItemId = failureEvent.cartItemId; + error = failureEvent.error; + } else if ([event isKindOfClass:[RoktInstantPurchaseDismissal class]]) { + RoktInstantPurchaseDismissal *dismissalEvent = (RoktInstantPurchaseDismissal *)event; + eventName = @"InstantPurchaseDismissal"; + placementId = dismissalEvent.identifier; + } else if ([event isKindOfClass:[RoktCartItemDevicePay class]]) { + RoktCartItemDevicePay *devicePayEvent = (RoktCartItemDevicePay *)event; + eventName = @"CartItemDevicePay"; + placementId = devicePayEvent.identifier; + catalogItemId = devicePayEvent.catalogItemId; + cartItemId = devicePayEvent.cartItemId; + paymentProvider = devicePayEvent.paymentProvider; } + NSMutableDictionary *payload = [@{@"event": eventName} mutableCopy]; if (viewName != nil) { [payload setObject:viewName forKey:@"viewName"]; @@ -190,6 +222,12 @@ - (void)onRoktEvents:(MPRoktEvent * _Nonnull)event viewName:(NSString * _Nullabl if (unitPrice != nil) { [payload setObject:unitPrice forKey:@"unitPrice"]; } + if (error != nil) { + [payload setObject:error forKey:@"error"]; + } + if (paymentProvider != nil) { + [payload setObject:paymentProvider forKey:@"paymentProvider"]; + } [self sendEventWithName:@"RoktEvents" body:payload]; } diff --git a/ios/RNMParticle/RoktLayoutManager.m b/ios/RNMParticle/RoktLayoutManager.m index 168c783..4252576 100644 --- a/ios/RNMParticle/RoktLayoutManager.m +++ b/ios/RNMParticle/RoktLayoutManager.m @@ -1,6 +1,11 @@ #import #import -#import +#if defined(__has_include) && __has_include() + #import +#else + #import +#endif +@import RoktContracts; @interface RoktLayoutViewManager : RCTViewManager @end @@ -11,7 +16,7 @@ @implementation RoktLayoutViewManager - (UIView *)view { - return [[MPRoktEmbeddedView alloc] init]; + return [[RoktEmbeddedView alloc] init]; } + (BOOL)requiresMainQueueSetup diff --git a/ios/RNMParticle/RoktNativeLayoutComponentView.h b/ios/RNMParticle/RoktNativeLayoutComponentView.h index 8055710..81a8258 100644 --- a/ios/RNMParticle/RoktNativeLayoutComponentView.h +++ b/ios/RNMParticle/RoktNativeLayoutComponentView.h @@ -2,7 +2,16 @@ #import #import #import -#import +#if defined(__has_include) && __has_include() + #import +#else + #import +#endif +#if __has_include() + #import +#elif __has_include() + #import +#endif #ifndef RoktNativeLayoutComponentView_h #define RoktNativeLayoutComponentView_h @@ -10,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN @interface RoktNativeLayoutComponentView : RCTViewComponentView -@property (nonatomic, readonly) MPRoktEmbeddedView *roktEmbeddedView; +@property (nonatomic, readonly) RoktEmbeddedView *roktEmbeddedView; @end NS_ASSUME_NONNULL_END diff --git a/ios/RNMParticle/RoktNativeLayoutComponentView.mm b/ios/RNMParticle/RoktNativeLayoutComponentView.mm index 8ae353f..7145191 100644 --- a/ios/RNMParticle/RoktNativeLayoutComponentView.mm +++ b/ios/RNMParticle/RoktNativeLayoutComponentView.mm @@ -9,7 +9,7 @@ using namespace facebook::react; @interface RoktNativeLayoutComponentView () -@property (nonatomic, nullable) MPRoktEmbeddedView *roktEmbeddedView; +@property (nonatomic, nullable) RoktEmbeddedView *roktEmbeddedView; @property (nonatomic, nullable) NSString *placeholderName; @end @@ -25,7 +25,7 @@ + (ComponentDescriptorProvider)componentDescriptorProvider - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { - _roktEmbeddedView = [[MPRoktEmbeddedView alloc] initWithFrame:self.bounds]; + _roktEmbeddedView = [[RoktEmbeddedView alloc] initWithFrame:self.bounds]; _roktEmbeddedView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self addSubview:_roktEmbeddedView]; NSLog(@"[ROKT] iOS Fabric: RoktFabricWrapperView initialized"); diff --git a/js/codegenSpecs/rokt/NativeMPRokt.ts b/js/codegenSpecs/rokt/NativeMPRokt.ts index 07ded38..54fe306 100644 --- a/js/codegenSpecs/rokt/NativeMPRokt.ts +++ b/js/codegenSpecs/rokt/NativeMPRokt.ts @@ -27,6 +27,12 @@ export interface Spec extends TurboModule { catalogItemId: string, success: boolean ): void; + + selectShoppableAds( + identifier: string, + attributes: { [key: string]: string }, + roktConfig?: RoktConfigType + ): void; } export default TurboModuleRegistry.getEnforcing('RNMPRokt'); diff --git a/js/rokt/rokt.ts b/js/rokt/rokt.ts index d8d51cd..11c0dd9 100644 --- a/js/rokt/rokt.ts +++ b/js/rokt/rokt.ts @@ -31,6 +31,14 @@ export abstract class Rokt { ); } + static async selectShoppableAds( + identifier: string, + attributes: Record, + roktConfig?: IRoktConfig + ): Promise { + MPRokt.selectShoppableAds(identifier, attributes, roktConfig); + } + static async purchaseFinalized( placementId: string, catalogItemId: string, diff --git a/package.json b/package.json index f21cb98..67a3e98 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "https://www.mparticle.com", "license": "Apache-2.0", "repository": "mParticle/react-native-mparticle", - "version": "2.9.2", + "version": "3.0.0", "main": "lib/index.js", "types": "lib/index.d.ts", "react-native": "js/index", @@ -66,7 +66,9 @@ "javaPackageName": "com.mparticle.react" }, "ios": { - "RoktNativeLayout": "RoktNativeLayoutComponentView" + "componentProvider": { + "RoktNativeLayout": "RoktNativeLayoutComponentView" + } } } } diff --git a/react-native-mparticle.podspec b/react-native-mparticle.podspec index 62f99c9..c7d69ce 100644 --- a/react-native-mparticle.podspec +++ b/react-native-mparticle.podspec @@ -1,7 +1,7 @@ require 'json' new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' -ios_platform = new_arch_enabled ? '11.0' : '9.0' +ios_platform = '15.6' package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.homepage = package['homepage'] s.license = package['license'] - s.platforms = { :ios => ios_platform, :tvos => "9.2" } + s.platforms = { :ios => ios_platform, :tvos => "15.6" } s.source = { :git => "https://github.com/mParticle/react-native-mparticle.git", :tag => "#{s.version}" } s.source_files = "ios/**/*.{h,m,mm,swift}" @@ -25,5 +25,6 @@ Pod::Spec.new do |s| s.dependency "React-Core" end - s.dependency 'mParticle-Apple-SDK', '~> 8.0' + s.dependency 'mParticle-Apple-SDK-ObjC', '~> 9.0' + s.dependency 'RoktContracts', '~> 0.1' end From 0fa56864169acdb85a8d9afcb684020fcf593da9 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Tue, 14 Apr 2026 14:55:05 -0400 Subject: [PATCH 2/5] Address Expo prebuild issues --- plugin/src/withMParticleIOS.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/withMParticleIOS.ts b/plugin/src/withMParticleIOS.ts index 6c1f38a..69da5fa 100644 --- a/plugin/src/withMParticleIOS.ts +++ b/plugin/src/withMParticleIOS.ts @@ -338,7 +338,7 @@ function addMParticleToObjcAppDelegate( * These are dependencies of mParticle kits that must also be dynamic frameworks */ const KIT_TRANSITIVE_DEPENDENCIES: Record = { - 'mParticle-Rokt': ['Rokt-Widget'], + 'mParticle-Rokt': ['Rokt-Widget', 'RoktContracts', 'RoktUXHelper', 'DcuiSchema'], // Add other kit dependencies here as needed // "mParticle-Amplitude": [], // "mParticle-Braze": [], @@ -348,7 +348,7 @@ const KIT_TRANSITIVE_DEPENDENCIES: Record = { * Get all pods that need dynamic framework linking */ function getDynamicFrameworkPods(iosKits?: string[]): string[] { - const pods = ['mParticle-Apple-SDK']; + const pods = ['mParticle-Apple-SDK', 'mParticle-Apple-SDK-ObjC', 'mParticle-Apple-SDK-Swift']; if (iosKits) { for (const kit of iosKits) { From aa42c0d074aeff82b02c3b3f28647079cd18db18 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 15 Apr 2026 10:26:02 -0400 Subject: [PATCH 3/5] Add Implementtion Guide --- ExpoTestApp/App.tsx | 28 ++++++++++++++++------- ExpoTestApp/README.md | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/ExpoTestApp/App.tsx b/ExpoTestApp/App.tsx index f4d776f..b27dc77 100644 --- a/ExpoTestApp/App.tsx +++ b/ExpoTestApp/App.tsx @@ -221,20 +221,32 @@ export default function App() { const handleRoktShoppableAds = () => { const attributes = { - email: 'user@example.com', - firstname: 'John', - lastname: 'Doe', - confirmationref: 'ORDER-12345', - amount: '99.99', - currency: 'USD', - paymenttype: 'credit_card', + country: "US", + shippingstate: "NY", + shippingzipcode: "10001", + firstname: "Jenny", + stripeApplePayAvailable: "true", + last4digits: "4444", + shippingaddress1: "123 Main St", + colormode: "LIGHT", + billingzipcode: "07762", + paymenttype: "ApplePay", + shippingcountry: "US", + sandbox: "true", + shippingaddress2: "Apt 4B", + confirmationref: "ORD-12345", + shippingcity: "New York", + newToApplePay: "false", + applePayCapabilities: "true", + lastname: "Smith", + email: "jenny.smith@example.com" }; const config = MParticle.Rokt.createRoktConfig('system'); addLog('Rokt: Calling selectShoppableAds'); - MParticle.Rokt.selectShoppableAds('ConfirmationPage', attributes, config) + MParticle.Rokt.selectShoppableAds('StgRoktShoppableAds', attributes, config) .then((result: any) => { addLog(`Rokt selectShoppableAds success: ${JSON.stringify(result)}`); setStatus('Rokt: Shoppable Ads loaded'); diff --git a/ExpoTestApp/README.md b/ExpoTestApp/README.md index 0b0e082..3a3df9f 100644 --- a/ExpoTestApp/README.md +++ b/ExpoTestApp/README.md @@ -80,6 +80,7 @@ The app also includes Rokt placement testing via the mParticle Rokt kit: - **Embedded**: Loads an embedded Rokt placement that renders in-line within the app content. The placement appears in the designated placeholder area below the buttons. - **Overlay**: Loads a full-screen overlay Rokt placement that appears on top of the app content. - **Bottom Sheet**: Loads a bottom sheet Rokt placement that slides up from the bottom of the screen. +- **Shoppable Ads**: Calls `MParticle.Rokt.selectShoppableAds` with a staging placement identifier and checkout-style attributes (see implementation guide below). The Rokt section also demonstrates: @@ -87,6 +88,56 @@ The Rokt section also demonstrates: - Rokt event listeners for callbacks and placement events - Using `RoktLayoutView` as an embedded placeholder component +### Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions + +This mirrors the recent SDK work (Shoppable Ads API on iOS and the Expo test app wiring) and how to pair it with native payment registration. + +#### JavaScript: `selectShoppableAds` + +Use `MParticle.Rokt.selectShoppableAds(identifier, attributes, roktConfig?)` when you need the Shoppable Ads experience instead of `selectPlacements`. + +- **identifier**: Rokt page / placement identifier configured for your account (the Expo test app uses a staging example such as `StgRoktShoppableAds`; replace with your production identifier). +- **attributes**: String key/value pairs passed to Rokt (shipping, billing, payment hints, sandbox flags, etc.). The demo in `App.tsx` includes fields like `country`, `shippingstate`, `paymenttype`, `stripeApplePayAvailable`, `applePayCapabilities`, and `sandbox`—adjust to match your integration and Rokt’s attribute contract. +- **roktConfig**: Optional; the demo uses `MParticle.Rokt.createRoktConfig('system')` for color mode. Add a cache config if you use caching elsewhere. + +Example (same pattern as `ExpoTestApp/App.tsx`): + +```javascript +const config = MParticle.Rokt.createRoktConfig('system'); + +MParticle.Rokt.selectShoppableAds('YOUR_PLACEMENT_ID', attributes, config) + .then(() => { /* success */ }) + .catch((error) => { /* handle */ }); +``` + +Listen for `RoktCallback` and `RoktEvents` on `RoktEventManager` to observe load/unload and Shoppable Ads–related events emitted by the native bridge. + +**Android:** `selectShoppableAds` is not implemented on Android yet; the native module logs a warning and does not run the Shoppable Ads flow. Plan for iOS-only behavior until Android support ships. + +#### iOS native: `RoktStripePaymentExtension` (payment extensions) + +Shoppable Ads flows that use Apple Pay / Stripe integration expect a **payment extension** to be registered on mParticle’s Rokt interface after the SDK starts. + +In `ios/MParticleExpoTest/AppDelegate.swift`, the test app: + +1. Imports the Stripe payment extension module provided with the Rokt / kit stack: `import RoktStripePaymentExtension`. +2. After `MParticle.sharedInstance().start(with: mParticleOptions)`, constructs `RoktStripePaymentExtension(applePayMerchantId: "...")` with your **Apple Pay merchant ID** (replace `merchant.dummy` with your real `merchant.*` identifier from Apple Developer). +3. Registers it: `MParticle.sharedInstance().rokt.register(paymentExtension)`. + +```swift +import RoktStripePaymentExtension + +// After MParticle.sharedInstance().start(with: mParticleOptions): +if let paymentExtension = RoktStripePaymentExtension(applePayMerchantId: "merchant.your.id") { + MParticle.sharedInstance().rokt.register(paymentExtension) +} +``` + +**Important:** + +- The Expo config plugin **does not** generate the payment extension block today. After `expo prebuild`, add or merge this code into `AppDelegate.swift` (inside the same app launch path as mParticle init). If you regenerate native projects with `--clean`, re-apply this snippet. +- Ensure the **mParticle Rokt kit** (and transitive Rokt dependencies) are installed so `RoktStripePaymentExtension` resolves—same as configuring `iosKits`: `["mParticle-Rokt"]` in `app.json`. + All activity is logged in the Activity Log section at the bottom of the screen. ## Verifying Plugin Integration @@ -116,6 +167,8 @@ Check `ios/MParticleExpoTest/AppDelegate.swift` for: MParticle.sharedInstance().start(with: mParticleOptions) ``` +For Shoppable Ads with Apple Pay / Stripe, you may also need to register `RoktStripePaymentExtension` after `start`—see **Implementation guide: Shoppable Ads (`selectShoppableAds`) and iOS payment extensions** above. + #### Objective-C AppDelegate (Legacy) For older Expo SDK versions, check `ios/MParticleExpoTest/AppDelegate.mm` for: From fe342f63fee755a3bdc832f20ae3901ce3f92ae1 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Wed, 15 Apr 2026 15:29:04 -0400 Subject: [PATCH 4/5] Fix CI --- .trunk/trunk.yaml | 9 +++-- ExpoTestApp/App.tsx | 38 +++++++++---------- ExpoTestApp/README.md | 34 +++++++++-------- .../com/mparticle/react/NativeMPRoktSpec.kt | 6 +++ package.json | 2 +- plugin/src/withMParticleIOS.ts | 13 ++++++- sample/ios/Podfile | 5 ++- 7 files changed, 65 insertions(+), 42 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 74ad5ff..b28274b 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,7 +8,8 @@ runtimes: enabled: - go@1.19.5 - java@13.0.11 - - node@18.12.1 + # markdownlint 0.48+ / string-width need Node 20+ (regex v flag) + - node@20.18.0 - python@3.10.8 # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) lint: @@ -40,10 +41,12 @@ lint: - actionlint@1.6.9 - checkov@3.2.507 - dotenv-linter@3.3.0 - - eslint@10.0.2 + # ESLint 9+ defaults to flat config only; this repo uses .eslintrc.js (ESLint 8 style). + - eslint@8.57.1 - git-diff-check - ktlint@0.43.2 - - markdownlint@0.48.0 + # 0.48+ pulls string-width that requires Node 20+ for regex /v; Trunk's runner uses Node 18. + - markdownlint@0.39.0 - mparticle-api-key-check - osv-scanner@1.3.6 - oxipng@7.0.0 diff --git a/ExpoTestApp/App.tsx b/ExpoTestApp/App.tsx index b27dc77..3668573 100644 --- a/ExpoTestApp/App.tsx +++ b/ExpoTestApp/App.tsx @@ -221,25 +221,25 @@ export default function App() { const handleRoktShoppableAds = () => { const attributes = { - country: "US", - shippingstate: "NY", - shippingzipcode: "10001", - firstname: "Jenny", - stripeApplePayAvailable: "true", - last4digits: "4444", - shippingaddress1: "123 Main St", - colormode: "LIGHT", - billingzipcode: "07762", - paymenttype: "ApplePay", - shippingcountry: "US", - sandbox: "true", - shippingaddress2: "Apt 4B", - confirmationref: "ORD-12345", - shippingcity: "New York", - newToApplePay: "false", - applePayCapabilities: "true", - lastname: "Smith", - email: "jenny.smith@example.com" + country: 'US', + shippingstate: 'NY', + shippingzipcode: '10001', + firstname: 'Jenny', + stripeApplePayAvailable: 'true', + last4digits: '4444', + shippingaddress1: '123 Main St', + colormode: 'LIGHT', + billingzipcode: '07762', + paymenttype: 'ApplePay', + shippingcountry: 'US', + sandbox: 'true', + shippingaddress2: 'Apt 4B', + confirmationref: 'ORD-12345', + shippingcity: 'New York', + newToApplePay: 'false', + applePayCapabilities: 'true', + lastname: 'Smith', + email: 'jenny.smith@example.com', }; const config = MParticle.Rokt.createRoktConfig('system'); diff --git a/ExpoTestApp/README.md b/ExpoTestApp/README.md index 3a3df9f..3edb204 100644 --- a/ExpoTestApp/README.md +++ b/ExpoTestApp/README.md @@ -106,8 +106,12 @@ Example (same pattern as `ExpoTestApp/App.tsx`): const config = MParticle.Rokt.createRoktConfig('system'); MParticle.Rokt.selectShoppableAds('YOUR_PLACEMENT_ID', attributes, config) - .then(() => { /* success */ }) - .catch((error) => { /* handle */ }); + .then(() => { + /* success */ + }) + .catch(error => { + /* handle */ + }); ``` Listen for `RoktCallback` and `RoktEvents` on `RoktEventManager` to observe load/unload and Shoppable Ads–related events emitted by the native bridge. @@ -277,16 +281,16 @@ dependencies { ## Plugin Configuration Options -| Option | Type | Description | -|--------|------|-------------| -| `iosApiKey` | string | mParticle iOS API key | -| `iosApiSecret` | string | mParticle iOS API secret | -| `androidApiKey` | string | mParticle Android API key | -| `androidApiSecret` | string | mParticle Android API secret | -| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` | -| `environment` | string | Environment: `development`, `production`, `autoDetect` | -| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) | -| `dataPlanId` | string | Data plan ID for validation | -| `dataPlanVersion` | number | Data plan version | -| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) | -| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) | +| Option | Type | Description | +| ------------------------- | -------- | --------------------------------------------------------- | +| `iosApiKey` | string | mParticle iOS API key | +| `iosApiSecret` | string | mParticle iOS API secret | +| `androidApiKey` | string | mParticle Android API key | +| `androidApiSecret` | string | mParticle Android API secret | +| `logLevel` | string | Log level: `none`, `error`, `warning`, `debug`, `verbose` | +| `environment` | string | Environment: `development`, `production`, `autoDetect` | +| `useEmptyIdentifyRequest` | boolean | Initialize with empty identify request (default: true) | +| `dataPlanId` | string | Data plan ID for validation | +| `dataPlanVersion` | number | Data plan version | +| `iosKits` | string[] | iOS kit pod names (e.g., `["mParticle-Rokt"]`) | +| `androidKits` | string[] | Android kit dependencies (e.g., `["android-rokt-kit"]`) | diff --git a/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt b/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt index 75b2ed9..b43c9e4 100644 --- a/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt +++ b/android/src/oldarch/java/com/mparticle/react/NativeMPRoktSpec.kt @@ -21,6 +21,12 @@ abstract class NativeMPRoktSpec( fontFilesMap: ReadableMap?, ) + abstract fun selectShoppableAds( + identifier: String, + attributes: ReadableMap?, + roktConfig: ReadableMap?, + ) + abstract fun purchaseFinalized( placementId: String, catalogItemId: String, diff --git a/package.json b/package.json index 67a3e98..b613e1c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "dependencies": {}, "peerDependencies": { "react": ">= 16.0.0-alpha.12", - "react-native": ">= 0.45.0", + "react-native": ">= 0.76.0", "@expo/config-plugins": ">=7.0.0" }, "peerDependenciesMeta": { diff --git a/plugin/src/withMParticleIOS.ts b/plugin/src/withMParticleIOS.ts index 69da5fa..2e43a3a 100644 --- a/plugin/src/withMParticleIOS.ts +++ b/plugin/src/withMParticleIOS.ts @@ -338,7 +338,12 @@ function addMParticleToObjcAppDelegate( * These are dependencies of mParticle kits that must also be dynamic frameworks */ const KIT_TRANSITIVE_DEPENDENCIES: Record = { - 'mParticle-Rokt': ['Rokt-Widget', 'RoktContracts', 'RoktUXHelper', 'DcuiSchema'], + 'mParticle-Rokt': [ + 'Rokt-Widget', + 'RoktContracts', + 'RoktUXHelper', + 'DcuiSchema', + ], // Add other kit dependencies here as needed // "mParticle-Amplitude": [], // "mParticle-Braze": [], @@ -348,7 +353,11 @@ const KIT_TRANSITIVE_DEPENDENCIES: Record = { * Get all pods that need dynamic framework linking */ function getDynamicFrameworkPods(iosKits?: string[]): string[] { - const pods = ['mParticle-Apple-SDK', 'mParticle-Apple-SDK-ObjC', 'mParticle-Apple-SDK-Swift']; + const pods = [ + 'mParticle-Apple-SDK', + 'mParticle-Apple-SDK-ObjC', + 'mParticle-Apple-SDK-Swift', + ]; if (iosKits) { for (const kit of iosKits) { diff --git a/sample/ios/Podfile b/sample/ios/Podfile index 412c086..9276719 100644 --- a/sample/ios/Podfile +++ b/sample/ios/Podfile @@ -5,12 +5,13 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, min_ios_version_supported +# react-native-mparticle.podspec requires iOS 15.6+; RN's min_ios_version_supported is still 15.1. +platform :ios, '15.6' prepare_react_native_project! pre_install do |installer| installer.pod_targets.each do |pod| - if pod.name == 'mParticle-Apple-SDK' || pod.name == 'mParticle-Rokt' || pod.name == 'Rokt-Widget' + if pod.name == 'mParticle-Apple-SDK' || pod.name == 'mParticle-Apple-SDK-ObjC' || pod.name == 'mParticle-Apple-SDK-Swift' || pod.name == 'mParticle-Rokt' || pod.name == 'Rokt-Widget' || pod.name == 'RoktContracts' || pod.name == 'RoktUXHelper' || pod.name == 'DcuiSchema' def pod.build_type; Pod::BuildType.new(:linkage => :dynamic, :packaging => :framework) end From 3df7fe16f0b2cec40b7208f481fd55c7387d5240 Mon Sep 17 00:00:00 2001 From: Brandon Stalnaker Date: Thu, 16 Apr 2026 08:38:57 -0400 Subject: [PATCH 5/5] fix trunk --- .github/workflows/pull-request.yml | 7 +++++++ .trunk/trunk.yaml | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 4c389aa..5d81174 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -25,6 +25,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: yarn + cache-dependency-path: yarn.lock + - name: Install node modules + run: yarn install --frozen-lockfile - name: Trunk Check uses: trunk-io/trunk-action@v1 diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index b28274b..bb89142 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -42,7 +42,14 @@ lint: - checkov@3.2.507 - dotenv-linter@3.3.0 # ESLint 9+ defaults to flat config only; this repo uses .eslintrc.js (ESLint 8 style). - - eslint@8.57.1 + # Trunk runs ESLint in an isolated env without the repo's node_modules; bundle the same + # plugins/parser as package.json so @typescript-eslint/* resolves (CI + local). + - eslint@8.57.1: + packages: + - '@typescript-eslint/eslint-plugin@5.62.0' + - '@typescript-eslint/parser@5.62.0' + - 'eslint-config-prettier@8.10.0' + - 'eslint-plugin-prettier@4.2.1' - git-diff-check - ktlint@0.43.2 # 0.48+ pulls string-width that requires Node 20+ for regex /v; Trunk's runner uses Node 18.