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
2 changes: 0 additions & 2 deletions ios/RNMParticle/RNMPRokt.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

#ifdef RCT_NEW_ARCH_ENABLED
#import <RNMParticle/RNMParticle.h>
#import <React/RCTBridge.h>

@interface RNMPRokt : NSObject<NativeMPRoktSpec>
@property (nonatomic, weak, nullable) RCTBridge *bridge;
#else

#import <React/RCTBridgeModule.h>
Expand Down
62 changes: 29 additions & 33 deletions ios/RNMParticle/RNMPRokt.mm
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTBridge.h>
#import <React/RCTLog.h>
#import <React/RCTUtils.h>
#import <os/log.h>
#import "RoktEventManager.h"

Expand Down Expand Up @@ -56,7 +56,8 @@ @interface RNMPRokt ()

@implementation RNMPRokt

@synthesize bridge = _bridge;
// Maps React tags to UIViews in both bridge and bridgeless modes, unlike bridge.uiManager.
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;

RCT_EXTERN void RCTRegisterModule(Class);

Expand All @@ -71,10 +72,9 @@ + (void)load {

- (dispatch_queue_t)methodQueue
{
BOOL bridgeNil = (self.bridge == nil);
BOOL uiManagerNil = (self.bridge.uiManager == nil);
_rokt_log(@"[mParticle-Rokt] methodQueue called, bridge %@, uiManager %@", bridgeNil ? @"nil" : @"non-nil", uiManagerNil ? @"nil" : @"non-nil");
return self.bridge.uiManager.methodQueue;
// selectPlacements mutates the placeholder view hierarchy, so SDK calls must run on
// the main thread. Matches Android's UiThreadUtil.runOnUiThread (MPRoktModule.kt).
return dispatch_get_main_queue();
}

- (void)setMethodQueue:(dispatch_queue_t)methodQueue
Expand Down Expand Up @@ -147,20 +147,11 @@ - (void)selectPlacements:(NSString *)identifer
[self ensureEventManager];
__weak __typeof__(self) weakSelf = self;

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");

if (bridgeNil || uiManagerNil) {
_rokt_log(@"[mParticle-Rokt] addUIBlock skipped: self.bridge%@ is nil. selectPlacements will not be called. This can occur in New Architecture bridgeless production builds.", bridgeNil ? @"" : @".uiManager");
} else {
_rokt_log(@"[mParticle-Rokt] queuing addUIBlock for identifier: %@", identifer);
}
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
// Replaces [self.bridge.uiManager addUIBlock:], which silently drops the call (no
// event emitted) when RCT_REMOVE_LEGACY_ARCH is set, React Native 0.84's default.
RCTExecuteOnMainQueue(^{
__strong __typeof__(weakSelf) strongSelf = weakSelf;
_rokt_log(@"[mParticle-Rokt] addUIBlock executing for identifier: %@, viewRegistry count: %lu", identifer, (unsigned long)viewRegistry.count);

NSMutableDictionary *nativePlaceholders = strongSelf ? [strongSelf getNativePlaceholders:placeholders viewRegistry:viewRegistry] : [NSMutableDictionary dictionary];
NSMutableDictionary *nativePlaceholders = strongSelf ? [strongSelf resolvePlaceholders:placeholders] : [NSMutableDictionary dictionary];

id mpInstance = [MParticle sharedInstance];
id roktKit = mpInstance ? [mpInstance rokt] : nil;
Expand All @@ -173,8 +164,7 @@ - (void)selectPlacements:(NSString *)identifer
onEvent:^(RoktEvent * _Nonnull event) {
[weakSelf.eventManager onRoktEvents:event viewName:identifer];
}];
}];
_rokt_log(@"[mParticle-Rokt] addUIBlock enqueued for identifier: %@", identifer);
});
}

#ifdef RCT_NEW_ARCH_ENABLED
Expand Down Expand Up @@ -328,22 +318,30 @@ - (RoktConfig *)buildRoktConfigFromDict:(NSDictionary<NSString *, id> *)configMa
return isConfigEmpty ? nil : [builder build];
}

- (NSMutableDictionary *)getNativePlaceholders:(NSDictionary *)placeholders viewRegistry:(NSDictionary<NSNumber *, UIView *> *)viewRegistry
// Main thread only — RCTViewRegistry reads the mounted view hierarchy.
- (NSMutableDictionary *)resolvePlaceholders:(NSDictionary *)placeholders
{
_rokt_log(@"[mParticle-Rokt] getNativePlaceholders: placeholders %lu, viewRegistry %lu", (unsigned long)placeholders.count, (unsigned long)viewRegistry.count);
_rokt_log(@"[mParticle-Rokt] resolvePlaceholders: %lu placeholder(s)", (unsigned long)placeholders.count);
NSMutableDictionary *nativePlaceholders = [[NSMutableDictionary alloc]initWithCapacity:placeholders.count];

for(id key in placeholders){
// The spec allows `number | null`; viewForReactTag: would throw on NSNull.
NSNumber *reactTag = [placeholders objectForKey:key];
if (![reactTag isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Invalid react tag for placeholder %@", key);
continue;
}

// nil fails isKindOfClass:, covering both "not mounted" and "wrong class".
UIView *view = [_viewRegistry_DEPRECATED viewForReactTag:reactTag];
#ifdef RCT_NEW_ARCH_ENABLED
RoktNativeLayoutComponentView *wrapperView = (RoktNativeLayoutComponentView *)viewRegistry[[placeholders objectForKey:key]];
if (!wrapperView || ![wrapperView isKindOfClass:[RoktNativeLayoutComponentView class]]) {
RCTLogError(@"Cannot find RoktNativeWidgetComponentView with tag #%@", key);
if (![view isKindOfClass:[RoktNativeLayoutComponentView class]]) {
RCTLogError(@"Cannot find RoktNativeLayoutComponentView for placeholder %@ (reactTag %@)", key, reactTag);
continue;
}
nativePlaceholders[key] = wrapperView.roktEmbeddedView;
nativePlaceholders[key] = ((RoktNativeLayoutComponentView *)view).roktEmbeddedView;
#else
RoktEmbeddedView *view = viewRegistry[[placeholders objectForKey:key]];
if (!view || ![view isKindOfClass:[RoktEmbeddedView class]]) {
if (![view isKindOfClass:[RoktEmbeddedView class]]) {
RCTLogError(@"Cannot find RoktEmbeddedView with tag #%@", key);
continue;
}
Expand All @@ -352,14 +350,12 @@ - (NSMutableDictionary *)getNativePlaceholders:(NSDictionary *)placeholders view
#endif // RCT_NEW_ARCH_ENABLED
}

_rokt_log(@"[mParticle-Rokt] getNativePlaceholders: resolved %lu native placeholder(s)", (unsigned long)nativePlaceholders.count);
_rokt_log(@"[mParticle-Rokt] resolvePlaceholders: resolved %lu native placeholder(s)", (unsigned long)nativePlaceholders.count);
return nativePlaceholders;
}

#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
self.bridge = params.instance.bridge;
_rokt_log(@"[mParticle-Rokt] getTurboModule: bridge set to %@", self.bridge == nil ? @"nil" : @"non-nil");
return std::make_shared<facebook::react::NativeMPRoktSpecJSI>(params);
}
#endif // RCT_NEW_ARCH_ENABLED
Expand Down
8 changes: 6 additions & 2 deletions sample/ios/MParticleSample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@

/* Begin PBXBuildFile section */
00E356F31AD99517003FC87E /* MParticleSampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* MParticleSampleTests.m */; };
B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */; };
0C80B921A6F3F58F76C31292 /* libPods-MParticleSample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-MParticleSample.a */; };
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
4B55574964776A1532DBA98C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
7699B88040F8A987B510C191 /* libPods-MParticleSample-MParticleSampleTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-MParticleSample-MParticleSampleTests.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */; };
B7C10E932E50AA1100000004 /* RNMPRoktPlaceholderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -32,7 +33,6 @@
00E356EE1AD99517003FC87E /* MParticleSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MParticleSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* MParticleSampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MParticleSampleTests.m; sourceTree = "<group>"; };
B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* MParticleSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MParticleSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = MParticleSample/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = MParticleSample/AppDelegate.mm; sourceTree = "<group>"; };
Expand All @@ -47,6 +47,8 @@
5DCACB8F33CDC322A6C60F78 /* libPods-MParticleSample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MParticleSample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = MParticleSample/LaunchScreen.storyboard; sourceTree = "<group>"; };
89C6BE57DB24E9ADA2F236DE /* Pods-MParticleSample-MParticleSampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MParticleSample-MParticleSampleTests.release.xcconfig"; path = "Target Support Files/Pods-MParticleSample-MParticleSampleTests/Pods-MParticleSample-MParticleSampleTests.release.xcconfig"; sourceTree = "<group>"; };
B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = "<group>"; };
B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNMPRoktPlaceholderTests.m; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -75,6 +77,7 @@
children = (
00E356F21AD99517003FC87E /* MParticleSampleTests.m */,
B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */,
B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */,
00E356F01AD99517003FC87E /* Supporting Files */,
);
path = MParticleSampleTests;
Expand Down Expand Up @@ -393,6 +396,7 @@
files = (
00E356F31AD99517003FC87E /* MParticleSampleTests.m in Sources */,
B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */,
B7C10E932E50AA1100000004 /* RNMPRoktPlaceholderTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
6 changes: 6 additions & 0 deletions sample/ios/MParticleSample/AppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
#import "mParticle.h"

@implementation AppDelegate
Expand All @@ -18,6 +19,11 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
{
self.moduleName = @"MParticleSample";
self.initialProps = @{};

// Required since React Native 0.76. Without it no third-party Fabric component is
// registered, so <RoktLayoutView> mounts as RCTUnimplementedViewComponentView and
// embedded placements resolve no placeholder view.
self.dependencyProvider = [RCTAppDependencyProvider new];

MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init];
networkOptions.pinningDisabled = true;
Expand Down
143 changes: 143 additions & 0 deletions sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#import <XCTest/XCTest.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>
#import "../../../ios/RNMParticle/RNMPRokt.h"

// Implemented in RNMPRokt.mm.
@interface RNMPRokt (PlaceholderTests)
- (NSMutableDictionary *)resolvePlaceholders:(NSDictionary *)placeholders;
@end

/**
* Guards how `-[RNMPRokt resolvePlaceholders:]` turns placeholder react tags into the
* embedded views handed to `MPRokt selectPlacements`.
*
* Resolution goes through `RCTViewRegistry` rather than the legacy
* `self.bridge.uiManager addUIBlock:` view registry, because the latter is a no-op method
* body when RCT_REMOVE_LEGACY_ARCH is defined (React Native 0.84's default) and a no-op
* when `self.bridge` is nil — either way selectPlacements was discarded with no event
* emitted. The registry is exercised for real here: the tests install a bridgeless
* component-view provider, the same hook RCTInstance wires to the surface presenter in
* production.
*
* Scope limit, deliberate: this test target statically links the react-native-mparticle
* pod a second time on top of the app it hosts, so `RoktNativeLayoutComponentView` and
* `RNMPRokt` each exist in two binaries and `isKindOfClass:` cannot match across them
* ("Class ... is implemented in both", i.e. the runtime's spurious-casting-failure
* warning). Asserting a mounted placeholder resolves all the way to its `RoktEmbeddedView`
* would therefore be testing the linkage, not the code. That step is verified by running
* an embedded placement in the sample app instead. What is covered below is
* binary-independent: that the module is wired to a working registry, and every branch
* that refuses to resolve a tag.
*/
@interface RNMPRoktPlaceholderTests : XCTestCase
@end

@implementation RNMPRoktPlaceholderTests {
RNMPRokt *_rokt;
// viewRegistry_DEPRECATED is a weak property (React Native retains the registry via
// RCTBridgeModuleDecorator for the instance's lifetime), so the test has to own it.
RCTViewRegistry *_viewRegistry;
NSMutableDictionary<NSNumber *, UIView *> *_views;
NSInteger _loggedErrorCount;
NSMutableArray<NSString *> *_loggedErrors;
RCTLogFunction _originalLogFunction;
}

- (void)setUp
{
[super setUp];
_rokt = [RNMPRokt new];
_views = [NSMutableDictionary new];

_viewRegistry = [RCTViewRegistry new];
__weak __typeof__(self) weakSelf = self;
[_viewRegistry setBridgelessComponentViewProvider:^UIView *(NSNumber *reactTag) {
__strong __typeof__(weakSelf) strongSelf = weakSelf;
return strongSelf ? strongSelf->_views[reactTag] : nil;
}];
_rokt.viewRegistry_DEPRECATED = _viewRegistry;

// Unresolvable placeholders are reported via RCTLogError. Capture instead of letting
// it surface as test noise, so the diagnostic itself can be asserted.
_loggedErrorCount = 0;
_loggedErrors = [NSMutableArray new];
_originalLogFunction = RCTGetLogFunction();
RCTSetLogFunction(^(RCTLogLevel level,
__unused RCTLogSource source,
__unused NSString *fileName,
__unused NSNumber *lineNumber,
NSString *message) {
__strong __typeof__(weakSelf) strongSelf = weakSelf;
if (strongSelf && level >= RCTLogLevelError) {
strongSelf->_loggedErrorCount++;
[strongSelf->_loggedErrors addObject:message ?: @""];
}
});
}

- (void)tearDown
{
RCTSetLogFunction(_originalLogFunction);
_rokt = nil;
_viewRegistry = nil;
_views = nil;
[super tearDown];
}

// Regression guard for the change itself: without `@synthesize viewRegistry_DEPRECATED`
// in RNMPRokt.mm the module has no way to reach a view, and every embedded placement
// silently resolves to nothing.
- (void)testModuleIsWiredToAViewRegistryThatResolvesMountedViews
{
UIView *mountedView = [[UIView alloc] init];
_views[@101] = mountedView;

XCTAssertNotNil(_rokt.viewRegistry_DEPRECATED);
XCTAssertEqualObjects([_rokt.viewRegistry_DEPRECATED viewForReactTag:@101], mountedView);
XCTAssertNil([_rokt.viewRegistry_DEPRECATED viewForReactTag:@999]);
}

- (void)testSkipsTagThatIsNotMounted
{
NSDictionary *resolved = [_rokt resolvePlaceholders:@{@"Location1" : @999}];

XCTAssertEqual(resolved.count, 0u);
XCTAssertEqual(_loggedErrorCount, 1, @"errors: %@", _loggedErrors);
}

- (void)testSkipsTagResolvingToUnexpectedViewClass
{
_views[@101] = [[UIView alloc] init];

NSDictionary *resolved = [_rokt resolvePlaceholders:@{@"Location1" : @101}];

XCTAssertEqual(resolved.count, 0u);
XCTAssertEqual(_loggedErrorCount, 1, @"errors: %@", _loggedErrors);
}

- (void)testSkipsNonNumericTagWithoutThrowing
{
// `placeholders?: {[key: string]: number | null}` in js/codegenSpecs/rokt/NativeMPRokt.ts.
// The old dictionary-subscript lookup tolerated NSNull; viewForReactTag: would throw
// on it, so resolvePlaceholders: has to reject non-numeric tags itself.
NSDictionary *resolved =
[_rokt resolvePlaceholders:@{@"Location1" : [NSNull null], @"Location2" : @"101"}];

XCTAssertEqual(resolved.count, 0u);
XCTAssertEqual(_loggedErrorCount, 2, @"errors: %@", _loggedErrors);
XCTAssertTrue([_loggedErrors.firstObject hasPrefix:@"Invalid react tag"],
@"errors: %@", _loggedErrors);
}

- (void)testEmptyPlaceholdersResolveToEmptyDictionary
{
// Overlay / bottom-sheet placements pass no placeholders at all, so this path must not
// depend on the view hierarchy in any way.
NSDictionary *resolved = [_rokt resolvePlaceholders:@{}];

XCTAssertEqual(resolved.count, 0u);
XCTAssertEqual(_loggedErrorCount, 0, @"errors: %@", _loggedErrors);
}

@end
Loading