Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.usercentrics.reactnative

import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.usercentrics.sdk.UsercentricsDisposableEvent
import com.usercentrics.sdk.UsercentricsEvent
import com.usercentrics.reactnative.api.UsercentricsProxy
import com.usercentrics.reactnative.extensions.*
import com.usercentrics.sdk.UsercentricsAnalyticsEventType
import com.usercentrics.sdk.models.settings.UsercentricsConsentType
import com.usercentrics.sdk.services.gpp.GppSectionChangePayload
import com.usercentrics.sdk.services.tcf.TCFDecisionUILayer

internal class RNUsercentricsModule(
reactContext: ReactApplicationContext,
private val usercentricsProxy: UsercentricsProxy,
private val reactContextProvider: ReactContextProvider,
) : RNUsercentricsModuleSpec(reactContext) {
private var gppSectionChangeSubscription: UsercentricsDisposableEvent<GppSectionChangePayload>? = null
private var gppSectionChangeListenersCount = 0

override fun getName() = NAME

Expand Down Expand Up @@ -121,6 +127,27 @@ internal class RNUsercentricsModule(
promise.resolve(usercentricsProxy.instance.getUSPData().serialize())
}

@ReactMethod
override fun getGPPData(promise: Promise) {
promise.resolve(usercentricsProxy.instance.getGPPData().serializeGppData())
}

@ReactMethod
override fun getGPPString(promise: Promise) {
promise.resolve(usercentricsProxy.instance.getGPPString())
}

@ReactMethod
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
if (!value.hasKey("value")) return
val parsedValue = if (value.getType("value") == ReadableType.Null) {
null
} else {
readableMapValueToAny(value)
}
usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
}

@ReactMethod
override fun changeLanguage(language: String, promise: Promise) {
usercentricsProxy.instance.changeLanguage(language, {
Expand Down Expand Up @@ -216,11 +243,73 @@ internal class RNUsercentricsModule(
})
}

@ReactMethod
override fun addListener(eventName: String) {
if (eventName != ON_GPP_SECTION_CHANGE_EVENT) return

gppSectionChangeListenersCount++
if (gppSectionChangeSubscription != null) return

gppSectionChangeSubscription = UsercentricsEvent.onGppSectionChange { payload ->
emitEvent(ON_GPP_SECTION_CHANGE_EVENT, payload.serializeGppPayload())
}
}

@ReactMethod
override fun removeListeners(count: Double) {
gppSectionChangeListenersCount = (gppSectionChangeListenersCount - count.toInt()).coerceAtLeast(0)
if (gppSectionChangeListenersCount == 0) {
gppSectionChangeSubscription?.dispose()
gppSectionChangeSubscription = null
}
}

override fun invalidate() {
gppSectionChangeSubscription?.dispose()
gppSectionChangeSubscription = null
gppSectionChangeListenersCount = 0
super.invalidate()
}

private fun emitEvent(eventName: String, payload: WritableMap) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, payload)
}

private fun readableMapValueToAny(map: ReadableMap): Any? {
if (!map.hasKey("value")) return null
return when (map.getType("value")) {
ReadableType.Null -> null
ReadableType.Boolean -> map.getBoolean("value")
ReadableType.Number -> normalizeNumber(map.getDouble("value"))
ReadableType.String -> map.getString("value")
ReadableType.Map -> normalizeCompositeValue(map.getMap("value")?.toHashMap())
ReadableType.Array -> normalizeCompositeValue(map.getArray("value")?.toArrayList())
}
}

private fun normalizeCompositeValue(value: Any?): Any? {
return when (value) {
is Double -> normalizeNumber(value)
is ArrayList<*> -> value.map { normalizeCompositeValue(it) }
is HashMap<*, *> -> value.entries.associate { (key, nestedValue) ->
key.toString() to normalizeCompositeValue(nestedValue)
}
else -> value
}
}

private fun normalizeNumber(value: Double): Any {
return if (value % 1.0 == 0.0) value.toInt() else value
}

private fun runOnUiThread(block: () -> Unit) {
UiThreadUtil.runOnUiThread(block)
}

companion object {
const val NAME = "RNUsercentricsModule"
const val ON_GPP_SECTION_CHANGE_EVENT = "onGppSectionChange"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
@ReactMethod
abstract fun getUSPData(promise: Promise)

@ReactMethod
abstract fun getGPPData(promise: Promise)

@ReactMethod
abstract fun getGPPString(promise: Promise)

@ReactMethod
abstract fun getABTestingVariant(promise: Promise)

Expand All @@ -60,6 +66,9 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
@ReactMethod
abstract fun setABTestingVariant(variant: String)

@ReactMethod
abstract fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap)

@ReactMethod
abstract fun changeLanguage(language: String, promise: Promise)

Expand Down Expand Up @@ -93,6 +102,12 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
@ReactMethod
abstract fun track(event: Double)

@ReactMethod
abstract fun addListener(eventName: String)

@ReactMethod
abstract fun removeListeners(count: Double)

companion object {
const val NAME = "RNUsercentricsModule"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.usercentrics.reactnative.extensions

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.usercentrics.sdk.services.gpp.GppData
import com.usercentrics.sdk.services.gpp.GppSectionChangePayload

private fun normalizeNumber(value: Double): Any {
return if (value % 1.0 == 0.0 && value <= Int.MAX_VALUE && value >= Int.MIN_VALUE) {
value.toInt()
} else {
value
}
}

private fun Any?.toWritableValue(): Any? {
return when (this) {
null -> null
is Boolean -> this
is Number -> normalizeNumber(this.toDouble())
is String -> this
is Map<*, *> -> this.toWritableMap()
is Iterable<*> -> this.toWritableArray()
is Array<*> -> this.asList().toWritableArray()
else -> this.toString()
}
}

private fun Map<*, *>.toWritableMap(): WritableMap {
val result = Arguments.createMap()
for ((key, value) in this) {
val fieldName = key?.toString() ?: continue
when (val writableValue = value.toWritableValue()) {
null -> result.putNull(fieldName)
is Boolean -> result.putBoolean(fieldName, writableValue)
is Int -> result.putInt(fieldName, writableValue)
is Double -> result.putDouble(fieldName, writableValue)
is String -> result.putString(fieldName, writableValue)
is WritableMap -> result.putMap(fieldName, writableValue)
is WritableArray -> result.putArray(fieldName, writableValue)
else -> result.putString(fieldName, writableValue.toString())
}
}
return result
}

private fun Iterable<*>.toWritableArray(): WritableArray {
val result = Arguments.createArray()
for (item in this) {
when (val writableValue = item.toWritableValue()) {
null -> result.pushNull()
is Boolean -> result.pushBoolean(writableValue)
is Int -> result.pushInt(writableValue)
is Double -> result.pushDouble(writableValue)
is String -> result.pushString(writableValue)
is WritableMap -> result.pushMap(writableValue)
is WritableArray -> result.pushArray(writableValue)
else -> result.pushString(writableValue.toString())
}
}
return result
}

internal fun GppData.serializeGppData(): WritableMap {
val sectionsMap = Arguments.createMap()
sections.forEach { (sectionName, fields) ->
val fieldsMap = fields.toWritableMap()
sectionsMap.putMap(sectionName, fieldsMap)
}

val result = Arguments.createMap()
result.putString("gppString", gppString)
result.putArray("applicableSections", applicableSections.serialize())
result.putMap("sections", sectionsMap)
return result
}

internal fun GppSectionChangePayload.serializeGppPayload(): WritableMap {
val result = Arguments.createMap()
result.putString("data", data)
return result
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private fun TCF2Settings.serialize(): WritableMap {
"disabledSpecialFeatures" to disabledSpecialFeatures,
"firstLayerShowDescriptions" to firstLayerShowDescriptions,
"hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer,
"resurfacePeriodEnded" to resurfacePeriodEnded,
"resurfacePeriodEnded" to getResurfacePeriodEndedCompat(),
"resurfacePurposeChanged" to resurfacePurposeChanged,
"resurfaceVendorAdded" to resurfaceVendorAdded,
"firstLayerDescription" to firstLayerDescription,
Expand All @@ -245,6 +245,16 @@ private fun TCF2Settings.serialize(): WritableMap {
).toWritableMap()
}

private fun TCF2Settings.getResurfacePeriodEndedCompat(): Boolean {
val reflectionValue = runCatching {
javaClass.getMethod("getResurfacePeriodEnded").invoke(this)
}.getOrNull() ?: runCatching {
javaClass.getMethod("isResurfacePeriodEnded").invoke(this)
}.getOrNull()

return reflectionValue as? Boolean ?: false
}

private fun UsercentricsCustomization.serialize(): WritableMap {
return mapOf(
"color" to color?.serialize(),
Expand Down
25 changes: 25 additions & 0 deletions example/ios/exampleTests/Fake/FakeUsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ final class FakeUsercentricsManager: UsercentricsManager {
return getUSPDataResponse!
}

var getGPPDataResponse: GppData?
func getGPPData() -> GppData {
return getGPPDataResponse!
}

var getGPPStringResponse: String?
func getGPPString() -> String? {
return getGPPStringResponse
}

var setGPPConsentSectionName: String?
var setGPPConsentFieldName: String?
var setGPPConsentValue: Any?
func setGPPConsent(sectionName: String, fieldName: String, value: Any) {
self.setGPPConsentSectionName = sectionName
self.setGPPConsentFieldName = fieldName
self.setGPPConsentValue = value
}

var gppSectionChangeDisposableEvent = UsercentricsDisposableEvent<GppSectionChangePayload>()
func onGppSectionChange(callback: @escaping (GppSectionChangePayload) -> Void) -> UsercentricsDisposableEvent<GppSectionChangePayload> {
gppSectionChangeDisposableEvent.callback = callback
return gppSectionChangeDisposableEvent
}

var getTCFDataResponse: TCFData?
func getTCFData(callback: @escaping (TCFData) -> Void) {
callback(getTCFDataResponse!)
Expand Down
45 changes: 45 additions & 0 deletions ios/Extensions/GppData+Dict.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import Usercentrics

private func bridgeValue(_ value: Any) -> Any {
switch value {
case let dictionary as [String: Any]:
return bridgeDictionary(dictionary)
case let array as [Any]:
return array.map { item in
if let nested = item as? [String: Any] {
return bridgeDictionary(nested)
}
if let nestedArray = item as? [Any] {
return nestedArray.map { nestedItem in
if let nestedDict = nestedItem as? [String: Any] {
return bridgeDictionary(nestedDict)
}
return nestedItem
}
}
return item
} as NSArray
default:
return value
}
}

private func bridgeDictionary(_ dictionary: [String: Any]) -> NSDictionary {
let mapped = dictionary.mapValues { bridgeValue($0) }
return mapped as NSDictionary
}

extension GppData {
func toDictionary() -> NSDictionary {
let bridgedSections = sections.reduce(into: [String: Any]()) { partialResult, section in
partialResult[section.key] = bridgeDictionary(section.value)
}

return [
"gppString": self.gppString,
"applicableSections": self.applicableSections as NSArray,
"sections": bridgedSections as NSDictionary,
]
}
}
10 changes: 10 additions & 0 deletions ios/Extensions/GppSectionChangePayload+Dict.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
import Usercentrics

extension GppSectionChangePayload {
func toDictionary() -> NSDictionary {
return [
"data": self.data
]
}
}
1 change: 0 additions & 1 deletion ios/Extensions/UsercentricsCMPData+Dict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ extension TCF2Settings {
"disabledSpecialFeatures" : self.disabledSpecialFeatures,
"firstLayerShowDescriptions" : self.firstLayerShowDescriptions,
"hideNonIabOnFirstLayer" : self.hideNonIabOnFirstLayer,
"resurfacePeriodEnded" : self.resurfacePeriodEnded,
"resurfacePurposeChanged" : self.resurfacePurposeChanged,
"resurfaceVendorAdded" : self.resurfaceVendorAdded,
"firstLayerDescription" : self.firstLayerDescription as Any,
Expand Down
Loading
Loading