@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.
- package/README.md +7 -7
- package/android/build-legacy.gradle +1 -1
- package/android/build.gradle.kts +1 -1
- package/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt +102 -2
- package/android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt +19 -1
- package/android/src/main/java/com/usercentrics/reactnative/extensions/BannerSettingsExtensions.kt +2 -1
- package/android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt +83 -0
- package/android/src/main/java/com/usercentrics/reactnative/extensions/UserDecisionExtensions.kt +2 -1
- package/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt +46 -2
- package/android/src/test/java/com/usercentrics/reactnative/extensions/NullabilityTest.kt +189 -0
- package/ios/Extensions/GppData+Dict.swift +45 -0
- package/ios/Extensions/GppSectionChangePayload+Dict.swift +10 -0
- package/ios/Extensions/UIImage+UsercentricsLogoDict.swift +31 -6
- package/ios/Extensions/UsercentricsCMPData+Dict.swift +8 -2
- package/ios/Manager/UsercentricsManager.swift +28 -3
- package/ios/RNUsercentricsModule.mm +11 -7
- package/ios/RNUsercentricsModule.swift +56 -15
- package/ios/RNUsercentricsModule.xcodeproj/project.pbxproj +2 -2
- package/ios/RNUsercentricsModuleSpec.h +13 -0
- package/lib/NativeUsercentrics.d.ts +8 -2
- package/lib/NativeUsercentrics.js.map +1 -1
- package/lib/Usercentrics.d.ts +8 -2
- package/lib/Usercentrics.js +23 -3
- package/lib/Usercentrics.js.map +1 -1
- package/lib/fabric/NativeUsercentricsModule.d.ts +7 -1
- package/lib/fabric/NativeUsercentricsModule.js.map +1 -1
- package/lib/models/CCPASettings.d.ts +3 -1
- package/lib/models/CCPASettings.js +3 -1
- package/lib/models/CCPASettings.js.map +1 -1
- package/lib/models/GppData.d.ts +6 -0
- package/lib/models/GppData.js +8 -0
- package/lib/models/GppData.js.map +1 -0
- package/lib/models/GppSectionChangePayload.d.ts +4 -0
- package/lib/models/GppSectionChangePayload.js +6 -0
- package/lib/models/GppSectionChangePayload.js.map +1 -0
- package/lib/models/TCF2Settings.d.ts +2 -2
- package/lib/models/TCF2Settings.js +2 -2
- package/lib/models/TCF2Settings.js.map +1 -1
- package/lib/models/UsercentricsSettings.d.ts +3 -1
- package/lib/models/UsercentricsSettings.js +3 -1
- package/lib/models/UsercentricsSettings.js.map +1 -1
- package/lib/models/index.d.ts +2 -0
- package/lib/models/index.js +2 -0
- package/lib/models/index.js.map +1 -1
- package/package.json +14 -8
- package/src/NativeUsercentrics.ts +10 -1
- package/src/Usercentrics.tsx +31 -3
- package/src/__tests__/index.test.ts +66 -1
- package/src/__tests__/mocks.ts +17 -1
- package/src/fabric/NativeUsercentricsModule.ts +8 -1
- package/src/models/CCPASettings.tsx +7 -1
- package/src/models/GppData.tsx +15 -0
- package/src/models/GppSectionChangePayload.tsx +7 -0
- package/src/models/TCF2Settings.tsx +3 -3
- package/src/models/UsercentricsSettings.tsx +6 -0
- 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
|
-
##
|
|
64
|
+
## Sample app
|
|
65
65
|
|
|
66
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
package/android/build.gradle.kts
CHANGED
|
@@ -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
|
}
|
package/android/src/main/java/com/usercentrics/reactnative/extensions/BannerSettingsExtensions.kt
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|
package/android/src/main/java/com/usercentrics/reactnative/extensions/UserDecisionExtensions.kt
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"
|
|
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
|
+
}
|