Conversation
Add GPP (Global Privacy Platform) support to the React Native SDK: - Wire getGPPData(), getGPPString(), setGPPConsent() bridge methods for Android and iOS - Add onGppSectionChange event listener via RCTDeviceEventEmitter - Define GppData and GppSectionChangePayload TypeScript models - Serialize GPP data structures on both native platforms - Add GPP testing screen to the sample app - Add unit tests for all GPP methods Made-with: Cursor
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Review Summary by QodoAdd Global Privacy Platform (GPP) support to React Native SDK
WalkthroughsDescription• Add GPP (Global Privacy Platform) support with three bridge methods - getGPPData() returns GPP data structure with string, sections, applicable sections - getGPPString() returns GPP consent string - setGPPConsent() updates GPP consent for specific section and field • Implement onGppSectionChange event listener on both Android and iOS platforms - Android uses DeviceEventManagerModule.RCTDeviceEventEmitter for event emission - iOS extends RCTEventEmitter with startObserving() and stopObserving() lifecycle • Add TypeScript models GppData and GppSectionChangePayload for type safety • Create GPP testing screen in sample app with UI for all GPP operations • Add comprehensive unit tests for all new GPP methods and event handling Diagramflowchart LR
A["Native Layer<br/>Android & iOS"] -->|Bridge Methods| B["RNUsercentricsModule"]
B -->|getGPPData| C["GppData Model"]
B -->|getGPPString| D["GPP String"]
B -->|setGPPConsent| E["Update Consent"]
A -->|Event Emission| F["onGppSectionChange"]
F -->|NativeEventEmitter| G["JavaScript Layer"]
G -->|Callback| H["GppSectionChangePayload"]
I["Sample App"] -->|Testing UI| J["GppTestingScreen"]
File Changes1. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
|
Code Review by Qodo
1. Null GPP consent ignored
|
📝 WalkthroughWalkthroughAdds GPP (Global Privacy Platform) support across platforms: new APIs to get GPP data/string, set GPP consent, and emit onGppSectionChange events. Changes span Android and iOS native modules, TypeScript models and APIs, test/fake implementations, and the sample app with a GPP testing screen. Changes
Sequence Diagram(s)sequenceDiagram
participant JS as JavaScript/React
participant Bridge as RN Bridge / Native Module
participant NativeCore as Usercentrics Core
participant EventMgr as UsercentricsEvent
JS->>Bridge: getGPPData()
Bridge->>NativeCore: UsercentricsCore.getGPPData()
NativeCore-->>Bridge: GppData
Bridge-->>JS: Promise resolved with serialized GppData
JS->>Bridge: setGPPConsent(sectionName, fieldName, value)
Bridge->>NativeCore: UsercentricsCore.setGPPConsent(...)
NativeCore-->>Bridge: ack/void
sequenceDiagram
participant JS as JavaScript/React
participant Bridge as RNUsercentricsModule
participant NativeEvent as UsercentricsEvent
participant JSCallback as JS listener
JS->>Bridge: addListener("onGppSectionChange")
Bridge->>NativeEvent: onGppSectionChange(register callback)
NativeEvent-->>Bridge: UsercentricsDisposableEvent
NativeEvent->>Bridge: emits GppSectionChangePayload
Bridge-->>JSCallback: emits "onGppSectionChange" event via RN emitter
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Sequence DiagramThis PR adds React Native support for retrieving GPP data/string and updating GPP consent, wiring these calls through the native bridges to the Usercentrics core and exposing GPP section change notifications back to the app. sequenceDiagram
participant App
participant ReactNativeSDK
participant UsercentricsCore
App->>ReactNativeSDK: Request GPP data or string
ReactNativeSDK->>UsercentricsCore: Get GPP data and string
UsercentricsCore-->>ReactNativeSDK: GPP data and string
ReactNativeSDK-->>App: Resolve promise with GPP data/string
App->>ReactNativeSDK: setGPPConsent for a section
ReactNativeSDK->>UsercentricsCore: Update GPP consent value
UsercentricsCore-->>ReactNativeSDK: Notify GPP section change
ReactNativeSDK-->>App: Invoke onGppSectionChange callback
Generated by CodeAnt AI |
|
PR Summary: Implements GPP (Global Privacy Platform) support: adds GPP data access, GPP consent setters, and a GPP section change event across native Android/iOS and JS. Changes:
Compatibility notes:
|
| @ReactMethod | ||
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | ||
| val parsedValue = readableMapValueToAny(value) ?: return | ||
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) | ||
| } |
There was a problem hiding this comment.
Suggestion: When the JS layer calls the GPP consent API with an explicit null value (e.g. to clear a field), the Android bridge silently returns early because readableMapValueToAny returns null and ?: return short-circuits, so the underlying SDK never receives the update, unlike the iOS implementation which forwards an NSNull(); this leads to inconsistent behavior and prevents clearing GPP values on Android. [logic error]
Severity Level: Major ⚠️
- ⚠️ Android `Usercentrics.setGPPConsent` ignores explicit null values.
- ⚠️ GPP consent clearing inconsistent between iOS and Android bridges.
- ⚠️ Android GPP state may not reset when apps expect.
- ⚠️ Public API in `src/Usercentrics.tsx` behaves differently per platform.| @ReactMethod | |
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | |
| val parsedValue = readableMapValueToAny(value) ?: return | |
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) | |
| } | |
| @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) | |
| } |
Steps of Reproduction ✅
1. From JS, call the public API `Usercentrics.setGPPConsent("usnat", "SaleOptOut", null)`
(defined in `src/Usercentrics.tsx:150-153`, which wraps the value into `{ value }` and
calls `RNUsercentricsModule.setGPPConsent`).
2. On Android, React Native bridges this call into
`RNUsercentricsModule.setGPPConsent(sectionName, fieldName, value: ReadableMap)` in
`android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt:140-144`,
where `value` is a `ReadableMap` containing key `"value"` with type `ReadableType.Null`.
3. Inside `setGPPConsent`, the helper `readableMapValueToAny` at
`RNUsercentricsModule.kt:275-284` is invoked; because the `"value"` key is present and its
type is `ReadableType.Null`, this function returns `null`, causing the Elvis operator `?:
return` at line 142 to short‑circuit and exit `setGPPConsent` without calling
`usercentricsProxy.instance.setGPPConsent(...)`.
4. On iOS, the same JS call is bridged via `ios/RNUsercentricsModule.swift:157-160`, where
`setGPPConsent(_:fieldName:value:)` unwraps `value["value"] ?? NSNull()` and *always*
calls `usercentricsManager.setGPPConsent(...)`, passing `NSNull()` when the JS value is
null, so the GPP consent update/clear is applied on iOS but is a no‑op on Android.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
**Line:** 140:144
**Comment:**
*Logic Error: When the JS layer calls the GPP consent API with an explicit `null` value (e.g. to clear a field), the Android bridge silently returns early because `readableMapValueToAny` returns `null` and `?: return` short-circuits, so the underlying SDK never receives the update, unlike the iOS implementation which forwards an `NSNull()`; this leads to inconsistent behavior and prevents clearing GPP values on Android.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.|
CodeAnt AI finished reviewing your PR. |
| internal fun GppData.serializeGppData(): WritableMap { | ||
| val sectionsMap = Arguments.createMap() | ||
| sections.forEach { (sectionName, fields) -> | ||
| val fieldsMap = Arguments.createMap() | ||
| fields.forEach { (fieldName, value) -> | ||
| when (value) { | ||
| null -> fieldsMap.putNull(fieldName) | ||
| is Boolean -> fieldsMap.putBoolean(fieldName, value) | ||
| is Int -> fieldsMap.putInt(fieldName, value) | ||
| is Double -> fieldsMap.putDouble(fieldName, value) | ||
| is String -> fieldsMap.putString(fieldName, value) | ||
| else -> fieldsMap.putString(fieldName, value.toString()) | ||
| } | ||
| } | ||
| sectionsMap.putMap(sectionName, fieldsMap) | ||
| } | ||
|
|
||
| val result = Arguments.createMap() | ||
| result.putString("gppString", gppString) | ||
| result.putArray("applicableSections", applicableSections.serialize()) | ||
| result.putMap("sections", sectionsMap) | ||
| return result |
There was a problem hiding this comment.
[REFACTORING] Current serialization handles only Int/Double/Boolean/String and falls back to toString() for other numeric types or nested structures. Improve robustness by: (1) treating all numeric types via 'is Number' and mapping integers vs floats appropriately (e.g. detect integral numbers and use putInt, otherwise putDouble), (2) recursively serializing nested maps/arrays into WritableMap/WritableArray (instead of toString()) so complex section values are preserved, and (3) explicitly handling Long/Float/Short types that may appear in the SDK.
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 Any?.toWritableValue(): Any? = when (this) {
null -> null
is Boolean -> this
is Number -> {
// Preserve integers as Int, others as Double
val doubleValue = this.toDouble()
if (doubleValue % 1.0 == 0.0 && doubleValue <= Int.MAX_VALUE && doubleValue >= Int.MIN_VALUE) {
doubleValue.toInt()
} else {
doubleValue
}
}
is Map<*, *> -> this.toWritableMap()
is Iterable<*> -> this.toWritableArray()
else -> this.toString()
}
private fun Map<*, *>.toWritableMap(): WritableMap {
val map = Arguments.createMap()
for ((key, value) in this) {
val name = key?.toString() ?: continue
when (val v = value.toWritableValue()) {
null -> map.putNull(name)
is Boolean -> map.putBoolean(name, v)
is Int -> map.putInt(name, v)
is Double -> map.putDouble(name, v)
is String -> map.putString(name, v)
is WritableMap -> map.putMap(name, v)
is WritableArray -> map.putArray(name, v)
else -> map.putString(name, v.toString())
}
}
return map
}
private fun Iterable<*>.toWritableArray(): WritableArray {
val array = Arguments.createArray()
for (item in this) {
when (val v = item.toWritableValue()) {
null -> array.pushNull()
is Boolean -> array.pushBoolean(v)
is Int -> array.pushInt(v)
is Double -> array.pushDouble(v)
is String -> array.pushString(v)
is WritableMap -> array.pushMap(v)
is WritableArray -> array.pushArray(v)
else -> array.pushString(v.toString())
}
}
return array
}
internal fun GppData.serializeGppData(): WritableMap {
val sectionsMap = Arguments.createMap()
sections.forEach { (sectionName, fields) ->
val fieldsMap = (fields as? Map<*, *>)?.toWritableMap() ?: Arguments.createMap()
sectionsMap.putMap(sectionName, fieldsMap)
}
return Arguments.createMap().apply {
putString("gppString", gppString)
putArray("applicableSections", applicableSections.serialize())
putMap("sections", sectionsMap)
}
}
internal fun GppSectionChangePayload.serializeGppPayload(): WritableMap {
return Arguments.createMap().apply {
putString("data", data)
}
}| 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 | ||
| } |
There was a problem hiding this comment.
[VALIDATION] readableMapValueToAny currently expects the incoming ReadableMap to have a 'value' key and silently returns when missing. This makes setGPPConsent a no-op if callers don't wrap the primitive in { value }. Consider: (a) accepting primitive values directly (overload or document expected shape), (b) returning an explicit error/log if 'value' key is missing so the caller learns why nothing happened, or (c) add input validation and a Promise rejection/throw for malformed input to avoid silently ignoring calls.
@ReactMethod
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
if (!value.hasKey("value")) {
// Option A: surface an explicit error so JS can react
throw IllegalArgumentException("setGPPConsent: 'value' key is missing from payload. Expected shape: { value: any }")
// or, if you prefer non-throwing behavior, log and return:
// Log.w(NAME, "setGPPConsent called without 'value' key. Expected shape: { value: any }")
// return
}
val parsedValue = readableMapValueToAny(value)
usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
}| extension GppData { | ||
| func toDictionary() -> NSDictionary { | ||
| return [ | ||
| "gppString": self.gppString, | ||
| "applicableSections": self.applicableSections, | ||
| "sections": self.sections, | ||
| ] | ||
| } |
There was a problem hiding this comment.
[REFACTORING] toDictionary returns Swift collections directly — ensure values are converted to Foundation types expected by React bridge (NSArray/NSDictionary) and that nested values are serialized safely (numbers, booleans, nested maps/arrays). Prefer mapping applicableSections -> NSArray and sections -> NSDictionary by explicitly transforming elements to Foundation types to avoid unexpected bridging behavior at runtime.
import Foundation
import Usercentrics
extension GppData {
func toDictionary() -> NSDictionary {
return [
"gppString": gppString as NSString,
"applicableSections": applicableSections as NSArray,
"sections": sections as NSDictionary
]
}
}| - (void)setGPPConsent:(NSString *)sectionName | ||
| fieldName:(NSString *)fieldName | ||
| value:(id)value; |
There was a problem hiding this comment.
[NITPICK] Signature mismatch: the spec declares setGPPConsent:value:(id)value while RNUsercentricsModule.mm extern uses value:(NSDictionary *)value. Prefer using a consistent type across the ObjC header and extern declaration (e.g. NSDictionary * or id) to avoid ambiguity and make intended JS shape explicit.
// ios/RNUsercentricsModuleSpec.h
- (void)setGPPConsent:(NSString *)sectionName
fieldName:(NSString *)fieldName
value:(NSDictionary *)value;|
Reviewed up to commit:a7bea02ac299987074920df6e7a918f60597186f |
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | ||
| val parsedValue = readableMapValueToAny(value) ?: return | ||
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) | ||
| } |
There was a problem hiding this comment.
1. Null gpp consent ignored 🐞 Bug ✓ Correctness
Android RNUsercentricsModule.setGPPConsent silently no-ops when JS passes a null value because readableMapValueToAny returns null and the method returns early. This prevents clearing a GPP field on Android and diverges from iOS which forwards the null (as NSNull).
Agent Prompt
### Issue description
Android `setGPPConsent` drops updates when the JS wrapper contains `{ value: null }`, because the code returns early on `null`.
### Issue Context
iOS forwards the wrapped value even when it is null (`NSNull()`), so Android/iOS behavior diverges and Android cannot clear a GPP field.
### Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[141-144]
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[275-284]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| sections.forEach { (sectionName, fields) -> | ||
| val fieldsMap = Arguments.createMap() | ||
| fields.forEach { (fieldName, value) -> | ||
| when (value) { | ||
| null -> fieldsMap.putNull(fieldName) | ||
| is Boolean -> fieldsMap.putBoolean(fieldName, value) | ||
| is Int -> fieldsMap.putInt(fieldName, value) | ||
| is Double -> fieldsMap.putDouble(fieldName, value) | ||
| is String -> fieldsMap.putString(fieldName, value) | ||
| else -> fieldsMap.putString(fieldName, value.toString()) | ||
| } |
There was a problem hiding this comment.
2. Gpp composite values lost 🐞 Bug ✓ Correctness
Android getGPPData serialization stringifies any non-primitive section field values, losing structure for arrays/maps. Because Android setGPPConsent explicitly supports Map/Array inputs, setting composite values cannot round-trip back through getGPPData.
Agent Prompt
### Issue description
`GppData.serializeGppData()` stringifies non-primitive values, so composite consent values (arrays/maps) cannot be round-tripped.
### Issue Context
The same module supports setting composite values via `setGPPConsent` (ReadableType.Map/Array), so `getGPPData` should preserve these structures.
### Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt[12-20]
- android/src/main/java/com/usercentrics/reactnative/extensions/ReadableMapExtensions.kt[32-116]
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[277-296]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| "disabledSpecialFeatures" to disabledSpecialFeatures, | ||
| "firstLayerShowDescriptions" to firstLayerShowDescriptions, | ||
| "hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer, | ||
| "resurfacePeriodEnded" to resurfacePeriodEnded, | ||
| "resurfacePurposeChanged" to resurfacePurposeChanged, | ||
| "resurfaceVendorAdded" to resurfaceVendorAdded, |
There was a problem hiding this comment.
3. Missing resurfaceperiodended key 🐞 Bug ✓ Correctness
Android TCF2Settings.serialize no longer includes resurfacePeriodEnded in the object returned to JS. The TypeScript TCF2Settings model still requires this field and iOS continues to provide it, causing Android/iOS payload divergence and missing data on Android.
Agent Prompt
### Issue description
Android `TCF2Settings.serialize()` omits the `resurfacePeriodEnded` field, breaking the TS model contract and iOS parity.
### Issue Context
TS `TCF2Settings` requires `resurfacePeriodEnded: boolean` and iOS still serializes it.
### Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt[184-245]
- src/models/TCF2Settings.tsx[30-55]
- ios/Extensions/UsercentricsCMPData+Dict.swift[204-214]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
CI Feedback 🧐A test triggered by this PR failed. Here is an AI-generated analysis of the failure:
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
sample/src/screens/GppTestingScreen.tsx (1)
20-37: Consider adding error handling to async operations.The async handlers call SDK methods without try-catch blocks. If the SDK isn't properly initialized or an error occurs, the app could display unclear errors to testers.
💡 Proposed fix to add error handling
const fetchGppString = async () => { + try { const value = await Usercentrics.getGPPString(); setGppString(value); + } catch (error) { + setGppString(`Error: ${error}`); + } }; const fetchGppData = async () => { + try { const value = await Usercentrics.getGPPData(); const asJson = JSON.stringify(value, null, 2); setGppDataJson(asJson); + } catch (error) { + setGppDataJson(`Error: ${error}`); + } }; const setUsNatSaleOptOut = async () => { + try { await Usercentrics.setGPPConsent('usnat', 'SaleOptOut', 2); + } catch (error) { + console.error('Failed to set usnat consent:', error); + } }; const setUsFlSaleOptOut = async () => { + try { await Usercentrics.setGPPConsent('usfl', 'SaleOptOut', 2); + } catch (error) { + console.error('Failed to set usfl consent:', error); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sample/src/screens/GppTestingScreen.tsx` around lines 20 - 37, The async handlers fetchGppString, fetchGppData, setUsNatSaleOptOut, and setUsFlSaleOptOut lack error handling; wrap each function body in a try-catch, await the SDK call inside try, handle errors in catch by logging the error (e.g., console.error or process logger) and updating UI state (e.g., set an error string or toast) so failures are visible to testers; ensure you preserve existing success behavior (setGppString/setGppDataJson) and do not swallow errors silently.android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt (1)
12-20: Add missing List and Map type handling to GppDataExtensions.The
whenblock only handles primitives (Boolean, Int, Double, String, null). SinceGppData.sectionscontainsMap<String, Any>, fields can hold arrays or nested objects. These will fall back totoString(), corrupting complex data. The codebase already hasList<*>.serialize()andMap<String, Any?>.toWritableMap()extensions that properly handle these types.♻️ Add support for List and Map types
fields.forEach { (fieldName, value) -> when (value) { null -> fieldsMap.putNull(fieldName) is Boolean -> fieldsMap.putBoolean(fieldName, value) is Int -> fieldsMap.putInt(fieldName, value) is Double -> fieldsMap.putDouble(fieldName, value) is String -> fieldsMap.putString(fieldName, value) + is List<*> -> fieldsMap.putArray(fieldName, value.serialize()) + is Map<*, *> -> { + `@Suppress`("UNCHECKED_CAST") + fieldsMap.putMap(fieldName, (value as Map<String, Any>).toWritableMap()) + } else -> fieldsMap.putString(fieldName, value.toString()) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt` around lines 12 - 20, The when-block in GppDataExtensions.kt only handles primitives and falls back to toString() for complex types; update the fields.forEach handling to detect List and Map values and serialize them using the existing List<*>.serialize() and Map<String, Any?>.toWritableMap() extensions (e.g., add an is List<*> branch to call fieldsMap.putArray(fieldName, value.serialize()) and an is Map<*,*> branch that safely casts to Map<String, Any?> and calls fieldsMap.putMap(fieldName, value.toWritableMap())), ensuring nullability is respected and avoiding the toString() fallback for nested structures.src/models/GppData.tsx (1)
1-15: Consider using an interface instead of a class, though verify alignment with architectural patterns.The native bridge returns plain JavaScript objects, not class instances. Using
export class GppDatais semantically less precise than an interface sinceUsercentrics.getGPPData()returns raw bridge objects without calling this constructor. However, all 45+ other models in src/models/ consistently use theexport classpattern. Changing only GppData to an interface would break this established architectural consistency.This is not blocking and carries minimal risk (no
instanceofchecks are used with this class in the codebase), but the pattern choice should align with a broader decision about how models in this project are standardized.💡 Suggested alternative using interface (if adopting interface pattern project-wide)
-export class GppData { - gppString: string - applicableSections: number[] - sections: Record<string, Record<string, unknown>> - - constructor( - gppString: string, - applicableSections: number[], - sections: Record<string, Record<string, unknown>> - ) { - this.gppString = gppString - this.applicableSections = applicableSections - this.sections = sections - } -} +export interface GppData { + gppString: string + applicableSections: number[] + sections: Record<string, Record<string, unknown>> +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/models/GppData.tsx` around lines 1 - 15, The GppData type is declared as a class but native bridge returns plain objects (Usercentrics.getGPPData()), so either convert GppData to an interface or keep the class for consistency across models; to fix, choose one approach and apply it consistently: if you opt to use interface, change export class GppData to export interface GppData { gppString: string; applicableSections: number[]; sections: Record<string, Record<string, unknown>> } and update any other model files to interfaces, or if you keep the class (recommended for alignment with the 45+ existing models) leave export class GppData as-is but add a short comment near the GppData declaration noting that bridge returns plain objects and instances are not required (reference symbols: GppData and Usercentrics.getGPPData).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt`:
- Around line 141-143: The current setGPPConsent implementation treats both a
missing "value" key and an explicit null value as a no-op because
readableMapValueToAny() returning null causes an early return; change
setGPPConsent to distinguish "no key" from "key present with null" by checking
the ReadableMap for the "value" key instead of simply returning on parsedValue
== null, and when the key is present but null forward an explicit null
placeholder to usercentricsProxy.instance.setGPPConsent (so
readableMapValueToAny, setGPPConsent, and the call to
usercentricsProxy.instance.setGPPConsent are adjusted to propagate an explicit
null value rather than swallowing it).
In `@ios/RNUsercentricsModule.swift`:
- Around line 38-52: The subscription is created inside queue.async which can
race with immediate JS calls like setGPPConsent and miss the first event; change
startObserving to register gppSectionChangeSubscription synchronously (remove
the outer queue.async and create the subscription immediately in startObserving)
while keeping event dispatching onto queue if needed (i.e., inside the
onGppSectionChange handler use queue.async { self.sendEvent(...) }). Keep
stopObserving disposing gppSectionChangeSubscription on the queue (queue.async {
... }) and reference the existing gppSectionChangeSubscription,
usercentricsManager.onGppSectionChange, startObserving, stopObserving,
setGPPConsent, and onGppSectionChangeEvent symbols when making the change.
In `@ios/RNUsercentricsModuleSpec.h`:
- Around line 63-65: The spec declares setGPPConsent with parameter type `id`
for `value` but the implementations (RNUsercentricsModule.mm and
RNUsercentricsModule.swift) expect an NSDictionary; update the
RNUsercentricsModuleSpec declaration for the method `-
(void)setGPPConsent:(NSString *)sectionName fieldName:(NSString *)fieldName
value:(id)value;` to use `NSDictionary *value` instead of `id` so the
Objective-C bridge matches the .mm and Swift implementations.
---
Nitpick comments:
In
`@android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt`:
- Around line 12-20: The when-block in GppDataExtensions.kt only handles
primitives and falls back to toString() for complex types; update the
fields.forEach handling to detect List and Map values and serialize them using
the existing List<*>.serialize() and Map<String, Any?>.toWritableMap()
extensions (e.g., add an is List<*> branch to call fieldsMap.putArray(fieldName,
value.serialize()) and an is Map<*,*> branch that safely casts to Map<String,
Any?> and calls fieldsMap.putMap(fieldName, value.toWritableMap())), ensuring
nullability is respected and avoiding the toString() fallback for nested
structures.
In `@sample/src/screens/GppTestingScreen.tsx`:
- Around line 20-37: The async handlers fetchGppString, fetchGppData,
setUsNatSaleOptOut, and setUsFlSaleOptOut lack error handling; wrap each
function body in a try-catch, await the SDK call inside try, handle errors in
catch by logging the error (e.g., console.error or process logger) and updating
UI state (e.g., set an error string or toast) so failures are visible to
testers; ensure you preserve existing success behavior
(setGppString/setGppDataJson) and do not swallow errors silently.
In `@src/models/GppData.tsx`:
- Around line 1-15: The GppData type is declared as a class but native bridge
returns plain objects (Usercentrics.getGPPData()), so either convert GppData to
an interface or keep the class for consistency across models; to fix, choose one
approach and apply it consistently: if you opt to use interface, change export
class GppData to export interface GppData { gppString: string;
applicableSections: number[]; sections: Record<string, Record<string, unknown>>
} and update any other model files to interfaces, or if you keep the class
(recommended for alignment with the 45+ existing models) leave export class
GppData as-is but add a short comment near the GppData declaration noting that
bridge returns plain objects and instances are not required (reference symbols:
GppData and Usercentrics.getGPPData).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1d60dae1-efab-4668-99bb-2df74c7d6f6c
📒 Files selected for processing (24)
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.ktandroid/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.ktandroid/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.ktandroid/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.ktexample/ios/exampleTests/Fake/FakeUsercentricsManager.swiftios/Extensions/GppData+Dict.swiftios/Extensions/GppSectionChangePayload+Dict.swiftios/Manager/UsercentricsManager.swiftios/RNUsercentricsModule.mmios/RNUsercentricsModule.swiftios/RNUsercentricsModuleSpec.hsample/ios/sampleTests/Fake/FakeUsercentricsManager.swiftsample/src/App.tsxsample/src/screens/GppTestingScreen.tsxsample/src/screens/Home.tsxsample/src/screens/index.tsxsrc/NativeUsercentrics.tssrc/Usercentrics.tsxsrc/__tests__/index.test.tssrc/__tests__/mocks.tssrc/fabric/NativeUsercentricsModule.tssrc/models/GppData.tsxsrc/models/GppSectionChangePayload.tsxsrc/models/index.tsx
💤 Files with no reviewable changes (1)
- android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | ||
| val parsedValue = readableMapValueToAny(value) ?: return | ||
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) |
There was a problem hiding this comment.
Don't turn { value: null } into a silent no-op.
Line 142 collapses both “missing key” and explicit null into return. The JS API accepts unknown, and ios/RNUsercentricsModule.swift forwards the wrapped null value instead of swallowing it, so Android currently behaves differently for callers trying to clear a GPP field.
🐛 Proposed fix
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
- val parsedValue = readableMapValueToAny(value) ?: return
- usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
+ if (!value.hasKey("value")) return
+ val parsedValue = readableMapValueToAny(value)
+ usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | |
| val parsedValue = readableMapValueToAny(value) ?: return | |
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) | |
| override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) { | |
| if (!value.hasKey("value")) return | |
| val parsedValue = readableMapValueToAny(value) | |
| usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt`
around lines 141 - 143, The current setGPPConsent implementation treats both a
missing "value" key and an explicit null value as a no-op because
readableMapValueToAny() returning null causes an early return; change
setGPPConsent to distinguish "no key" from "key present with null" by checking
the ReadableMap for the "value" key instead of simply returning on parsedValue
== null, and when the key is present but null forward an explicit null
placeholder to usercentricsProxy.instance.setGPPConsent (so
readableMapValueToAny, setGPPConsent, and the call to
usercentricsProxy.instance.setGPPConsent are adjusted to propagate an explicit
null value rather than swallowing it).
| override func startObserving() { | ||
| queue.async { [weak self] in | ||
| guard let self = self, self.gppSectionChangeSubscription == nil else { return } | ||
| self.gppSectionChangeSubscription = self.usercentricsManager.onGppSectionChange { payload in | ||
| self.sendEvent(withName: Self.onGppSectionChangeEvent, body: payload.toDictionary()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override func stopObserving() { | ||
| queue.async { [weak self] in | ||
| self?.gppSectionChangeSubscription?.dispose() | ||
| self?.gppSectionChangeSubscription = nil | ||
| } | ||
| } |
There was a problem hiding this comment.
Register the GPP observer before returning from startObserving.
The async hops on Line 38 and Line 47 can reorder subscription setup and teardown relative to setGPPConsent on Line 157. A JS caller that subscribes and immediately calls setGPPConsent can miss the first onGppSectionChange event on iOS.
🐛 Proposed fix
override func startObserving() {
- queue.async { [weak self] in
- guard let self = self, self.gppSectionChangeSubscription == nil else { return }
- self.gppSectionChangeSubscription = self.usercentricsManager.onGppSectionChange { payload in
- self.sendEvent(withName: Self.onGppSectionChangeEvent, body: payload.toDictionary())
- }
- }
+ let subscribe = { [weak self] in
+ guard let self = self, self.gppSectionChangeSubscription == nil else { return }
+ self.gppSectionChangeSubscription = self.usercentricsManager.onGppSectionChange { [weak self] payload in
+ self?.sendEvent(withName: Self.onGppSectionChangeEvent, body: payload.toDictionary())
+ }
+ }
+
+ if Thread.isMainThread {
+ subscribe()
+ } else {
+ DispatchQueue.main.sync(execute: subscribe)
+ }
}
override func stopObserving() {
- queue.async { [weak self] in
- self?.gppSectionChangeSubscription?.dispose()
- self?.gppSectionChangeSubscription = nil
- }
+ let unsubscribe = { [weak self] in
+ self?.gppSectionChangeSubscription?.dispose()
+ self?.gppSectionChangeSubscription = nil
+ }
+
+ if Thread.isMainThread {
+ unsubscribe()
+ } else {
+ DispatchQueue.main.sync(execute: unsubscribe)
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| override func startObserving() { | |
| queue.async { [weak self] in | |
| guard let self = self, self.gppSectionChangeSubscription == nil else { return } | |
| self.gppSectionChangeSubscription = self.usercentricsManager.onGppSectionChange { payload in | |
| self.sendEvent(withName: Self.onGppSectionChangeEvent, body: payload.toDictionary()) | |
| } | |
| } | |
| } | |
| override func stopObserving() { | |
| queue.async { [weak self] in | |
| self?.gppSectionChangeSubscription?.dispose() | |
| self?.gppSectionChangeSubscription = nil | |
| } | |
| } | |
| override func startObserving() { | |
| let subscribe = { [weak self] in | |
| guard let self = self, self.gppSectionChangeSubscription == nil else { return } | |
| self.gppSectionChangeSubscription = self.usercentricsManager.onGppSectionChange { [weak self] payload in | |
| self?.sendEvent(withName: Self.onGppSectionChangeEvent, body: payload.toDictionary()) | |
| } | |
| } | |
| if Thread.isMainThread { | |
| subscribe() | |
| } else { | |
| DispatchQueue.main.sync(execute: subscribe) | |
| } | |
| } | |
| override func stopObserving() { | |
| let unsubscribe = { [weak self] in | |
| self?.gppSectionChangeSubscription?.dispose() | |
| self?.gppSectionChangeSubscription = nil | |
| } | |
| if Thread.isMainThread { | |
| unsubscribe() | |
| } else { | |
| DispatchQueue.main.sync(execute: unsubscribe) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/RNUsercentricsModule.swift` around lines 38 - 52, The subscription is
created inside queue.async which can race with immediate JS calls like
setGPPConsent and miss the first event; change startObserving to register
gppSectionChangeSubscription synchronously (remove the outer queue.async and
create the subscription immediately in startObserving) while keeping event
dispatching onto queue if needed (i.e., inside the onGppSectionChange handler
use queue.async { self.sendEvent(...) }). Keep stopObserving disposing
gppSectionChangeSubscription on the queue (queue.async { ... }) and reference
the existing gppSectionChangeSubscription,
usercentricsManager.onGppSectionChange, startObserving, stopObserving,
setGPPConsent, and onGppSectionChangeEvent symbols when making the change.
| - (void)setGPPConsent:(NSString *)sectionName | ||
| fieldName:(NSString *)fieldName | ||
| value:(id)value; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -A3 -B1 '\bsetGPPConsent\b' ios/RNUsercentricsModuleSpec.h ios/RNUsercentricsModule.mm ios/RNUsercentricsModule.swiftRepository: Usercentrics/react-native-sdk
Length of output: 1424
Align iOS setGPPConsent bridge parameter type in the spec.
The header declares value as id, but ios/RNUsercentricsModule.mm (line 111) and ios/RNUsercentricsModule.swift (line 157) both require NSDictionary. Update the spec to match the actual implementation:
♻️ Proposed fix
- (void)setGPPConsent:(NSString *)sectionName
fieldName:(NSString *)fieldName
- value:(id)value;
+ value:(NSDictionary *)value;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - (void)setGPPConsent:(NSString *)sectionName | |
| fieldName:(NSString *)fieldName | |
| value:(id)value; | |
| - (void)setGPPConsent:(NSString *)sectionName | |
| fieldName:(NSString *)fieldName | |
| value:(NSDictionary *)value; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ios/RNUsercentricsModuleSpec.h` around lines 63 - 65, The spec declares
setGPPConsent with parameter type `id` for `value` but the implementations
(RNUsercentricsModule.mm and RNUsercentricsModule.swift) expect an NSDictionary;
update the RNUsercentricsModuleSpec declaration for the method `-
(void)setGPPConsent:(NSString *)sectionName fieldName:(NSString *)fieldName
value:(id)value;` to use `NSDictionary *value` instead of `id` so the
Objective-C bridge matches the .mm and Swift implementations.
|
CodeAnt AI is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR adds Global Privacy Platform support so React Native apps can read GPP string/data, update GPP consent, and receive GPP section change events from the native Usercentrics core. sequenceDiagram
participant App
participant SDK
participant UsercentricsCore
App->>SDK: Request GPP data or GPP string
SDK->>UsercentricsCore: Get GPP data or string
UsercentricsCore-->>SDK: Return GPP information
SDK-->>App: Resolve promise with GPP information
App->>SDK: setGPPConsent(section, field, value)
SDK->>UsercentricsCore: Apply GPP consent update
UsercentricsCore-->>SDK: GPP section change event
SDK-->>App: Trigger onGppSectionChange callback with payload
Generated by CodeAnt AI |
|
CodeAnt AI Incremental review completed. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
ios/RNUsercentricsModule.swift (1)
38-52:⚠️ Potential issue | 🟠 MajorRegister the GPP observer synchronously in
startObserving.
queue.asyncstill leaves a window where JS can subscribe and immediately callsetGPPConsentbeforegppSectionChangeSubscriptionexists, so the firstonGppSectionChangeevent can be missed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ios/RNUsercentricsModule.swift` around lines 38 - 52, The GPP observer is registered asynchronously which can miss the first event; change startObserving to register the subscription synchronously on queue (replace queue.async with queue.sync) so gppSectionChangeSubscription is set before JS can call setGPPConsent; keep the same weak self guard and disposal logic (stopObserving can remain using queue.async) and reference the startObserving/stopObserving methods, gppSectionChangeSubscription property, and usercentricsManager.onGppSectionChange callback when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@ios/RNUsercentricsModule.swift`:
- Around line 38-52: The GPP observer is registered asynchronously which can
miss the first event; change startObserving to register the subscription
synchronously on queue (replace queue.async with queue.sync) so
gppSectionChangeSubscription is set before JS can call setGPPConsent; keep the
same weak self guard and disposal logic (stopObserving can remain using
queue.async) and reference the startObserving/stopObserving methods,
gppSectionChangeSubscription property, and
usercentricsManager.onGppSectionChange callback when making the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6eb66210-10d3-4980-9a93-defbabbf8d1c
📒 Files selected for processing (2)
ios/Extensions/UsercentricsCMPData+Dict.swiftios/RNUsercentricsModule.swift
💤 Files with no reviewable changes (1)
- ios/Extensions/UsercentricsCMPData+Dict.swift
islameldesoky95
left a comment
There was a problem hiding this comment.
I would suggest a run on the iOS as well just to make sure things working for the new arch and fabric and that they are compatible
User description
Add GPP (Global Privacy Platform) support to the React Native SDK:
Summary by CodeRabbit
CodeAnt-AI Description
Add Global Privacy Platform (GPP) APIs and event support to the React Native SDK
What Changed
Impact
✅ Can read GPP string from the SDK✅ Can read structured GPP data from the SDK✅ Can set GPP consent and receive GPP section change events💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.