From b07f775deacf64643305c4ec6048d73ca327b2f9 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Fri, 21 Aug 2026 14:22:53 -0400 Subject: [PATCH 1/2] fix(ios): resolve Rokt placeholders via RCTViewRegistry selectPlacements routed its only native call through [self.bridge.uiManager addUIBlock:]. That path is no longer viable on the New Architecture: - React Native 0.84 defaults RCT_REMOVE_LEGACY_ARCH=1, under which RCTUIManager's addUIBlock: compiles to an empty method body, so the block never runs. - In bridgeless mode self.bridge is an RCTBridgeProxy whose RCTUIManagerProxy logs "This method isn't implemented faithfully. Please migrate to RCTViewRegistry". - The existing nil-bridge check only logged; it had no early return, so the call was messaged to nil and discarded with no RoktEvent, no promise rejection and no diagnostic. Callers only saw a placement that never settled. - methodQueue returned self.bridge.uiManager.methodQueue, which is nil on the proxy, so React Native silently substituted a shared background queue. Resolve placeholders through RCTViewRegistry instead, which React Native populates in both bridge and bridgeless modes, and dispatch with RCTExecuteOnMainQueue. methodQueue now returns the main queue, matching Android's UiThreadUtil.runOnUiThread, and the bridge property is removed. Non-numeric react tags are rejected up front because the spec allows `number | null` and viewForReactTag: would throw on NSNull, where the old dictionary subscript returned nil. Also set dependencyProvider in the sample app. It has been required since React Native 0.76; without it RCTReactNativeFactory reports no third-party Fabric components, so RoktNativeLayout was never registered, mounted as RCTUnimplementedViewComponentView and embedded placements resolved no placeholder view. Co-Authored-By: Claude Opus 5 (1M context) --- ios/RNMParticle/RNMPRokt.h | 2 - ios/RNMParticle/RNMPRokt.mm | 60 ++++---- .../MParticleSample.xcodeproj/project.pbxproj | 8 +- sample/ios/MParticleSample/AppDelegate.mm | 6 + .../RNMPRoktPlaceholderTests.m | 143 ++++++++++++++++++ 5 files changed, 183 insertions(+), 36 deletions(-) create mode 100644 sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m diff --git a/ios/RNMParticle/RNMPRokt.h b/ios/RNMParticle/RNMPRokt.h index 8cbf140d..30f5c5d8 100644 --- a/ios/RNMParticle/RNMPRokt.h +++ b/ios/RNMParticle/RNMPRokt.h @@ -2,10 +2,8 @@ #ifdef RCT_NEW_ARCH_ENABLED #import -#import @interface RNMPRokt : NSObject -@property (nonatomic, weak, nullable) RCTBridge *bridge; #else #import diff --git a/ios/RNMParticle/RNMPRokt.mm b/ios/RNMParticle/RNMPRokt.mm index 123f37ba..d6e45478 100644 --- a/ios/RNMParticle/RNMPRokt.mm +++ b/ios/RNMParticle/RNMPRokt.mm @@ -19,8 +19,8 @@ #import #import #import -#import -#import +#import +#import #import #import "RoktEventManager.h" @@ -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); @@ -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 @@ -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 *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; @@ -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 @@ -328,22 +318,30 @@ - (RoktConfig *)buildRoktConfigFromDict:(NSDictionary *)configMa return isConfigEmpty ? nil : [builder build]; } -- (NSMutableDictionary *)getNativePlaceholders:(NSDictionary *)placeholders viewRegistry:(NSDictionary *)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); 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; } @@ -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)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(params); } #endif // RCT_NEW_ARCH_ENABLED diff --git a/sample/ios/MParticleSample.xcodeproj/project.pbxproj b/sample/ios/MParticleSample.xcodeproj/project.pbxproj index c97202ff..fd40637f 100644 --- a/sample/ios/MParticleSample.xcodeproj/project.pbxproj +++ b/sample/ios/MParticleSample.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* 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 */; }; @@ -16,6 +15,8 @@ 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 */ @@ -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 = ""; }; 00E356F21AD99517003FC87E /* MParticleSampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MParticleSampleTests.m; sourceTree = ""; }; - B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = ""; }; 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 = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = MParticleSample/AppDelegate.mm; sourceTree = ""; }; @@ -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 = ""; }; 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 = ""; }; + B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTConvertCommerceMappingTests.m; sourceTree = ""; }; + B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNMPRoktPlaceholderTests.m; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -75,6 +77,7 @@ children = ( 00E356F21AD99517003FC87E /* MParticleSampleTests.m */, B7C10E902E50AA1100000001 /* RCTConvertCommerceMappingTests.m */, + B7C10E922E50AA1100000003 /* RNMPRoktPlaceholderTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = MParticleSampleTests; @@ -393,6 +396,7 @@ files = ( 00E356F31AD99517003FC87E /* MParticleSampleTests.m in Sources */, B7C10E912E50AA1100000002 /* RCTConvertCommerceMappingTests.m in Sources */, + B7C10E932E50AA1100000004 /* RNMPRoktPlaceholderTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sample/ios/MParticleSample/AppDelegate.mm b/sample/ios/MParticleSample/AppDelegate.mm index 4070959d..58c70b75 100644 --- a/sample/ios/MParticleSample/AppDelegate.mm +++ b/sample/ios/MParticleSample/AppDelegate.mm @@ -10,6 +10,7 @@ #import "AppDelegate.h" #import +#import #import "mParticle.h" @implementation AppDelegate @@ -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 mounts as RCTUnimplementedViewComponentView and + // embedded placements resolve no placeholder view. + self.dependencyProvider = [RCTAppDependencyProvider new]; MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init]; networkOptions.pinningDisabled = true; diff --git a/sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m b/sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m new file mode 100644 index 00000000..e7d3d280 --- /dev/null +++ b/sample/ios/MParticleSampleTests/RNMPRoktPlaceholderTests.m @@ -0,0 +1,143 @@ +#import +#import +#import +#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 *_views; + NSInteger _loggedErrorCount; + NSMutableArray *_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 From 4126215cc93b636f89857b4748549318b7a71039 Mon Sep 17 00:00:00 2001 From: Thomson Thomas <125323226+thomson-t@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:33:05 -0400 Subject: [PATCH 2/2] Log update Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ios/RNMParticle/RNMPRokt.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/RNMParticle/RNMPRokt.mm b/ios/RNMParticle/RNMPRokt.mm index d6e45478..107a7b04 100644 --- a/ios/RNMParticle/RNMPRokt.mm +++ b/ios/RNMParticle/RNMPRokt.mm @@ -336,7 +336,7 @@ - (NSMutableDictionary *)resolvePlaceholders:(NSDictionary *)placeholders UIView *view = [_viewRegistry_DEPRECATED viewForReactTag:reactTag]; #ifdef RCT_NEW_ARCH_ENABLED if (![view isKindOfClass:[RoktNativeLayoutComponentView class]]) { - RCTLogError(@"Cannot find RoktNativeWidgetComponentView with tag #%@", key); + RCTLogError(@"Cannot find RoktNativeLayoutComponentView for placeholder %@ (reactTag %@)", key, reactTag); continue; } nativePlaceholders[key] = ((RoktNativeLayoutComponentView *)view).roktEmbeddedView;