diff --git a/example-new-architecture/ios/Podfile.lock b/example-new-architecture/ios/Podfile.lock
index 692567406..19bb79785 100644
--- a/example-new-architecture/ios/Podfile.lock
+++ b/example-new-architecture/ios/Podfile.lock
@@ -1,23 +1,23 @@
 PODS:
   - boost (1.76.0)
   - CocoaAsyncSocket (7.6.5)
-  - DatadogCore (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogCrashReporting (2.4.0):
-    - DatadogInternal (= 2.4.0)
+  - DatadogCore (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogCrashReporting (2.5.0):
+    - DatadogInternal (= 2.5.0)
     - PLCrashReporter (~> 1.11.1)
-  - DatadogInternal (2.4.0)
-  - DatadogLogs (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogRUM (2.4.0):
-    - DatadogInternal (= 2.4.0)
+  - DatadogInternal (2.5.0)
+  - DatadogLogs (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogRUM (2.5.0):
+    - DatadogInternal (= 2.5.0)
   - DatadogSDKReactNative (2.0.1):
-    - DatadogCore (~> 2.4.0)
-    - DatadogCrashReporting (~> 2.4.0)
-    - DatadogLogs (~> 2.4.0)
-    - DatadogRUM (~> 2.4.0)
-    - DatadogTrace (~> 2.4.0)
-    - DatadogWebViewTracking (~> 2.4.0)
+    - DatadogCore (~> 2.5.0)
+    - DatadogCrashReporting (~> 2.5.0)
+    - DatadogLogs (~> 2.5.0)
+    - DatadogRUM (~> 2.5.0)
+    - DatadogTrace (~> 2.5.0)
+    - DatadogWebViewTracking (~> 2.5.0)
     - RCT-Folly (= 2021.07.22.00)
     - RCTRequired
     - RCTTypeSafety
@@ -26,10 +26,10 @@ PODS:
     - React-RCTFabric
     - ReactCommon/turbomodule/bridging
     - ReactCommon/turbomodule/core
-  - DatadogTrace (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogWebViewTracking (2.4.0):
-    - DatadogInternal (= 2.4.0)
+  - DatadogTrace (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogWebViewTracking (2.5.0):
+    - DatadogInternal (= 2.5.0)
   - DoubleConversion (1.1.6)
   - FBLazyVector (0.71.10)
   - FBReactNativeSpec (0.71.10):
@@ -959,14 +959,14 @@ EXTERNAL SOURCES:
 SPEC CHECKSUMS:
   boost: 57d2868c099736d80fcd648bf211b4431e51a558
   CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
-  DatadogCore: 67ce2f8cd2f58cc90bdee5486d847b49e4b31b0b
-  DatadogCrashReporting: 0d4bd014714946be77a1ed2d34897036892f2b7b
-  DatadogInternal: 5789bca7a0284b20655ba2a79738ac7d0cd56e70
-  DatadogLogs: 3b8c8778c32b780f916c2894b9d2c53bbf590803
-  DatadogRUM: 4207d091be536b888719969a7ca078e2c830819a
-  DatadogSDKReactNative: 9c95bed4eef14236ad675d88d6d3605a41f5d7be
-  DatadogTrace: ec75b1da1dcf9f5b574481a3773b296e0e1fda38
-  DatadogWebViewTracking: e7a5841f001f488fc0240d7cba1a984ab6c86e9a
+  DatadogCore: a152fbcc24ea1a6b937c9844b1c1d5b86f0a375e
+  DatadogCrashReporting: 53b458152130de5505901e025e0dd031ce057f31
+  DatadogInternal: 96448807156495aa41a9f177b8c849a404618948
+  DatadogLogs: 2e67adf2e2cccd84b880b42f52e56cd0b8c7ef82
+  DatadogRUM: d807827ad24ae6c738867e853df38c6cb2bb555b
+  DatadogSDKReactNative: 0b659c7de3043d16465ae3fd6e2d418a5b62261c
+  DatadogTrace: 703d7572acc1dcda474ab33b7db3d8d67984192c
+  DatadogWebViewTracking: 87c0c1c9de4da7bb4f9efb87595da6c3d333aba9
   DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
   FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5
   FBReactNativeSpec: 33a87f65f1a467d5f63d11d0cc106a10d3b0639d
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index b97b71251..30197e7e9 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,43 +1,44 @@
 PODS:
   - boost (1.76.0)
-  - DatadogCore (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogCrashReporting (2.4.0):
-    - DatadogInternal (= 2.4.0)
+  - DatadogCore (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogCrashReporting (2.5.0):
+    - DatadogInternal (= 2.5.0)
     - PLCrashReporter (~> 1.11.1)
-  - DatadogInternal (2.4.0)
-  - DatadogLogs (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogRUM (2.4.0):
-    - DatadogInternal (= 2.4.0)
+  - DatadogInternal (2.5.0)
+  - DatadogLogs (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogRUM (2.5.0):
+    - DatadogInternal (= 2.5.0)
   - DatadogSDKReactNative (2.0.1):
-    - DatadogCore (~> 2.4.0)
-    - DatadogCrashReporting (~> 2.4.0)
-    - DatadogLogs (~> 2.4.0)
-    - DatadogRUM (~> 2.4.0)
-    - DatadogTrace (~> 2.4.0)
-    - DatadogWebViewTracking (~> 2.4.0)
+    - DatadogCore (~> 2.5.0)
+    - DatadogCrashReporting (~> 2.5.0)
+    - DatadogLogs (~> 2.5.0)
+    - DatadogRUM (~> 2.5.0)
+    - DatadogTrace (~> 2.5.0)
+    - DatadogWebViewTracking (~> 2.5.0)
     - React-Core
   - DatadogSDKReactNative/Tests (2.0.1):
-    - DatadogCore (~> 2.4.0)
-    - DatadogCrashReporting (~> 2.4.0)
-    - DatadogLogs (~> 2.4.0)
-    - DatadogRUM (~> 2.4.0)
-    - DatadogTrace (~> 2.4.0)
-    - DatadogWebViewTracking (~> 2.4.0)
+    - DatadogCore (~> 2.5.0)
+    - DatadogCrashReporting (~> 2.5.0)
+    - DatadogLogs (~> 2.5.0)
+    - DatadogRUM (~> 2.5.0)
+    - DatadogTrace (~> 2.5.0)
+    - DatadogWebViewTracking (~> 2.5.0)
     - React-Core
   - DatadogSDKReactNativeSessionReplay (2.0.1):
-    - DatadogSessionReplay (~> 2.4.0)
+    - DatadogSessionReplay (~> 2.5.0)
     - React-Core
   - DatadogSDKReactNativeSessionReplay/Tests (2.0.1):
-    - DatadogSessionReplay (~> 2.4.0)
+    - DatadogSessionReplay (~> 2.5.0)
     - React-Core
-  - DatadogSessionReplay (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogTrace (2.4.0):
-    - DatadogInternal (= 2.4.0)
-  - DatadogWebViewTracking (2.4.0):
-    - DatadogInternal (= 2.4.0)
+    - React-RCTText
+  - DatadogSessionReplay (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogTrace (2.5.0):
+    - DatadogInternal (= 2.5.0)
+  - DatadogWebViewTracking (2.5.0):
+    - DatadogInternal (= 2.5.0)
   - DoubleConversion (1.1.6)
   - FBLazyVector (0.71.10)
   - FBReactNativeSpec (0.71.10):
@@ -569,16 +570,16 @@ EXTERNAL SOURCES:
 
 SPEC CHECKSUMS:
   boost: 57d2868c099736d80fcd648bf211b4431e51a558
-  DatadogCore: 67ce2f8cd2f58cc90bdee5486d847b49e4b31b0b
-  DatadogCrashReporting: 0d4bd014714946be77a1ed2d34897036892f2b7b
-  DatadogInternal: 5789bca7a0284b20655ba2a79738ac7d0cd56e70
-  DatadogLogs: 3b8c8778c32b780f916c2894b9d2c53bbf590803
-  DatadogRUM: 4207d091be536b888719969a7ca078e2c830819a
-  DatadogSDKReactNative: b4d2c0926219c7fa0c7fdb3b9faf963b0c37bc4f
-  DatadogSDKReactNativeSessionReplay: c320633e2dc1f8d6a8656ff60ca9a3d9cf305035
-  DatadogSessionReplay: 8d17ac983669b62e3dd9159cf4a04e98fcf9abeb
-  DatadogTrace: ec75b1da1dcf9f5b574481a3773b296e0e1fda38
-  DatadogWebViewTracking: e7a5841f001f488fc0240d7cba1a984ab6c86e9a
+  DatadogCore: a152fbcc24ea1a6b937c9844b1c1d5b86f0a375e
+  DatadogCrashReporting: 53b458152130de5505901e025e0dd031ce057f31
+  DatadogInternal: 96448807156495aa41a9f177b8c849a404618948
+  DatadogLogs: 2e67adf2e2cccd84b880b42f52e56cd0b8c7ef82
+  DatadogRUM: d807827ad24ae6c738867e853df38c6cb2bb555b
+  DatadogSDKReactNative: 3bdadcd1ad69e2fb9fd53b823dddaa8d503edf6f
+  DatadogSDKReactNativeSessionReplay: 08001fa73bc35a9acb783cd4c4428cc3d22fdeb9
+  DatadogSessionReplay: e264895cd8093c22408f518e0c736f6c3ca6882c
+  DatadogTrace: 703d7572acc1dcda474ab33b7db3d8d67984192c
+  DatadogWebViewTracking: 87c0c1c9de4da7bb4f9efb87595da6c3d333aba9
   DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
   FBLazyVector: ddb55c55295ea51ed98aa7e2e08add2f826309d5
   FBReactNativeSpec: 90fc1a90b4b7a171e0a7c20ea426c1bf6ce4399c
diff --git a/example/ios/PodfileForTests b/example/ios/PodfileForSRTests
similarity index 74%
rename from example/ios/PodfileForTests
rename to example/ios/PodfileForSRTests
index eaa582754..64a08d0c0 100644
--- a/example/ios/PodfileForTests
+++ b/example/ios/PodfileForSRTests
@@ -21,8 +21,23 @@ if linkage != nil
   use_frameworks! :linkage => linkage.to_sym
 end
 
+use_modular_headers!
+
 target 'ddSdkReactnativeExample' do
+  native_ios_sdk_path = ENV['DD_NATIVE_IOS_SDK_PATH']
+  if (native_ios_sdk_path) then
+    pod 'DatadogCore', :path => "#{native_ios_sdk_path}/DatadogCore.podspec"
+    pod 'DatadogLogs', :path => "#{native_ios_sdk_path}/DatadogLogs.podspec"
+    pod 'DatadogTrace', :path => "#{native_ios_sdk_path}/DatadogTrace.podspec"
+    pod 'DatadogInternal', :path => "#{native_ios_sdk_path}/DatadogInternal.podspec"
+    pod 'DatadogRUM', :path => "#{native_ios_sdk_path}/DatadogRUM.podspec"
+    pod 'DatadogCrashReporting', :path => "#{native_ios_sdk_path}/DatadogCrashReporting.podspec"
+    pod 'DatadogWebViewTracking', :path => "#{native_ios_sdk_path}/DatadogWebViewTracking.podspec"
+    pod 'DatadogSessionReplay', :path => "#{native_ios_sdk_path}/DatadogSessionReplay.podspec"
+  end
+
   pod 'DatadogSDKReactNative', :path => '../../packages/core/DatadogSDKReactNative.podspec', :testspecs => ['Tests']
+  pod 'DatadogSDKReactNativeSessionReplay', :path => '../../packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec', :testspecs => ['Tests']
 
   config = use_native_modules!
 
diff --git a/packages/core/DatadogSDKReactNative.podspec b/packages/core/DatadogSDKReactNative.podspec
index 840b9946e..c71a9f671 100644
--- a/packages/core/DatadogSDKReactNative.podspec
+++ b/packages/core/DatadogSDKReactNative.podspec
@@ -19,12 +19,12 @@ Pod::Spec.new do |s|
   s.dependency "React-Core"
 
   # /!\ Remember to keep the versions in sync with DatadogSDKReactNativeSessionReplay.podspec
-  s.dependency 'DatadogCore', '~> 2.4.0'
-  s.dependency 'DatadogLogs', '~> 2.4.0'
-  s.dependency 'DatadogTrace', '~> 2.4.0'
-  s.dependency 'DatadogRUM', '~> 2.4.0'
-  s.dependency 'DatadogCrashReporting', '~> 2.4.0'
-  s.dependency 'DatadogWebViewTracking', '~> 2.4.0'
+  s.dependency 'DatadogCore', '~> 2.5.0'
+  s.dependency 'DatadogLogs', '~> 2.5.0'
+  s.dependency 'DatadogTrace', '~> 2.5.0'
+  s.dependency 'DatadogRUM', '~> 2.5.0'
+  s.dependency 'DatadogCrashReporting', '~> 2.5.0'
+  s.dependency 'DatadogWebViewTracking', '~> 2.5.0'
   
   s.test_spec 'Tests' do |test_spec|
     test_spec.source_files = 'ios/Tests/*.swift'
diff --git a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec
index 7312d109e..1d2cd1802 100644
--- a/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec
+++ b/packages/react-native-session-replay/DatadogSDKReactNativeSessionReplay.podspec
@@ -19,9 +19,10 @@ Pod::Spec.new do |s|
   s.dependency "React-Core"
 
   # /!\ Remember to keep the version in sync with DatadogSDKReactNative.podspec
-  s.dependency 'DatadogSessionReplay', '~> 2.4.0'
+  s.dependency 'DatadogSessionReplay', '~> 2.5.0'
   
   s.test_spec 'Tests' do |test_spec|
+    test_spec.dependency "React-RCTText"
     test_spec.source_files = 'ios/Tests/*.swift'
   end
 
diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm
index 9f3a79b34..a416d5413 100644
--- a/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm
+++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplay.mm
@@ -14,6 +14,7 @@
 
 @implementation DdSessionReplay
 
+@synthesize bridge = _bridge;
 RCT_EXPORT_MODULE()
 
 RCT_REMAP_METHOD(enable, withEnableReplaySampleRate:(double)replaySampleRate
@@ -36,7 +37,7 @@ @implementation DdSessionReplay
 - (DdSessionReplayImplementation*)ddSessionReplayImplementation
 {
     if (_ddSessionReplayImplementation == nil) {
-        _ddSessionReplayImplementation = [[DdSessionReplayImplementation alloc] init];
+        _ddSessionReplayImplementation = [[DdSessionReplayImplementation alloc] initWithBridge:_bridge];
     }
     return _ddSessionReplayImplementation;
 }
diff --git a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift
index 61515b9b7..8f0c92411 100644
--- a/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift
+++ b/packages/react-native-session-replay/ios/Sources/DdSessionReplayImplementation.swift
@@ -5,30 +5,40 @@
  */
 
 import Foundation
-import DatadogSessionReplay
+@_spi(Internal) import DatadogSessionReplay
 import DatadogInternal
+import React
 
 @objc
 public class DdSessionReplayImplementation: NSObject {
     private lazy var sessionReplay: SessionReplayProtocol = sessionReplayProvider()
     private let sessionReplayProvider: () -> SessionReplayProtocol
+    private let uiManager: RCTUIManager
     
-    internal init(_ sessionReplayProvider: @escaping () -> SessionReplayProtocol) {
+    internal init(sessionReplayProvider: @escaping () -> SessionReplayProtocol, uiManager: RCTUIManager) {
         self.sessionReplayProvider = sessionReplayProvider
+        self.uiManager = uiManager
     }
 
     @objc
-    public override convenience init() {
-        self.init({ NativeSessionReplay() })
+    public convenience init(bridge: RCTBridge) {
+        self.init(
+            sessionReplayProvider: { NativeSessionReplay() },
+            uiManager: bridge.uiManager
+        )
     }
 
     @objc
     public func enable(replaySampleRate: Double, defaultPrivacyLevel: String, resolve:RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
+        var sessionReplayConfiguration = SessionReplay.Configuration(
+            replaySampleRate: Float(replaySampleRate),
+            defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString)
+        )
+                    
+        sessionReplayConfiguration.setAdditionalNodeRecorders([RCTTextViewRecorder(uiManager: self.uiManager)])
+
         sessionReplay.enable(
-            with: SessionReplay.Configuration(
-                replaySampleRate: Float(replaySampleRate),
-                defaultPrivacyLevel: buildPrivacyLevel(privacyLevel: defaultPrivacyLevel as NSString)
-            )
+            with: sessionReplayConfiguration
         )
         resolve(nil)
     }
diff --git a/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift
new file mode 100644
index 000000000..5a6db8466
--- /dev/null
+++ b/packages/react-native-session-replay/ios/Sources/RCTTextViewRecorder.swift
@@ -0,0 +1,138 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2019-Present Datadog, Inc.
+ */
+
+import UIKit
+@_spi(Internal)
+import DatadogSessionReplay
+import React
+
+internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
+    internal var textObfuscator: (SessionReplayViewTreeRecordingContext) -> SessionReplayTextObfuscating = { context in
+        return context.recorder.privacy.staticTextObfuscator
+    }
+
+    internal var identifier = UUID()
+
+    internal let uiManager: RCTUIManager
+
+    internal init(uiManager: RCTUIManager) {
+        self.uiManager = uiManager
+    }
+
+    internal func extractTextFromSubViews(
+        subviews: [RCTShadowView]?
+    ) -> String? {
+        if let subviews = subviews {
+            return subviews.compactMap { subview in
+                if let sub = subview as? RCTRawTextShadowView {
+                    return sub.text
+                }
+                if let sub = subview as? RCTVirtualTextShadowView {
+                    // We recursively get all subviews for nested Text components
+                    return extractTextFromSubViews(subviews: sub.reactSubviews())
+                }
+                return nil
+            }.joined()
+        }
+        return nil
+    }
+
+    public func semantics(
+        of view: UIView,
+        with attributes: SessionReplayViewAttributes,
+        in context: SessionReplayViewTreeRecordingContext
+    ) -> SessionReplayNodeSemantics? {
+        guard let textView = view as? RCTTextView else {
+            return nil
+        }
+
+        var shadowView: RCTTextShadowView? = nil
+        let tag = textView.reactTag
+
+        RCTGetUIManagerQueue().sync {
+            shadowView = uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView
+        }
+
+        if let shadow = shadowView {
+            // TODO: RUM-2173 check performance is ok
+            let text = extractTextFromSubViews(
+                subviews: shadow.reactSubviews()
+            )
+
+            let builder = RCTTextViewWireframesBuilder(
+                wireframeID: context.ids.nodeID(view: textView, nodeRecorder: self),
+                attributes: attributes,
+                text: text,
+                textAlignment: shadow.textAttributes.alignment,
+                textColor: shadow.textAttributes.foregroundColor?.cgColor,
+                textObfuscator: textObfuscator(context),
+                font: shadow.textAttributes.effectiveFont(), // Custom fonts are currently not supported for iOS
+                contentRect: shadow.contentFrame
+            )
+            let node = SessionReplayNode(viewAttributes: attributes, wireframesBuilder: builder)
+            return SessionReplaySpecificElement(subtreeStrategy: .ignore, nodes: [node])
+        }
+        return SessionReplayInvisibleElement.constant
+    }
+}
+
+internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder {
+    let wireframeID: WireframeID
+    let attributes: SessionReplayViewAttributes
+    let text: String?
+    var textAlignment: NSTextAlignment
+    let textColor: CGColor?
+    let textObfuscator: SessionReplayTextObfuscating
+    let font: UIFont?
+    let contentRect: CGRect
+
+    public var wireframeRect: CGRect {
+        attributes.frame
+    }
+
+    let DEFAULT_FONT_COLOR = UIColor.black.cgColor
+
+    private var clip: SRContentClip {
+        let top = abs(contentRect.origin.y)
+        let left = abs(contentRect.origin.x)
+        let bottom = max(contentRect.height - attributes.frame.height - top, 0)
+        let right = max(contentRect.width - attributes.frame.width - left, 0)
+        return SRContentClip.create(
+            bottom: Int64(withNoOverflow: bottom),
+            left: Int64(withNoOverflow: left),
+            right: Int64(withNoOverflow: right),
+            top: Int64(withNoOverflow: top)
+        )
+    }
+
+    private var relativeIntersectedRect: CGRect {
+        return CGRect(
+            x: attributes.frame.origin.x - contentRect.origin.x,
+            y: attributes.frame.origin.y - contentRect.origin.y ,
+            width: max(contentRect.width, attributes.frame.width),
+            height: max(contentRect.height, attributes.frame.height)
+        )
+    }
+
+    public func buildWireframes(with builder: SessionReplayWireframesBuilder) -> [SRWireframe] {
+        return [
+            builder.createTextWireframe(
+                id: wireframeID,
+                frame: relativeIntersectedRect,
+                text: textObfuscator.mask(text: text ?? ""),
+                textAlignment: .init(systemTextAlignment: textAlignment, vertical: .center),
+                clip: clip,
+                textColor: textColor ?? DEFAULT_FONT_COLOR,
+                font: font,
+                borderColor: attributes.layerBorderColor,
+                borderWidth: attributes.layerBorderWidth,
+                backgroundColor: attributes.backgroundColor,
+                cornerRadius: attributes.layerCornerRadius,
+                opacity: attributes.alpha
+            )
+        ]
+    }
+}
diff --git a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift
index 0af63ffaf..7dbc93d15 100644
--- a/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift
+++ b/packages/react-native-session-replay/ios/Tests/DdSessionReplayTests.swift
@@ -7,6 +7,7 @@
 import XCTest
 @testable import DatadogSDKReactNativeSessionReplay
 import DatadogSessionReplay
+import React
 
 internal class DdSessionReplayTests: XCTestCase {
     private func mockResolve(args: Any?) {}
@@ -14,7 +15,8 @@ internal class DdSessionReplayTests: XCTestCase {
  
     func testEnablesSessionReplayWithZeroReplaySampleRate() {
         let sessionReplayMock = MockSessionReplay()
-        DdSessionReplayImplementation({ sessionReplayMock })
+        let uiManagerMock = MockUIManager()
+        DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock)
             .enable(replaySampleRate: 0, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject)
         
         XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 0.0, privacyLevel: .mask))
@@ -22,7 +24,8 @@ internal class DdSessionReplayTests: XCTestCase {
     
     func testEnablesSessionReplayWithMaskPrivacyLevel() {
         let sessionReplayMock = MockSessionReplay()
-        DdSessionReplayImplementation({ sessionReplayMock })
+        let uiManagerMock = MockUIManager()
+        DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock)
             .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK", resolve: mockResolve, reject: mockReject)
         
         XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask))
@@ -30,7 +33,8 @@ internal class DdSessionReplayTests: XCTestCase {
     
     func testEnablesSessionReplayWithMaskUserInputPrivacyLevel() {
         let sessionReplayMock = MockSessionReplay()
-        DdSessionReplayImplementation({ sessionReplayMock })
+        let uiManagerMock = MockUIManager()
+        DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock)
             .enable(replaySampleRate: 100, defaultPrivacyLevel: "MASK_USER_INPUT", resolve: mockResolve, reject: mockReject)
         
         XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .maskUserInput))
@@ -38,7 +42,8 @@ internal class DdSessionReplayTests: XCTestCase {
     
     func testEnablesSessionReplayWithAllowPrivacyLevel() {
         let sessionReplayMock = MockSessionReplay()
-        DdSessionReplayImplementation({ sessionReplayMock })
+        let uiManagerMock = MockUIManager()
+        DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock)
             .enable(replaySampleRate: 100, defaultPrivacyLevel: "ALLOW", resolve: mockResolve, reject: mockReject)
         
         XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .allow))
@@ -46,7 +51,8 @@ internal class DdSessionReplayTests: XCTestCase {
     
     func testEnablesSessionReplayWithBadPrivacyLevel() {
         let sessionReplayMock = MockSessionReplay()
-        DdSessionReplayImplementation({ sessionReplayMock })
+        let uiManagerMock = MockUIManager()
+        DdSessionReplayImplementation(sessionReplayProvider:{ sessionReplayMock }, uiManager: uiManagerMock)
             .enable(replaySampleRate: 100, defaultPrivacyLevel: "BAD_VALUE", resolve: mockResolve, reject: mockReject)
         
         XCTAssertEqual(sessionReplayMock.calledMethods.first, .enable(replaySampleRate: 100.0, privacyLevel: .mask))
@@ -69,3 +75,5 @@ private class MockSessionReplay: SessionReplayProtocol {
         )
     }
 }
+
+private class MockUIManager: RCTUIManager {}
diff --git a/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift
new file mode 100644
index 000000000..852fde475
--- /dev/null
+++ b/packages/react-native-session-replay/ios/Tests/RCTTextViewRecorderTests.swift
@@ -0,0 +1,182 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2019-2020 Datadog, Inc.
+ */
+
+import XCTest
+@testable import DatadogSDKReactNativeSessionReplay
+@_spi(Internal)
+@testable import DatadogSessionReplay
+import React
+
+internal class RCTTextViewRecorderTests: XCTestCase {
+    let mockAttributes = SessionReplayViewAttributes(
+        frame: CGRect(x: 0, y: 0, width: 100, height: 100),
+        backgroundColor: UIColor.white.cgColor,
+        layerBorderColor: UIColor.blue.cgColor,
+        layerBorderWidth: CGFloat(1.0),
+        layerCornerRadius: CGFloat(1.0),
+        alpha: CGFloat(1.0),
+        isHidden: false,
+        intrinsicContentSize: CGSize(width: 100.0, height: 100.0)
+    )
+    
+    let mockAllowContext = SessionReplayViewTreeRecordingContext(
+        recorder: .init(privacy: SessionReplayPrivacyLevel.allow, applicationID: "app_id", sessionID: "session_id", viewID: "view_id", viewServerTimeOffset: nil),
+        coordinateSpace: UIView(),
+        ids: .init(),
+        imageDataProvider: ImageDataProvider()
+    )
+    
+    var mockShadowView: RCTTextShadowView {
+        // The shadow view must be initialized with a bridge so that we can insert React Subviews into it.
+        let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none));
+        
+        let rawTextShadowView = RCTRawTextShadowView()
+        rawTextShadowView.text = "This is the test text."
+        shadowView.insertReactSubview(rawTextShadowView, at: 0)
+        
+        return shadowView
+    }
+    
+    var mockShadowViewNestedText: RCTTextShadowView {
+        // The shadow view must be initialized with a bridge so that we can insert React Subviews into it.
+        let shadowView: RCTTextShadowView = .init(bridge: MockRCTBridge(delegate: .none));
+        
+        let rawTextShadowView = RCTRawTextShadowView()
+        rawTextShadowView.text = "This is the "
+        shadowView.insertReactSubview(rawTextShadowView, at: 0)
+        
+        let virtualTextShadowView = RCTVirtualTextShadowView()
+        let nestedRawTextShadowView = RCTRawTextShadowView()
+        nestedRawTextShadowView.text = "nested test text."
+        virtualTextShadowView.insertReactSubview(nestedRawTextShadowView, at: 0)
+        shadowView.insertReactSubview(virtualTextShadowView, at: 1)
+        
+        return shadowView
+    }
+ 
+    func testReturnsNilIfViewIsNotRCTTextView() {
+        let viewMock = UIView()
+        let uiManagerMock = MockUIManager()
+        let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock)
+        
+        let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext)
+        
+        XCTAssertNil(result)
+    }
+    
+    func testReturnsInvisibleElementIfShadowViewIsNotFound() throws {
+        let reactTag = NSNumber(value: 44)
+        let uiManagerMock = MockUIManager()
+        let viewMock = RCTTextView()
+        viewMock.reactTag = reactTag
+        let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock)
+
+        let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext)
+
+        let element = try XCTUnwrap(result as? SessionReplayInvisibleElement)
+        XCTAssertEqual(element, SessionReplayInvisibleElement.constant)
+    }
+    
+    func testReturnsBuilderWithCorrectInformation() throws {
+        let reactTag = NSNumber(value: 44)
+        let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView)
+        let viewMock = RCTTextView()
+        viewMock.reactTag = reactTag
+        let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock)
+
+        let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext)
+
+        let element = try XCTUnwrap(result as? SessionReplaySpecificElement)
+        XCTAssertEqual(element.subtreeStrategy, .ignore)
+        XCTAssertEqual(element.nodes.count, 1)
+        let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe())
+        XCTAssertEqual(wireframe.text, "This is the test text.")
+    }
+    
+    func testReturnsBuilderWithCorrectInformationWhenNestedTextComponents() throws {
+        let reactTag = NSNumber(value: 44)
+        let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowViewNestedText)
+        let viewMock = RCTTextView()
+        viewMock.reactTag = reactTag
+        let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock)
+
+        let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockAllowContext)
+
+        let element = try XCTUnwrap(result as? SessionReplaySpecificElement)
+        XCTAssertEqual(element.subtreeStrategy, .ignore)
+        XCTAssertEqual(element.nodes.count, 1)
+        let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe())
+        XCTAssertEqual(wireframe.text, "This is the nested test text.")
+    }
+    
+    func testReturnsBuilderWithCorrectInformationWhenTextIsObfuscated() throws {
+        let mockMaskContext = SessionReplayViewTreeRecordingContext(
+            recorder: .init(privacy: SessionReplayPrivacyLevel.mask, applicationID: "app_id", sessionID: "session_id", viewID: "view_id", viewServerTimeOffset: nil),
+            coordinateSpace: UIView(),
+            ids: .init(),
+            imageDataProvider: ImageDataProvider()
+        )
+        let reactTag = NSNumber(value: 44)
+        let uiManagerMock = MockUIManager(reactTag: reactTag, shadowView: mockShadowView)
+        let viewMock = RCTTextView()
+        viewMock.reactTag = reactTag
+        let viewRecorder = RCTTextViewRecorder(uiManager: uiManagerMock)
+
+        let result = viewRecorder.semantics(of: viewMock, with: mockAttributes, in: mockMaskContext)
+
+        let element = try XCTUnwrap(result as? SessionReplaySpecificElement)
+        XCTAssertEqual(element.subtreeStrategy, .ignore)
+        XCTAssertEqual(element.nodes.count, 1)
+        let wireframe = try XCTUnwrap(element.nodes[0].wireframesBuilder.buildWireframes(with: .init())[0].getAsTextWireframe())
+        XCTAssertEqual(wireframe.text, "xxxx xx xxx xxxx xxxxx")
+    }
+}
+
+private class MockRCTTextView: RCTTextView {}
+
+private class MockUIManager: RCTUIManager {
+    /// Tag to be used in the test corresponding to a shadow view
+    var shadowViewTag: NSNumber? = nil
+    var shadowView: RCTTextShadowView? = nil
+    
+    convenience init(reactTag: NSNumber, shadowView: RCTTextShadowView?) {
+        self.init()
+        self.shadowViewTag = reactTag
+        self.shadowView = shadowView
+    }
+
+    internal override func shadowView(forReactTag: NSNumber) -> RCTShadowView? {
+        if (forReactTag == shadowViewTag) {
+            return shadowView
+        }
+        return nil
+    }
+    
+}
+
+extension SessionReplayInvisibleElement: Equatable {
+    public static func ==(lhs: SessionReplayInvisibleElement, rhs: SessionReplayInvisibleElement) -> Bool {
+        // If two elements are indeed InvisibleElement they're InvisibleElement.constant
+        return true
+    }
+}
+
+extension SRWireframe {
+    public func getAsTextWireframe() -> SRTextWireframe? {
+        if case .textWireframe(let value) = self {
+            return value
+        }
+        return nil
+    }
+}
+
+private class MockRCTBridge: RCTBridge {
+    /// We need to override this function that would otherwise try to setup
+    /// a real bridge and fail as we don't have a bundled JS.
+    override func setUp() {
+        // do nothing
+    }
+}