@usercentrics/react-native-sdk 2.25.0 → 2.26.1
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/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 +44 -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 +6 -2
- package/ios/Manager/UsercentricsManager.swift +28 -3
- package/ios/RNUsercentricsModule+TurboModule.mm +34 -0
- package/ios/RNUsercentricsModule.mm +11 -7
- package/ios/RNUsercentricsModule.swift +72 -21
- 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 +2 -2
- package/react-native-usercentrics.podspec +6 -12
- 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/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(),
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Usercentrics
|
|
3
|
+
|
|
4
|
+
private func bridgeValue(_ value: Any) -> Any {
|
|
5
|
+
switch value {
|
|
6
|
+
case let dictionary as [String: Any]:
|
|
7
|
+
return bridgeDictionary(dictionary)
|
|
8
|
+
case let array as [Any]:
|
|
9
|
+
return array.map { item -> Any in
|
|
10
|
+
if let nested = item as? [String: Any] {
|
|
11
|
+
return bridgeDictionary(nested)
|
|
12
|
+
}
|
|
13
|
+
if let nestedArray = item as? [Any] {
|
|
14
|
+
return nestedArray.map { nestedItem -> Any in
|
|
15
|
+
if let nestedDict = nestedItem as? [String: Any] {
|
|
16
|
+
return bridgeDictionary(nestedDict)
|
|
17
|
+
}
|
|
18
|
+
return nestedItem
|
|
19
|
+
} as NSArray
|
|
20
|
+
}
|
|
21
|
+
return item
|
|
22
|
+
} as NSArray
|
|
23
|
+
default:
|
|
24
|
+
return value
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private func bridgeDictionary(_ dictionary: [String: Any]) -> NSDictionary {
|
|
29
|
+
let mapped = dictionary.mapValues { bridgeValue($0) }
|
|
30
|
+
return mapped as NSDictionary
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
extension GppData {
|
|
34
|
+
func toDictionary() -> NSDictionary {
|
|
35
|
+
let bridgedSections = sections.reduce(into: [String: Any]()) { partialResult, section in
|
|
36
|
+
partialResult[section.key] = bridgeDictionary(section.value)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
"gppString": self.gppString,
|
|
41
|
+
"applicableSections": self.applicableSections as NSArray,
|
|
42
|
+
"sections": bridgedSections as NSDictionary,
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|