@datadog/mobile-react-native-session-replay 2.6.1 → 2.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +3 -3
  2. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/DdSessionReplayImplementation.kt +5 -5
  3. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupport.kt +5 -29
  4. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/ShadowNodeWrapper.kt +2 -2
  5. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactEditTextMapper.kt +3 -26
  6. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/mappers/ReactTextMapper.kt +3 -25
  7. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/FabricTextViewUtils.kt +74 -0
  8. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/text/LegacyTextViewUtils.kt +118 -0
  9. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolver.kt → utils/text/TextViewUtils.kt} +60 -96
  10. package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -2
  11. package/android/src/rn75/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
  12. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/extensions/LengthPercentageExt.kt +1 -1
  13. package/android/src/rn76/kotlin/com/datadog/reactnative/sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +1 -3
  14. package/android/src/rnlegacy/kotlin/com.datadog.reactnative.sessionreplay/utils/ReactViewBackgroundDrawableUtils.kt +2 -7
  15. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/ReactNativeSessionReplayExtensionSupportTest.kt +3 -17
  16. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/{ReactTextPropertiesResolverTest.kt → utils/text/TextViewUtilsTest.kt} +171 -38
  17. package/ios/Sources/DdSessionReplay.mm +4 -4
  18. package/ios/Sources/DdSessionReplayImplementation.swift +13 -3
  19. package/ios/Sources/RCTFabricWrapper.h +13 -0
  20. package/ios/Sources/RCTFabricWrapper.mm +120 -0
  21. package/ios/Sources/RCTTextPropertiesWrapper.h +23 -0
  22. package/ios/Sources/RCTTextPropertiesWrapper.mm +28 -0
  23. package/ios/Sources/RCTTextViewRecorder.swift +69 -49
  24. package/ios/Sources/RCTVersion.h +8 -0
  25. package/package.json +5 -3
  26. package/scripts/set-ios-rn-version.js +47 -0
  27. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/NoopTextPropertiesResolver.kt +0 -22
  28. package/android/src/main/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtils.kt +0 -40
  29. package/android/src/test/kotlin/com/datadog/reactnative/sessionreplay/utils/TextViewUtilsTest.kt +0 -109
package/README.md CHANGED
@@ -4,7 +4,7 @@ Mobile Session Replay expands visibility into your mobile applications by visual
4
4
 
5
5
  ## Setup
6
6
 
7
- **Note:** Make sure you’ve setup and initialized the [Datadog React Native SDK][1] with views instrumentation enabled.
7
+ **Note**: Make sure you’ve setup and initialized the [Datadog React Native SDK][1] with views instrumentation enabled.
8
8
 
9
9
  To install with NPM, run:
10
10
 
@@ -26,14 +26,14 @@ To enable Session Replay, import and call the `enable` method with your configur
26
26
  import { SessionReplay } from "@datadog/mobile-react-native-session-replay";
27
27
 
28
28
  SessionReplay.enable({
29
- replaySampleRate: sampleRate // The percentage of sampled replays, in the range 0.0 - 100.0 (Default: 100.0).
29
+ replaySampleRate: sampleRate, // The percentage of sampled replays, in the range 0.0 - 100.0 (Default: 100.0).
30
30
  textAndInputPrivacyLevel: TextAndInputPrivacyLevel.MASK_ALL, // Defines the way text and input (e.g text fields, checkboxes) should be masked (Default: `MASK_ALL`).
31
31
  imagePrivacyLevel: ImagePrivacyLevel.MASK_ALL, // Defines the way images should be masked (Default: `MASK_ALL`).
32
32
  touchPrivacyLevel: TouchPrivacyLevel.HIDE // Defines the way user touches (e.g tap) should be masked (Default: `HIDE`).
33
33
  });
34
34
  ```
35
35
 
36
- **Note:**: All configuration properties are optional and should be adjusted based on your application's needs.
36
+ **Note**: All configuration properties are optional and should be adjusted based on your application's needs.
37
37
 
38
38
  ## Start or stop the recording manually
39
39
 
@@ -6,15 +6,13 @@
6
6
 
7
7
  package com.datadog.reactnative.sessionreplay
8
8
 
9
+ import android.annotation.SuppressLint
9
10
  import com.datadog.android.api.feature.FeatureSdkCore
10
- import com.datadog.android.sessionreplay.ImagePrivacy
11
11
  import com.datadog.android.sessionreplay.SessionReplayConfiguration
12
- import com.datadog.android.sessionreplay.TextAndInputPrivacy
13
- import com.datadog.android.sessionreplay.TouchPrivacy
14
12
  import com.datadog.reactnative.DatadogSDKWrapperStorage
13
+ import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
15
14
  import com.facebook.react.bridge.Promise
16
15
  import com.facebook.react.bridge.ReactContext
17
- import java.util.Locale
18
16
 
19
17
  /**
20
18
  * The entry point to use Datadog's Session Replay feature.
@@ -33,6 +31,7 @@ class DdSessionReplayImplementation(
33
31
  * @param customEndpoint Custom server url for sending replay data.
34
32
  * @param startRecordingImmediately Whether the recording should start immediately when the feature is enabled.
35
33
  */
34
+ @SuppressLint("VisibleForTests")
36
35
  fun enable(
37
36
  replaySampleRate: Double,
38
37
  customEndpoint: String,
@@ -42,12 +41,13 @@ class DdSessionReplayImplementation(
42
41
  ) {
43
42
  val sdkCore = DatadogSDKWrapperStorage.getSdkCore() as FeatureSdkCore
44
43
  val logger = sdkCore.internalLogger
44
+ val textViewUtils = TextViewUtils.create(reactContext, logger)
45
45
  val configuration = SessionReplayConfiguration.Builder(replaySampleRate.toFloat())
46
46
  .startRecordingImmediately(startRecordingImmediately)
47
47
  .setImagePrivacy(privacySettings.imagePrivacyLevel)
48
48
  .setTouchPrivacy(privacySettings.touchPrivacyLevel)
49
49
  .setTextAndInputPrivacy(privacySettings.textAndInputPrivacyLevel)
50
- .addExtensionSupport(ReactNativeSessionReplayExtensionSupport(reactContext, logger))
50
+ .addExtensionSupport(ReactNativeSessionReplayExtensionSupport(textViewUtils))
51
51
 
52
52
  if (customEndpoint != "") {
53
53
  configuration.useCustomEndpoint(customEndpoint)
@@ -6,8 +6,6 @@
6
6
 
7
7
  package com.datadog.reactnative.sessionreplay
8
8
 
9
- import androidx.annotation.VisibleForTesting
10
- import com.datadog.android.api.InternalLogger
11
9
  import com.datadog.android.sessionreplay.ExtensionSupport
12
10
  import com.datadog.android.sessionreplay.MapperTypeWrapper
13
11
  import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
@@ -16,47 +14,29 @@ import com.datadog.reactnative.sessionreplay.mappers.ReactEditTextMapper
16
14
  import com.datadog.reactnative.sessionreplay.mappers.ReactNativeImageViewMapper
17
15
  import com.datadog.reactnative.sessionreplay.mappers.ReactTextMapper
18
16
  import com.datadog.reactnative.sessionreplay.mappers.ReactViewGroupMapper
19
- import com.facebook.react.bridge.ReactContext
20
- import com.facebook.react.uimanager.UIManagerModule
17
+ import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
21
18
  import com.facebook.react.views.image.ReactImageView
22
19
  import com.facebook.react.views.text.ReactTextView
23
20
  import com.facebook.react.views.textinput.ReactEditText
24
21
  import com.facebook.react.views.view.ReactViewGroup
25
22
 
23
+
26
24
  internal class ReactNativeSessionReplayExtensionSupport(
27
- private val reactContext: ReactContext,
28
- private val logger: InternalLogger
25
+ private val textViewUtils: TextViewUtils
29
26
  ) : ExtensionSupport {
30
27
  override fun name(): String {
31
28
  return ReactNativeSessionReplayExtensionSupport::class.java.simpleName
32
29
  }
33
30
 
34
31
  override fun getCustomViewMappers(): List<MapperTypeWrapper<*>> {
35
- val uiManagerModule = getUiManagerModule()
36
-
37
32
  return listOf(
38
33
  MapperTypeWrapper(ReactImageView::class.java, ReactNativeImageViewMapper()),
39
34
  MapperTypeWrapper(ReactViewGroup::class.java, ReactViewGroupMapper()),
40
- MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(reactContext, uiManagerModule)),
41
- MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(reactContext, uiManagerModule)),
35
+ MapperTypeWrapper(ReactTextView::class.java, ReactTextMapper(textViewUtils)),
36
+ MapperTypeWrapper(ReactEditText::class.java, ReactEditTextMapper(textViewUtils)),
42
37
  )
43
38
  }
44
39
 
45
- @VisibleForTesting
46
- internal fun getUiManagerModule(): UIManagerModule? {
47
- return try {
48
- reactContext.getNativeModule(UIManagerModule::class.java)
49
- } catch (e: IllegalStateException) {
50
- logger.log(
51
- level = InternalLogger.Level.WARN,
52
- targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
53
- messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
54
- throwable = e
55
- )
56
- return null
57
- }
58
- }
59
-
60
40
  override fun getOptionSelectorDetectors(): List<OptionSelectorDetector> {
61
41
  return listOf()
62
42
  }
@@ -64,8 +44,4 @@ internal class ReactNativeSessionReplayExtensionSupport(
64
44
  override fun getCustomDrawableMapper(): List<DrawableToColorMapper> {
65
45
  return emptyList()
66
46
  }
67
-
68
- internal companion object {
69
- internal const val RESOLVE_UIMANAGERMODULE_ERROR = "Unable to resolve UIManagerModule"
70
- }
71
47
  }
@@ -33,7 +33,7 @@ internal class ShadowNodeWrapper(
33
33
  internal companion object {
34
34
  internal fun getShadowNodeWrapper(
35
35
  reactContext: ReactContext,
36
- uiManagerModule: UIManagerModule,
36
+ uiManagerModule: UIManagerModule?,
37
37
  reflectionUtils: ReflectionUtils,
38
38
  viewId: Int
39
39
  ): ShadowNodeWrapper? {
@@ -41,7 +41,7 @@ internal class ShadowNodeWrapper(
41
41
  var target: ReactShadowNode<out ReactShadowNode<*>>? = null
42
42
 
43
43
  val shadowNodeRunnable = Runnable {
44
- val node = resolveShadowNode(reflectionUtils, uiManagerModule, viewId)
44
+ val node = uiManagerModule?.let { resolveShadowNode(reflectionUtils, it, viewId) }
45
45
  if (node != null) {
46
46
  target = node
47
47
  }
@@ -19,19 +19,11 @@ import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
19
19
  import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
20
20
  import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
21
21
  import com.datadog.android.sessionreplay.utils.GlobalBounds
22
- import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
23
- import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
24
- import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
25
- import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
26
- import com.facebook.react.bridge.ReactContext
27
- import com.facebook.react.uimanager.UIManagerModule
28
- import com.facebook.react.views.image.ReactImageView
22
+ import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
29
23
  import com.facebook.react.views.textinput.ReactEditText
30
24
 
31
25
  internal class ReactEditTextMapper(
32
- private val reactTextPropertiesResolver: TextPropertiesResolver =
33
- NoopTextPropertiesResolver(),
34
- private val textViewUtils: TextViewUtils = TextViewUtils(),
26
+ private val textViewUtils: TextViewUtils
35
27
  ) : BaseAsyncBackgroundWireframeMapper<ReactEditText>(
36
28
  viewIdentifierResolver = DefaultViewIdentifierResolver,
37
29
  colorStringFormatter = DefaultColorStringFormatter,
@@ -47,20 +39,6 @@ internal class ReactEditTextMapper(
47
39
  drawableToColorMapper = drawableToColorMapper,
48
40
  )
49
41
 
50
- internal constructor(
51
- reactContext: ReactContext,
52
- uiManagerModule: UIManagerModule?
53
- ) : this(
54
- reactTextPropertiesResolver = if (uiManagerModule == null) {
55
- NoopTextPropertiesResolver()
56
- } else {
57
- ReactTextPropertiesResolver(
58
- reactContext = reactContext,
59
- uiManagerModule = uiManagerModule
60
- )
61
- }
62
- )
63
-
64
42
  override fun map(
65
43
  view: ReactEditText,
66
44
  mappingContext: MappingContext,
@@ -89,7 +67,6 @@ internal class ReactEditTextMapper(
89
67
  wireframes = backgroundWireframes,
90
68
  view = view,
91
69
  mappingContext = mappingContext,
92
- reactTextPropertiesResolver = reactTextPropertiesResolver
93
70
  )
94
71
  }
95
72
 
@@ -102,7 +79,7 @@ internal class ReactEditTextMapper(
102
79
  mappingContext: MappingContext,
103
80
  asyncJobStatusCallback: AsyncJobStatusCallback
104
81
  ): MobileSegment.Wireframe? {
105
- if (view !is ReactImageView) {
82
+ if (view !is ReactEditText) {
106
83
  return super.resolveBackgroundAsImageWireframe(
107
84
  view,
108
85
  bounds,
@@ -8,7 +8,6 @@ package com.datadog.reactnative.sessionreplay.mappers
8
8
 
9
9
  import android.widget.TextView
10
10
  import com.datadog.android.api.InternalLogger
11
- import com.datadog.android.sessionreplay.SessionReplayPrivacy
12
11
  import com.datadog.android.sessionreplay.model.MobileSegment
13
12
  import com.datadog.android.sessionreplay.recorder.MappingContext
14
13
  import com.datadog.android.sessionreplay.recorder.mapper.TextViewMapper
@@ -17,17 +16,10 @@ import com.datadog.android.sessionreplay.utils.DefaultColorStringFormatter
17
16
  import com.datadog.android.sessionreplay.utils.DefaultViewBoundsResolver
18
17
  import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
19
18
  import com.datadog.android.sessionreplay.utils.DrawableToColorMapper
20
- import com.datadog.reactnative.sessionreplay.NoopTextPropertiesResolver
21
- import com.datadog.reactnative.sessionreplay.ReactTextPropertiesResolver
22
- import com.datadog.reactnative.sessionreplay.TextPropertiesResolver
23
- import com.datadog.reactnative.sessionreplay.utils.TextViewUtils
24
- import com.facebook.react.bridge.ReactContext
25
- import com.facebook.react.uimanager.UIManagerModule
19
+ import com.datadog.reactnative.sessionreplay.utils.text.TextViewUtils
26
20
 
27
21
  internal class ReactTextMapper(
28
- private val reactTextPropertiesResolver: TextPropertiesResolver =
29
- NoopTextPropertiesResolver(),
30
- private val textViewUtils: TextViewUtils = TextViewUtils(),
22
+ private val textViewUtils: TextViewUtils
31
23
  ): TextViewMapper<TextView>(
32
24
  viewIdentifierResolver = DefaultViewIdentifierResolver,
33
25
  colorStringFormatter = DefaultColorStringFormatter,
@@ -35,20 +27,6 @@ internal class ReactTextMapper(
35
27
  drawableToColorMapper = DrawableToColorMapper.getDefault()
36
28
  ) {
37
29
 
38
- internal constructor(
39
- reactContext: ReactContext,
40
- uiManagerModule: UIManagerModule?
41
- ): this(
42
- reactTextPropertiesResolver = if (uiManagerModule == null) {
43
- NoopTextPropertiesResolver()
44
- } else {
45
- ReactTextPropertiesResolver(
46
- reactContext = reactContext,
47
- uiManagerModule = uiManagerModule
48
- )
49
- }
50
- )
51
-
52
30
  override fun map(
53
31
  view: TextView,
54
32
  mappingContext: MappingContext,
@@ -56,11 +34,11 @@ internal class ReactTextMapper(
56
34
  internalLogger: InternalLogger
57
35
  ): List<MobileSegment.Wireframe> {
58
36
  val wireframes = super.map(view, mappingContext, asyncJobStatusCallback, internalLogger)
37
+
59
38
  return textViewUtils.mapTextViewToWireframes(
60
39
  wireframes = wireframes,
61
40
  view = view,
62
41
  mappingContext = mappingContext,
63
- reactTextPropertiesResolver = reactTextPropertiesResolver
64
42
  )
65
43
  }
66
44
  }
@@ -0,0 +1,74 @@
1
+ package com.datadog.reactnative.sessionreplay.utils.text
2
+
3
+ import android.text.Spannable
4
+ import android.text.style.ForegroundColorSpan
5
+ import android.view.View
6
+ import android.widget.TextView
7
+ import com.datadog.android.api.InternalLogger
8
+ import com.datadog.android.sessionreplay.model.MobileSegment
9
+ import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
10
+ import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
11
+ import com.facebook.react.bridge.ReactContext
12
+ import java.util.Locale
13
+
14
+ internal class FabricTextViewUtils(private val reactContext: ReactContext, private val logger: InternalLogger, drawableUtils: DrawableUtils): TextViewUtils(reactContext, drawableUtils) {
15
+
16
+ override fun resolveTextStyle(
17
+ textWireframe: MobileSegment.Wireframe.TextWireframe,
18
+ pixelsDensity: Float,
19
+ view: TextView
20
+ ): MobileSegment.TextStyle {
21
+
22
+ val fontColor = getTextColor(view, textWireframe)
23
+ val fontSize = getFontSize(view, pixelsDensity)
24
+ val fontFamily = getFontFamily(textWireframe)
25
+
26
+ return MobileSegment.TextStyle(
27
+ family = fontFamily,
28
+ size = fontSize,
29
+ color = fontColor
30
+ )
31
+ }
32
+
33
+ private fun getTextColor(view: TextView, textWireframe: MobileSegment.Wireframe.TextWireframe): String {
34
+ val spanned = getFieldFromView(view, SPANNED_FIELD_NAME) as? Spannable
35
+ val spans = spanned?.getSpans(0, spanned.length, ForegroundColorSpan::class.java)
36
+ val fontColor = spans?.firstOrNull()?.foregroundColor?.let { formatAsRgba(it) } ?: textWireframe.textStyle.color
37
+
38
+ return fontColor
39
+ }
40
+
41
+ private fun getFontSize(view: TextView, pixelsDensity: Float): Long {
42
+ val fontSize = (view.textSize / pixelsDensity).toLong()
43
+ return fontSize
44
+ }
45
+
46
+ private fun getFontFamily(textWireframe: MobileSegment.Wireframe.TextWireframe): String {
47
+ val fontFamily = textWireframe.textStyle.family
48
+ return resolveFontFamily(fontFamily.lowercase(Locale.US))
49
+ }
50
+
51
+ internal fun getFieldFromView(view: View, value: String): Any? {
52
+ try {
53
+ val field = view.javaClass.getDeclaredField(value)
54
+ field.isAccessible = true
55
+ return field.get(view)
56
+ } catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
57
+ when (e) {
58
+ is NoSuchFieldException -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
59
+ is NullPointerException -> handleError(e, NULL_FABRICFIELD_ERROR)
60
+ else -> handleError(e, RESOLVE_FABRICFIELD_ERROR)
61
+ }
62
+ return null
63
+ }
64
+ }
65
+
66
+ private fun handleError(e: Exception, message: String) {
67
+ logger.log(
68
+ level = InternalLogger.Level.WARN,
69
+ targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
70
+ messageBuilder = { message },
71
+ throwable = e
72
+ )
73
+ }
74
+ }
@@ -0,0 +1,118 @@
1
+ package com.datadog.reactnative.sessionreplay.utils.text
2
+
3
+ import android.widget.TextView
4
+ import androidx.annotation.VisibleForTesting
5
+ import com.datadog.android.api.InternalLogger
6
+ import com.datadog.android.internal.utils.densityNormalized
7
+ import com.datadog.android.sessionreplay.model.MobileSegment
8
+ import com.datadog.reactnative.sessionreplay.ShadowNodeWrapper
9
+ import com.datadog.reactnative.sessionreplay.utils.DrawableUtils
10
+ import com.datadog.reactnative.sessionreplay.utils.ReflectionUtils
11
+ import com.datadog.reactnative.sessionreplay.utils.formatAsRgba
12
+ import com.facebook.react.bridge.ReactContext
13
+ import com.facebook.react.uimanager.UIManagerModule
14
+ import com.facebook.react.views.text.TextAttributes
15
+ import java.util.Locale
16
+
17
+ internal class LegacyTextViewUtils(
18
+ private val reactContext: ReactContext,
19
+ private val logger: InternalLogger,
20
+ private val reflectionUtils: ReflectionUtils,
21
+ drawableUtils: DrawableUtils,
22
+ ) : TextViewUtils(reactContext, drawableUtils) {
23
+
24
+ private val uiManager: UIManagerModule? by lazy {
25
+ getUiManagerModule()
26
+ }
27
+
28
+ override fun resolveTextStyle(
29
+ textWireframe: MobileSegment.Wireframe.TextWireframe,
30
+ pixelsDensity: Float,
31
+ view: TextView,
32
+ ): MobileSegment.TextStyle? {
33
+ val shadowNodeWrapper: ShadowNodeWrapper =
34
+ ShadowNodeWrapper.getShadowNodeWrapper(
35
+ reactContext = reactContext,
36
+ uiManagerModule = uiManager,
37
+ reflectionUtils = reflectionUtils,
38
+ viewId = view.id,
39
+ ) ?: return null
40
+
41
+ val fontFamily = getFontFamily(shadowNodeWrapper) ?: textWireframe.textStyle.family
42
+
43
+ val fontSize = getFontSize(shadowNodeWrapper)?.densityNormalized(pixelsDensity) ?: textWireframe.textStyle.size
44
+
45
+ val fontColor = getTextColor(shadowNodeWrapper) ?: textWireframe.textStyle.color
46
+
47
+ return MobileSegment.TextStyle(
48
+ family = fontFamily,
49
+ size = fontSize,
50
+ color = fontColor,
51
+ )
52
+ }
53
+
54
+ private fun getTextColor(shadowNodeWrapper: ShadowNodeWrapper?): String? {
55
+ if (shadowNodeWrapper == null) return null
56
+
57
+ val isColorSet =
58
+ shadowNodeWrapper
59
+ .getDeclaredShadowNodeField(IS_COLOR_SET_FIELD_NAME) as Boolean?
60
+
61
+ if (isColorSet != true) {
62
+ // Improvement: get default text color if different from black
63
+ return "#000000FF"
64
+ }
65
+ val resolvedColor =
66
+ shadowNodeWrapper
67
+ .getDeclaredShadowNodeField(COLOR_FIELD_NAME) as Int?
68
+ if (resolvedColor != null) {
69
+ return formatAsRgba(resolvedColor)
70
+ }
71
+
72
+ return null
73
+ }
74
+
75
+ private fun getFontSize(shadowNodeWrapper: ShadowNodeWrapper?): Long? {
76
+ if (shadowNodeWrapper == null) return null
77
+
78
+ val textAttributes =
79
+ shadowNodeWrapper
80
+ .getDeclaredShadowNodeField(TEXT_ATTRIBUTES_FIELD_NAME) as? TextAttributes?
81
+ if (textAttributes != null) {
82
+ return textAttributes.effectiveFontSize.toLong()
83
+ }
84
+
85
+ return null
86
+ }
87
+
88
+ private fun getFontFamily(shadowNodeWrapper: ShadowNodeWrapper?): String? {
89
+ if (shadowNodeWrapper == null) return null
90
+
91
+ val fontFamily =
92
+ shadowNodeWrapper
93
+ .getDeclaredShadowNodeField(FONT_FAMILY_FIELD_NAME) as? String
94
+
95
+ if (fontFamily != null) {
96
+ return resolveFontFamily(fontFamily.lowercase(Locale.US))
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ // store to avoid calling it multiple times
103
+ @VisibleForTesting
104
+ internal fun getUiManagerModule(): UIManagerModule? {
105
+ return try {
106
+ reactContext.getNativeModule(UIManagerModule::class.java)
107
+ } catch (e: IllegalStateException) {
108
+ logger.log(
109
+ level = InternalLogger.Level.WARN,
110
+ targets = listOf(InternalLogger.Target.MAINTAINER, InternalLogger.Target.TELEMETRY),
111
+ messageBuilder = { RESOLVE_UIMANAGERMODULE_ERROR },
112
+ throwable = e,
113
+ )
114
+ return null
115
+ }
116
+ }
117
+ }
118
+