Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
60 changes: 28 additions & 32 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]]) {
if (![view isKindOfClass:[RoktNativeLayoutComponentView class]]) {
RCTLogError(@"Cannot find RoktNativeWidgetComponentView with tag #%@", key);
Comment thread
Copilot marked this conversation as resolved.
Outdated
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