From 61272792a5c9698d4846e2f935b6b396c499012d Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 12 Aug 2026 11:14:34 +0200 Subject: [PATCH 1/3] fix: missing import in RN 0.87 --- .../ios/Sources/RCTFabricWrapper.mm | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm index 31141ba9c..9b69efab0 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm +++ b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm @@ -8,9 +8,10 @@ #if RCT_NEW_ARCH_ENABLED #import "RCTVersion.h" -#import "RCTParagraphComponentView.h" -#import "RCTConversions.h" #import "ParagraphProps.h" +#if RCT_VERSION_MINOR <= 73 +#import "RCTConversions.h" +#endif namespace rct = facebook::react; #endif @@ -18,29 +19,38 @@ @implementation RCTFabricWrapper /** * Extracts the text properties from the given UIView when the view is of type RCTParagraphComponentView, returns nil otherwise. + * + * This deliberately avoids importing RCTParagraphComponentView.h / RCTComponentViewProtocol.h: + * both live in the React-RCTFabric pod, which React Native's "prebuilt core" CocoaPods facade + * (RN >= 0.87) ships without public headers, aside from a narrow allowlist that doesn't cover + * either of them. The class and its `-props` accessor are resolved dynamically instead. */ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView *)view { #if RCT_NEW_ARCH_ENABLED - if (![view isKindOfClass:[RCTParagraphComponentView class]]) { + Class paragraphComponentViewClass = NSClassFromString(@"RCTParagraphComponentView"); + if (paragraphComponentViewClass == nil || ![view isKindOfClass:paragraphComponentViewClass]) { return nil; } - // Cast view to RCTParagraphComponentView - RCTParagraphComponentView* paragraphComponentView = (RCTParagraphComponentView *)view; - if (paragraphComponentView == nil) { + // `-props` is declared on RCTComponentViewProtocol (not on RCTParagraphComponentView + // itself) and returns a C++ shared_ptr, so it can't be reached through KVC — look it up + // and call it directly via its IMP instead of importing the protocol's header. + SEL propsSelector = NSSelectorFromString(@"props"); + if (![view respondsToSelector:propsSelector]) { return nil; } - - // Retrieve ParagraphProps from shared pointer - const rct::ParagraphProps* props = (rct::ParagraphProps*)paragraphComponentView.props.get(); + typedef facebook::react::Props::Shared (*PropsIMP)(id, SEL); + PropsIMP getProps = (PropsIMP)[view methodForSelector:propsSelector]; + facebook::react::Props::Shared sharedProps = getProps(view, propsSelector); + const rct::ParagraphProps* props = (rct::ParagraphProps*)sharedProps.get(); if (props == nil) { return nil; } // Extract Attributes RCTTextPropertiesWrapper* textPropertiesWrapper = [[RCTTextPropertiesWrapper alloc] init]; - textPropertiesWrapper.text = [RCTFabricWrapper getTextFromView:paragraphComponentView]; - textPropertiesWrapper.contentRect = paragraphComponentView.bounds; + textPropertiesWrapper.text = [RCTFabricWrapper getTextFromView:view]; + textPropertiesWrapper.contentRect = view.bounds; rct::TextAttributes textAttributes = props->textAttributes; textPropertiesWrapper.alignment = [RCTFabricWrapper getAlignmentFromAttributes:textAttributes]; @@ -54,12 +64,15 @@ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView } #if RCT_NEW_ARCH_ENABLED -+ (NSString* _Nonnull)getTextFromView:(RCTParagraphComponentView*)view { - if (view == nil || view.attributedText == nil) { ++ (NSString* _Nonnull)getTextFromView:(UIView*)view { + // `attributedText` is a plain Objective-C property on RCTParagraphComponentView, so it's + // safe to read through KVC without importing that class's header (see the note above). + NSAttributedString* attributedText = [view valueForKey:@"attributedText"]; + if (attributedText == nil) { return RCTTextPropertiesDefaultText; } - return view.attributedText.string; + return attributedText.string; } + (NSTextAlignment)getAlignmentFromAttributes:(rct::TextAttributes)textAttributes { From 4080e1ccbc972cfc2d68cd78df597b4229182c34 Mon Sep 17 00:00:00 2001 From: Marco Saia <164892343+marco-saia-datadog@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:41:34 +0200 Subject: [PATCH 2/3] fix(pr): null check before invoking getProps Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../ios/Sources/RCTFabricWrapper.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm index 9b69efab0..fb98b0fd3 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm +++ b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm @@ -41,6 +41,9 @@ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView } typedef facebook::react::Props::Shared (*PropsIMP)(id, SEL); PropsIMP getProps = (PropsIMP)[view methodForSelector:propsSelector]; + if (getProps == NULL) { + return nil; + } facebook::react::Props::Shared sharedProps = getProps(view, propsSelector); const rct::ParagraphProps* props = (rct::ParagraphProps*)sharedProps.get(); if (props == nil) { From 41c1fefa49a63f47bdad76d5c55d15a3dee792ec Mon Sep 17 00:00:00 2001 From: Marco Saia Date: Wed, 12 Aug 2026 12:16:26 +0200 Subject: [PATCH 3/3] chore(pr): cache dynamically resolved classes in RCTFabricWrapper --- .../ios/Sources/RCTFabricWrapper.mm | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm index fb98b0fd3..2c8f88bce 100644 --- a/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm +++ b/packages/react-native-session-replay/ios/Sources/RCTFabricWrapper.mm @@ -14,37 +14,81 @@ #endif namespace rct = facebook::react; +typedef facebook::react::Props::Shared (*RCTFabricWrapperPropsIMP)(id, SEL); #endif @implementation RCTFabricWrapper + +#if RCT_NEW_ARCH_ENABLED +/** + * `RCTParagraphComponentView` is compiled into the app (it's not a lazily-loaded plugin), so + * the class is resolvable as soon as this method can be called at all — it's safe to resolve + * it once and cache it rather than paying for an `NSClassFromString` lookup on every view + * checked during snapshotting. + */ ++ (nullable Class)paragraphComponentViewClass { + static Class sClass; + static dispatch_once_t sOnceToken; + dispatch_once(&sOnceToken, ^{ + sClass = NSClassFromString(@"RCTParagraphComponentView"); + }); + return sClass; +} + +/** + * `-props` is declared on RCTComponentViewProtocol (not on RCTParagraphComponentView itself), + * so its selector is looked up dynamically too rather than relying on the protocol's header. + */ ++ (SEL)propsSelector { + static SEL sSelector; + static dispatch_once_t sOnceToken; + dispatch_once(&sOnceToken, ^{ + sSelector = NSSelectorFromString(@"props"); + }); + return sSelector; +} + +/** + * `-props` returns a C++ shared_ptr, so it can't be reached through KVC — its IMP is looked up + * and cached once instead, since it's the same for every instance of the (fixed) class above. + */ ++ (nullable RCTFabricWrapperPropsIMP)propsIMP { + static RCTFabricWrapperPropsIMP sImp; + static dispatch_once_t sOnceToken; + dispatch_once(&sOnceToken, ^{ + Class paragraphComponentViewClass = [self paragraphComponentViewClass]; + SEL propsSelector = [self propsSelector]; + if (paragraphComponentViewClass != nil && [paragraphComponentViewClass instancesRespondToSelector:propsSelector]) { + sImp = (RCTFabricWrapperPropsIMP)[paragraphComponentViewClass instanceMethodForSelector:propsSelector]; + } + }); + return sImp; +} +#endif + /** * Extracts the text properties from the given UIView when the view is of type RCTParagraphComponentView, returns nil otherwise. * * This deliberately avoids importing RCTParagraphComponentView.h / RCTComponentViewProtocol.h: * both live in the React-RCTFabric pod, which React Native's "prebuilt core" CocoaPods facade * (RN >= 0.87) ships without public headers, aside from a narrow allowlist that doesn't cover - * either of them. The class and its `-props` accessor are resolved dynamically instead. + * either of them. The class and its `-props` accessor are resolved dynamically instead, and + * cached (see paragraphComponentViewClass / propsIMP above) since this runs on every view + * checked during snapshotting. */ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView *)view { #if RCT_NEW_ARCH_ENABLED - Class paragraphComponentViewClass = NSClassFromString(@"RCTParagraphComponentView"); + Class paragraphComponentViewClass = [RCTFabricWrapper paragraphComponentViewClass]; if (paragraphComponentViewClass == nil || ![view isKindOfClass:paragraphComponentViewClass]) { return nil; } - // `-props` is declared on RCTComponentViewProtocol (not on RCTParagraphComponentView - // itself) and returns a C++ shared_ptr, so it can't be reached through KVC — look it up - // and call it directly via its IMP instead of importing the protocol's header. - SEL propsSelector = NSSelectorFromString(@"props"); - if (![view respondsToSelector:propsSelector]) { - return nil; - } - typedef facebook::react::Props::Shared (*PropsIMP)(id, SEL); - PropsIMP getProps = (PropsIMP)[view methodForSelector:propsSelector]; + RCTFabricWrapperPropsIMP getProps = [RCTFabricWrapper propsIMP]; if (getProps == NULL) { return nil; } - facebook::react::Props::Shared sharedProps = getProps(view, propsSelector); + + facebook::react::Props::Shared sharedProps = getProps(view, [RCTFabricWrapper propsSelector]); const rct::ParagraphProps* props = (rct::ParagraphProps*)sharedProps.get(); if (props == nil) { return nil;