@usercentrics/react-native-sdk 2.25.1 โ†’ 2.26.2

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 (56) hide show
  1. package/README.md +7 -7
  2. package/android/build-legacy.gradle +1 -1
  3. package/android/build.gradle.kts +1 -1
  4. package/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt +102 -2
  5. package/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt +19 -1
  6. package/android/src/main/java/com/usercentrics/reactnative/extensions/BannerSettingsExtensions.kt +2 -1
  7. package/android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt +83 -0
  8. package/android/src/main/java/com/usercentrics/reactnative/extensions/UserDecisionExtensions.kt +2 -1
  9. package/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt +46 -2
  10. package/android/src/test/java/com/usercentrics/reactnative/extensions/NullabilityTest.kt +189 -0
  11. package/ios/Extensions/GppData+Dict.swift +45 -0
  12. package/ios/Extensions/GppSectionChangePayload+Dict.swift +10 -0
  13. package/ios/Extensions/UIImage+UsercentricsLogoDict.swift +31 -6
  14. package/ios/Extensions/UsercentricsCMPData+Dict.swift +8 -2
  15. package/ios/Manager/UsercentricsManager.swift +28 -3
  16. package/ios/RNUsercentricsModule.mm +11 -7
  17. package/ios/RNUsercentricsModule.swift +56 -15
  18. package/ios/RNUsercentricsModule.xcodeproj/project.pbxproj +2 -2
  19. package/ios/RNUsercentricsModuleSpec.h +13 -0
  20. package/lib/NativeUsercentrics.d.ts +8 -2
  21. package/lib/NativeUsercentrics.js.map +1 -1
  22. package/lib/Usercentrics.d.ts +8 -2
  23. package/lib/Usercentrics.js +23 -3
  24. package/lib/Usercentrics.js.map +1 -1
  25. package/lib/fabric/NativeUsercentricsModule.d.ts +7 -1
  26. package/lib/fabric/NativeUsercentricsModule.js.map +1 -1
  27. package/lib/models/CCPASettings.d.ts +3 -1
  28. package/lib/models/CCPASettings.js +3 -1
  29. package/lib/models/CCPASettings.js.map +1 -1
  30. package/lib/models/GppData.d.ts +6 -0
  31. package/lib/models/GppData.js +8 -0
  32. package/lib/models/GppData.js.map +1 -0
  33. package/lib/models/GppSectionChangePayload.d.ts +4 -0
  34. package/lib/models/GppSectionChangePayload.js +6 -0
  35. package/lib/models/GppSectionChangePayload.js.map +1 -0
  36. package/lib/models/TCF2Settings.d.ts +2 -2
  37. package/lib/models/TCF2Settings.js +2 -2
  38. package/lib/models/TCF2Settings.js.map +1 -1
  39. package/lib/models/UsercentricsSettings.d.ts +3 -1
  40. package/lib/models/UsercentricsSettings.js +3 -1
  41. package/lib/models/UsercentricsSettings.js.map +1 -1
  42. package/lib/models/index.d.ts +2 -0
  43. package/lib/models/index.js +2 -0
  44. package/lib/models/index.js.map +1 -1
  45. package/package.json +14 -8
  46. package/src/NativeUsercentrics.ts +10 -1
  47. package/src/Usercentrics.tsx +31 -3
  48. package/src/__tests__/index.test.ts +66 -1
  49. package/src/__tests__/mocks.ts +17 -1
  50. package/src/fabric/NativeUsercentricsModule.ts +8 -1
  51. package/src/models/CCPASettings.tsx +7 -1
  52. package/src/models/GppData.tsx +15 -0
  53. package/src/models/GppSectionChangePayload.tsx +7 -0
  54. package/src/models/TCF2Settings.tsx +3 -3
  55. package/src/models/UsercentricsSettings.tsx +6 -0
  56. package/src/models/index.tsx +2 -0
package/README.md CHANGED
@@ -61,9 +61,9 @@ In order to use and test the SDK, you will need a Usercentrics Account. Get [sta
61
61
 
62
62
  Continue to our [documentation](https://usercentrics.com/docs/apps/intro/) for a step by step integration guide.
63
63
 
64
- ## Example
64
+ ## Sample app
65
65
 
66
- See the [example](https://github.com/Usercentrics/react-native/tree/master/example) directory for a complete sample app using Usercentrics.
66
+ This repository includes a **`sample/`** app that demonstrates integrating the SDK. Use it as a reference and for local development.
67
67
 
68
68
  ## Quick Start
69
69
 
@@ -101,14 +101,14 @@ npm run ios
101
101
  ```sh
102
102
  npm install --legacy-peer-deps
103
103
  ```
104
- * From example folder:
104
+ * From `sample` folder:
105
105
  ```sh
106
106
  npm install --legacy-peer-deps
107
107
  ```
108
108
 
109
109
  ### iOS (macOS only)
110
110
 
111
- From example folder:
111
+ From `sample` folder:
112
112
  ```sh
113
113
  npx pod-install
114
114
  npx react-native run-ios
@@ -116,7 +116,7 @@ npx react-native run-ios
116
116
 
117
117
  ### Android
118
118
 
119
- Make sure you have Android SDK configured, then from example folder:
119
+ Make sure you have Android SDK configured, then from `sample` folder:
120
120
  ```sh
121
121
  npx react-native run-android
122
122
  ```
@@ -128,7 +128,7 @@ npx react-native run-android
128
128
  | `npm run auto-setup` | ๐Ÿค– Automatically install all required dependencies |
129
129
  | `npm run check-requirements` | ๐Ÿ” Check development environment requirements |
130
130
  | `npm run clean-all-caches` | ๐Ÿงน Clean all project and system caches |
131
- | `npm run install-dependencies` | ๐Ÿ“ฆ Install dependencies in root and example folders |
131
+ | `npm run install-dependencies` | ๐Ÿ“ฆ Install dependencies in root and `sample/` |
132
132
  | `npm run run-android` | ๐Ÿค– Kill conflicting ports and run Android app |
133
133
 
134
134
  ### ๐Ÿ”ง Troubleshooting
@@ -152,7 +152,7 @@ The project includes intelligent scripts that will:
152
152
 
153
153
  ## Consent Mediation
154
154
 
155
- You can enable and run the Consent Mediation example in this example by following the [documentation](https://usercentrics.com/docs/apps/features/consent-mediation/#enable-mediation).
155
+ You can enable and run the Consent Mediation flow in the sample app by following the [documentation](https://usercentrics.com/docs/apps/features/consent-mediation/#enable-mediation).
156
156
 
157
157
  ## Maintainers
158
158
  - [Usercentrics](https://github.com/Usercentrics)
@@ -1,4 +1,4 @@
1
- def usercentrics_version = "2.25.1"
1
+ def usercentrics_version = "2.26.2"
2
2
  version usercentrics_version
3
3
 
4
4
  buildscript {
@@ -1,4 +1,4 @@
1
- val usercentricsVersion = "2.25.1"
1
+ val usercentricsVersion = "2.26.2"
2
2
  val reactNativeVersion = "+"
3
3
 
4
4
  fun BooleanProperty(name: String): Boolean {
@@ -1,10 +1,14 @@
1
1
  package com.usercentrics.reactnative
2
2
 
3
3
  import com.facebook.react.bridge.*
4
+ import com.facebook.react.modules.core.DeviceEventManagerModule
5
+ import com.usercentrics.sdk.UsercentricsDisposableEvent
6
+ import com.usercentrics.sdk.UsercentricsEvent
4
7
  import com.usercentrics.reactnative.api.UsercentricsProxy
5
8
  import com.usercentrics.reactnative.extensions.*
6
9
  import com.usercentrics.sdk.UsercentricsAnalyticsEventType
7
10
  import com.usercentrics.sdk.models.settings.UsercentricsConsentType
11
+ import com.usercentrics.sdk.services.gpp.GppSectionChangePayload
8
12
  import com.usercentrics.sdk.services.tcf.TCFDecisionUILayer
9
13
 
10
14
  internal class RNUsercentricsModule(
@@ -12,6 +16,8 @@ internal class RNUsercentricsModule(
12
16
  private val usercentricsProxy: UsercentricsProxy,
13
17
  private val reactContextProvider: ReactContextProvider,
14
18
  ) : RNUsercentricsModuleSpec(reactContext) {
19
+ private var gppSectionChangeSubscription: UsercentricsDisposableEvent<GppSectionChangePayload>? = null
20
+ private var gppSectionChangeListenersCount = 0
15
21
 
16
22
  override fun getName() = NAME
17
23
 
@@ -79,6 +85,16 @@ internal class RNUsercentricsModule(
79
85
  promise.resolve(usercentricsProxy.instance.getABTestingVariant())
80
86
  }
81
87
 
88
+ @ReactMethod
89
+ override fun getDpsMetadata(templateId: String, promise: Promise) {
90
+ val metadata = usercentricsProxy.instance.getDpsMetadata(templateId)
91
+ if (metadata == null) {
92
+ promise.resolve(null)
93
+ } else {
94
+ promise.resolve(metadata.toWritableMap())
95
+ }
96
+ }
97
+
82
98
  @ReactMethod
83
99
  override fun getConsents(promise: Promise) {
84
100
  promise.resolve(usercentricsProxy.instance.getConsents().toWritableArray())
@@ -121,6 +137,28 @@ internal class RNUsercentricsModule(
121
137
  promise.resolve(usercentricsProxy.instance.getUSPData().serialize())
122
138
  }
123
139
 
140
+ @ReactMethod
141
+ override fun getGPPData(promise: Promise) {
142
+ promise.resolve(usercentricsProxy.instance.getGPPData().serializeGppData())
143
+ }
144
+
145
+ @ReactMethod
146
+ override fun getGPPString(promise: Promise) {
147
+ promise.resolve(usercentricsProxy.instance.getGPPString())
148
+ }
149
+
150
+ @ReactMethod
151
+ override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
152
+ if (!value.hasKey("value")) return
153
+ val parsedValue = if (value.getType("value") == ReadableType.Null) {
154
+ null
155
+ } else {
156
+ readableMapValueToAny(value)
157
+ }
158
+ val safeValue = parsedValue ?: org.json.JSONObject.NULL
159
+ usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, safeValue)
160
+ }
161
+
124
162
  @ReactMethod
125
163
  override fun changeLanguage(language: String, promise: Promise) {
126
164
  usercentricsProxy.instance.changeLanguage(language, {
@@ -149,10 +187,10 @@ internal class RNUsercentricsModule(
149
187
  }
150
188
 
151
189
  @ReactMethod
152
- override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
190
+ override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, unsavedVendorLIDecisions: ReadableArray, promise: Promise) {
153
191
  promise.resolve(
154
192
  usercentricsProxy.instance.denyAllForTCF(
155
- TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
193
+ TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap(), unsavedVendorLIDecisions.deserializePurposeLIDecisionsMap()
156
194
  ).toWritableArray()
157
195
  )
158
196
  }
@@ -216,11 +254,73 @@ internal class RNUsercentricsModule(
216
254
  })
217
255
  }
218
256
 
257
+ @ReactMethod
258
+ override fun addListener(eventName: String) {
259
+ if (eventName != ON_GPP_SECTION_CHANGE_EVENT) return
260
+
261
+ gppSectionChangeListenersCount++
262
+ if (gppSectionChangeSubscription != null) return
263
+
264
+ gppSectionChangeSubscription = UsercentricsEvent.onGppSectionChange { payload ->
265
+ emitEvent(ON_GPP_SECTION_CHANGE_EVENT, payload.serializeGppPayload())
266
+ }
267
+ }
268
+
269
+ @ReactMethod
270
+ override fun removeListeners(count: Double) {
271
+ gppSectionChangeListenersCount = (gppSectionChangeListenersCount - count.toInt()).coerceAtLeast(0)
272
+ if (gppSectionChangeListenersCount == 0) {
273
+ gppSectionChangeSubscription?.dispose()
274
+ gppSectionChangeSubscription = null
275
+ }
276
+ }
277
+
278
+ override fun invalidate() {
279
+ gppSectionChangeSubscription?.dispose()
280
+ gppSectionChangeSubscription = null
281
+ gppSectionChangeListenersCount = 0
282
+ super.invalidate()
283
+ }
284
+
285
+ private fun emitEvent(eventName: String, payload: WritableMap) {
286
+ reactApplicationContext
287
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
288
+ .emit(eventName, payload)
289
+ }
290
+
291
+ private fun readableMapValueToAny(map: ReadableMap): Any? {
292
+ if (!map.hasKey("value")) return null
293
+ return when (map.getType("value")) {
294
+ ReadableType.Null -> null
295
+ ReadableType.Boolean -> map.getBoolean("value")
296
+ ReadableType.Number -> normalizeNumber(map.getDouble("value"))
297
+ ReadableType.String -> map.getString("value")
298
+ ReadableType.Map -> normalizeCompositeValue(map.getMap("value")?.toHashMap())
299
+ ReadableType.Array -> normalizeCompositeValue(map.getArray("value")?.toArrayList())
300
+ }
301
+ }
302
+
303
+ private fun normalizeCompositeValue(value: Any?): Any? {
304
+ return when (value) {
305
+ is Double -> normalizeNumber(value)
306
+ is ArrayList<*> -> value.map { normalizeCompositeValue(it) }
307
+ is HashMap<*, *> -> value.entries.associate { (key, nestedValue) ->
308
+ key.toString() to normalizeCompositeValue(nestedValue)
309
+ }
310
+ else -> value
311
+ }
312
+ }
313
+
314
+ private fun normalizeNumber(value: Double): Any {
315
+ return if (value % 1.0 == 0.0) value.toInt() else value
316
+ }
317
+
219
318
  private fun runOnUiThread(block: () -> Unit) {
220
319
  UiThreadUtil.runOnUiThread(block)
221
320
  }
222
321
 
223
322
  companion object {
224
323
  const val NAME = "RNUsercentricsModule"
324
+ const val ON_GPP_SECTION_CHANGE_EVENT = "onGppSectionChange"
225
325
  }
226
326
  }
@@ -51,15 +51,27 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
51
51
  @ReactMethod
52
52
  abstract fun getUSPData(promise: Promise)
53
53
 
54
+ @ReactMethod
55
+ abstract fun getGPPData(promise: Promise)
56
+
57
+ @ReactMethod
58
+ abstract fun getGPPString(promise: Promise)
59
+
54
60
  @ReactMethod
55
61
  abstract fun getABTestingVariant(promise: Promise)
56
62
 
63
+ @ReactMethod
64
+ abstract fun getDpsMetadata(templateId: String, promise: Promise)
65
+
57
66
  @ReactMethod
58
67
  abstract fun setCMPId(id: Double)
59
68
 
60
69
  @ReactMethod
61
70
  abstract fun setABTestingVariant(variant: String)
62
71
 
72
+ @ReactMethod
73
+ abstract fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap)
74
+
63
75
  @ReactMethod
64
76
  abstract fun changeLanguage(language: String, promise: Promise)
65
77
 
@@ -73,7 +85,7 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
73
85
  abstract fun denyAll(consentType: Double, promise: Promise)
74
86
 
75
87
  @ReactMethod
76
- abstract fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise)
88
+ abstract fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, unsavedVendorLIDecisions: ReadableArray, promise: Promise)
77
89
 
78
90
  @ReactMethod
79
91
  abstract fun saveDecisions(decisions: ReadableArray, consentType: Double, promise: Promise)
@@ -93,6 +105,12 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
93
105
  @ReactMethod
94
106
  abstract fun track(event: Double)
95
107
 
108
+ @ReactMethod
109
+ abstract fun addListener(eventName: String)
110
+
111
+ @ReactMethod
112
+ abstract fun removeListeners(count: Double)
113
+
96
114
  companion object {
97
115
  const val NAME = "RNUsercentricsModule"
98
116
  }
@@ -137,9 +137,10 @@ internal fun ReadableMap.buttonLayoutFromMap(context: Context): ButtonLayout? {
137
137
 
138
138
  internal fun ReadableMap.buttonSettingsFromMap(context: Context): ButtonSettings {
139
139
  val assetManager = context.assets
140
+ val buttonType = getString("buttonType")?.deserializeButtonType() ?: ButtonType.MORE
140
141
 
141
142
  return ButtonSettings(
142
- type = getString("buttonType")!!.deserializeButtonType(),
143
+ type = buttonType,
143
144
  isAllCaps = getBooleanOrNull("isAllCaps"),
144
145
  font = assetManager.createFontFromName(getString("fontName")),
145
146
  textColor = getString("textColorHex")?.deserializeColor(),
@@ -0,0 +1,83 @@
1
+ package com.usercentrics.reactnative.extensions
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableArray
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.usercentrics.sdk.services.gpp.GppData
7
+ import com.usercentrics.sdk.services.gpp.GppSectionChangePayload
8
+
9
+ private fun normalizeNumber(value: Double): Any {
10
+ return if (value % 1.0 == 0.0 && value <= Int.MAX_VALUE && value >= Int.MIN_VALUE) {
11
+ value.toInt()
12
+ } else {
13
+ value
14
+ }
15
+ }
16
+
17
+ private fun Any?.toWritableValue(): Any? {
18
+ return when (this) {
19
+ null -> null
20
+ is Boolean -> this
21
+ is Number -> normalizeNumber(this.toDouble())
22
+ is String -> this
23
+ is Map<*, *> -> this.toWritableMap()
24
+ is Iterable<*> -> this.toWritableArray()
25
+ is Array<*> -> this.asList().toWritableArray()
26
+ else -> this.toString()
27
+ }
28
+ }
29
+
30
+ private fun Map<*, *>.toWritableMap(): WritableMap {
31
+ val result = Arguments.createMap()
32
+ for ((key, value) in this) {
33
+ val fieldName = key?.toString() ?: continue
34
+ when (val writableValue = value.toWritableValue()) {
35
+ null -> result.putNull(fieldName)
36
+ is Boolean -> result.putBoolean(fieldName, writableValue)
37
+ is Int -> result.putInt(fieldName, writableValue)
38
+ is Double -> result.putDouble(fieldName, writableValue)
39
+ is String -> result.putString(fieldName, writableValue)
40
+ is WritableMap -> result.putMap(fieldName, writableValue)
41
+ is WritableArray -> result.putArray(fieldName, writableValue)
42
+ else -> result.putString(fieldName, writableValue.toString())
43
+ }
44
+ }
45
+ return result
46
+ }
47
+
48
+ private fun Iterable<*>.toWritableArray(): WritableArray {
49
+ val result = Arguments.createArray()
50
+ for (item in this) {
51
+ when (val writableValue = item.toWritableValue()) {
52
+ null -> result.pushNull()
53
+ is Boolean -> result.pushBoolean(writableValue)
54
+ is Int -> result.pushInt(writableValue)
55
+ is Double -> result.pushDouble(writableValue)
56
+ is String -> result.pushString(writableValue)
57
+ is WritableMap -> result.pushMap(writableValue)
58
+ is WritableArray -> result.pushArray(writableValue)
59
+ else -> result.pushString(writableValue.toString())
60
+ }
61
+ }
62
+ return result
63
+ }
64
+
65
+ internal fun GppData.serializeGppData(): WritableMap {
66
+ val sectionsMap = Arguments.createMap()
67
+ sections.forEach { (sectionName, fields) ->
68
+ val fieldsMap = fields.toWritableMap()
69
+ sectionsMap.putMap(sectionName, fieldsMap)
70
+ }
71
+
72
+ val result = Arguments.createMap()
73
+ result.putString("gppString", gppString)
74
+ result.putArray("applicableSections", applicableSections.serialize())
75
+ result.putMap("sections", sectionsMap)
76
+ return result
77
+ }
78
+
79
+ internal fun GppSectionChangePayload.serializeGppPayload(): WritableMap {
80
+ val result = Arguments.createMap()
81
+ result.putString("data", data)
82
+ return result
83
+ }
@@ -14,9 +14,10 @@ internal fun ReadableArray.deserializeUserDecision(): List<UserDecision> {
14
14
  for (i in 0 until size()) {
15
15
  val map = getMap(i)
16
16
  map?.let {
17
+ val serviceId = it.getString("serviceId") ?: return@let
17
18
  decisionList.add(
18
19
  UserDecision(
19
- it.getString("serviceId")!!,
20
+ serviceId,
20
21
  it.getBoolean("consent")
21
22
  )
22
23
  )
@@ -63,7 +63,9 @@ private fun UsercentricsSettings.serialize(): WritableMap {
63
63
  "framework" to framework?.ordinal,
64
64
  "publishedApps" to publishedApps?.map { it.serialize() },
65
65
  "renewConsentsTimestamp" to renewConsentsTimestamp,
66
- "consentWebhook" to consentWebhook
66
+ "consentWebhook" to consentWebhook,
67
+ "gppSignalingEnabled" to getGppSignalingEnabledCompat(),
68
+ "gpcSignalHonoured" to getGpcSignalHonouredCompat(),
67
69
  ).toWritableMap()
68
70
  }
69
71
 
@@ -178,6 +180,8 @@ private fun CCPASettings.serialize(): WritableMap {
178
180
  "secondLayerDescription" to secondLayerDescription,
179
181
  "secondLayerHideLanguageSwitch" to secondLayerHideLanguageSwitch,
180
182
  "btnMoreInfo" to btnMoreInfo,
183
+ "mspaCoveredTransaction" to getMspaCoveredTransactionCompat(),
184
+ "mspaMode" to getMspaModeCompat(),
181
185
  ).toWritableMap()
182
186
  }
183
187
 
@@ -219,7 +223,7 @@ private fun TCF2Settings.serialize(): WritableMap {
219
223
  "disabledSpecialFeatures" to disabledSpecialFeatures,
220
224
  "firstLayerShowDescriptions" to firstLayerShowDescriptions,
221
225
  "hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer,
222
- "resurfacePeriodEnded" to resurfacePeriodEnded,
226
+ "resurfacePeriod" to getResurfacePeriodCompat(),
223
227
  "resurfacePurposeChanged" to resurfacePurposeChanged,
224
228
  "resurfaceVendorAdded" to resurfaceVendorAdded,
225
229
  "firstLayerDescription" to firstLayerDescription,
@@ -245,6 +249,44 @@ private fun TCF2Settings.serialize(): WritableMap {
245
249
  ).toWritableMap()
246
250
  }
247
251
 
252
+ private fun TCF2Settings.getResurfacePeriodCompat(): Int {
253
+ val intValue = runCatching {
254
+ javaClass.getMethod("getResurfacePeriod").invoke(this) as? Int
255
+ }.getOrNull()
256
+ if (intValue != null) return intValue
257
+ val boolValue = runCatching {
258
+ javaClass.getMethod("getResurfacePeriodEnded").invoke(this)
259
+ }.getOrNull() ?: runCatching {
260
+ javaClass.getMethod("isResurfacePeriodEnded").invoke(this)
261
+ }.getOrNull()
262
+ return if (boolValue as? Boolean == true) 1 else 0
263
+ }
264
+
265
+ private fun UsercentricsSettings.getGppSignalingEnabledCompat(): Boolean {
266
+ return runCatching {
267
+ javaClass.getMethod("getGppSignalingEnabled").invoke(this) as? Boolean
268
+ }.getOrNull() ?: false
269
+ }
270
+
271
+ private fun UsercentricsSettings.getGpcSignalHonouredCompat(): Boolean {
272
+ return runCatching {
273
+ javaClass.getMethod("getGpcSignalHonoured").invoke(this) as? Boolean
274
+ }.getOrNull() ?: false
275
+ }
276
+
277
+ private fun CCPASettings.getMspaCoveredTransactionCompat(): Boolean {
278
+ return runCatching {
279
+ javaClass.getMethod("getMspaCoveredTransaction").invoke(this) as? Boolean
280
+ }.getOrNull() ?: false
281
+ }
282
+
283
+ private fun CCPASettings.getMspaModeCompat(): Int? {
284
+ return runCatching {
285
+ val mode = javaClass.getMethod("getMspaMode").invoke(this) ?: return@runCatching null
286
+ mode.javaClass.getMethod("ordinal").invoke(mode) as? Int
287
+ }.getOrNull()
288
+ }
289
+
248
290
  private fun UsercentricsCustomization.serialize(): WritableMap {
249
291
  return mapOf(
250
292
  "color" to color?.serialize(),
@@ -284,6 +326,8 @@ private fun CustomizationColor.serialize(): WritableMap {
284
326
  "toggleDisabledBackground" to toggleDisabledBackground,
285
327
  "toggleDisabledIcon" to toggleDisabledIcon,
286
328
  "secondLayerTab" to secondLayerTab,
329
+ "moreBtnBackground" to moreBtnBackground,
330
+ "moreBtnText" to moreBtnText,
287
331
  ).toWritableMap()
288
332
  }
289
333
 
@@ -0,0 +1,189 @@
1
+ package com.usercentrics.reactnative.extensions
2
+
3
+ import com.facebook.react.bridge.JavaOnlyArray
4
+ import com.facebook.react.bridge.JavaOnlyMap
5
+ import org.junit.Test
6
+ import org.junit.Assert.*
7
+
8
+ /**
9
+ * Tests for nullability handling in React Native 0.77+
10
+ * These tests ensure that null values from ReadableArray/ReadableMap are handled safely
11
+ */
12
+ class NullabilityTest {
13
+
14
+ @Test
15
+ fun `deserializeUserDecision handles null map entries`() {
16
+ // Given: array with null entries
17
+ val array = JavaOnlyArray().apply {
18
+ pushMap(JavaOnlyMap.of("serviceId", "service1", "consent", true))
19
+ pushNull() // Simulate null entry
20
+ pushMap(JavaOnlyMap.of("serviceId", "service2", "consent", false))
21
+ }
22
+
23
+ // When: deserializing
24
+ val result = array.deserializeUserDecision()
25
+
26
+ // Then: null entries are skipped, valid entries are processed
27
+ assertEquals(2, result.size)
28
+ assertEquals("service1", result[0].serviceId)
29
+ assertEquals(true, result[0].consent)
30
+ assertEquals("service2", result[1].serviceId)
31
+ assertEquals(false, result[1].consent)
32
+ }
33
+
34
+ @Test
35
+ fun `deserializePurposeLIDecisionsMap handles null map entries`() {
36
+ // Given: array with null entries
37
+ val array = JavaOnlyArray().apply {
38
+ pushMap(JavaOnlyMap.of("id", 1, "legitimateInterestConsent", true))
39
+ pushNull() // Simulate null entry
40
+ pushMap(JavaOnlyMap.of("id", 2, "legitimateInterestConsent", false))
41
+ }
42
+
43
+ // When: deserializing
44
+ val result = array.deserializePurposeLIDecisionsMap()
45
+
46
+ // Then: null entries are skipped
47
+ assertNotNull(result)
48
+ assertEquals(2, result?.size)
49
+ assertEquals(true, result?.get(1))
50
+ assertEquals(false, result?.get(2))
51
+ }
52
+
53
+ @Test
54
+ fun `deserializePurposeLIDecisionsMap returns null for empty array`() {
55
+ // Given: empty array
56
+ val array = JavaOnlyArray()
57
+
58
+ // When: deserializing
59
+ val result = array.deserializePurposeLIDecisionsMap()
60
+
61
+ // Then: returns null
62
+ assertNull(result)
63
+ }
64
+
65
+ @Test
66
+ fun `deserializeTCFUserDecisions handles null purpose entries`() {
67
+ // Given: TCF decisions with null entries in purposes array
68
+ val purposesArray = JavaOnlyArray().apply {
69
+ pushMap(JavaOnlyMap.of("id", 1, "consent", true))
70
+ pushNull() // Simulate null entry
71
+ pushMap(JavaOnlyMap.of("id", 2, "consent", false))
72
+ }
73
+
74
+ val tcfMap = JavaOnlyMap.of("purposes", purposesArray)
75
+
76
+ // When: deserializing
77
+ val result = tcfMap.deserializeTCFUserDecisions()
78
+
79
+ // Then: null entries are skipped
80
+ assertNotNull(result.purposes)
81
+ assertEquals(2, result.purposes?.size)
82
+ }
83
+
84
+ @Test
85
+ fun `deserializeTCFUserDecisions handles null special features entries`() {
86
+ // Given: TCF decisions with null entries in specialFeatures array
87
+ val specialFeaturesArray = JavaOnlyArray().apply {
88
+ pushMap(JavaOnlyMap.of("id", 1, "consent", true))
89
+ pushNull()
90
+ pushMap(JavaOnlyMap.of("id", 2, "consent", false))
91
+ }
92
+
93
+ val tcfMap = JavaOnlyMap.of("specialFeatures", specialFeaturesArray)
94
+
95
+ // When: deserializing
96
+ val result = tcfMap.deserializeTCFUserDecisions()
97
+
98
+ // Then: null entries are skipped
99
+ assertNotNull(result.specialFeatures)
100
+ assertEquals(2, result.specialFeatures?.size)
101
+ }
102
+
103
+ @Test
104
+ fun `deserializeTCFUserDecisions handles null vendors entries`() {
105
+ // Given: TCF decisions with null entries in vendors array
106
+ val vendorsArray = JavaOnlyArray().apply {
107
+ pushMap(JavaOnlyMap.of("id", 1, "consent", true))
108
+ pushNull()
109
+ pushMap(JavaOnlyMap.of("id", 2, "consent", false))
110
+ }
111
+
112
+ val tcfMap = JavaOnlyMap.of("vendors", vendorsArray)
113
+
114
+ // When: deserializing
115
+ val result = tcfMap.deserializeTCFUserDecisions()
116
+
117
+ // Then: null entries are skipped
118
+ assertNotNull(result.vendors)
119
+ assertEquals(2, result.vendors?.size)
120
+ }
121
+
122
+ @Test
123
+ fun `deserializeTCFUserDecisions handles null adTechProviders entries`() {
124
+ // Given: TCF decisions with null entries in adTechProviders array
125
+ val adTechArray = JavaOnlyArray().apply {
126
+ pushMap(JavaOnlyMap.of("id", 1, "consent", true))
127
+ pushNull()
128
+ pushMap(JavaOnlyMap.of("id", 2, "consent", false))
129
+ }
130
+
131
+ val tcfMap = JavaOnlyMap.of("adTechProviders", adTechArray)
132
+
133
+ // When: deserializing
134
+ val result = tcfMap.deserializeTCFUserDecisions()
135
+
136
+ // Then: null entries are skipped
137
+ assertNotNull(result.adTechProviders)
138
+ assertEquals(2, result.adTechProviders.size)
139
+ }
140
+
141
+ @Test
142
+ fun `deserializeTCFUserDecisions handles missing arrays gracefully`() {
143
+ // Given: empty TCF map
144
+ val tcfMap = JavaOnlyMap()
145
+
146
+ // When: deserializing
147
+ val result = tcfMap.deserializeTCFUserDecisions()
148
+
149
+ // Then: all fields are null or empty
150
+ assertNull(result.purposes)
151
+ assertNull(result.specialFeatures)
152
+ assertNull(result.vendors)
153
+ assertEquals(0, result.adTechProviders.size)
154
+ }
155
+
156
+ @Test
157
+ fun `deserializeUserDecision handles array with all null entries`() {
158
+ // Given: array with only null entries
159
+ val array = JavaOnlyArray().apply {
160
+ pushNull()
161
+ pushNull()
162
+ pushNull()
163
+ }
164
+
165
+ // When: deserializing
166
+ val result = array.deserializeUserDecision()
167
+
168
+ // Then: returns empty list
169
+ assertEquals(0, result.size)
170
+ }
171
+
172
+ @Test
173
+ fun `deserializeUserDecision skips entries with null serviceId`() {
174
+ // Given: array with entry missing serviceId
175
+ val array = JavaOnlyArray().apply {
176
+ pushMap(JavaOnlyMap.of("serviceId", "service1", "consent", true))
177
+ pushMap(JavaOnlyMap.of("consent", false)) // Missing serviceId
178
+ pushMap(JavaOnlyMap.of("serviceId", "service2", "consent", true))
179
+ }
180
+
181
+ // When: deserializing
182
+ val result = array.deserializeUserDecision()
183
+
184
+ // Then: entry without serviceId is skipped
185
+ assertEquals(2, result.size)
186
+ assertEquals("service1", result[0].serviceId)
187
+ assertEquals("service2", result[1].serviceId)
188
+ }
189
+ }