Skip to content

[MSDK-3287] GPP implementation#189

Open
uc-brunosilva wants to merge 2 commits intomasterfrom
feature/MSDK-3287
Open

[MSDK-3287] GPP implementation#189
uc-brunosilva wants to merge 2 commits intomasterfrom
feature/MSDK-3287

Conversation

@uc-brunosilva
Copy link
Collaborator

@uc-brunosilva uc-brunosilva commented Mar 10, 2026

User description

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
image

Summary by CodeRabbit

  • New Features
    • Added GPP (Global Privacy Platform) support: retrieve GPP data and GPP string, set GPP consent by section/field, and subscribe to real-time GPP section-change events.
  • Samples
    • New GPP Testing screen in the sample app to view GPP string/data, trigger consent changes, and see live section-change payloads.
  • Tests
    • Added unit tests and mocks covering the new GPP APIs and events.

CodeAnt-AI Description

Add Global Privacy Platform (GPP) APIs and event support to the React Native SDK

What Changed

  • Exposes GPP data and controls to JavaScript: getGPPString(), getGPPData(), and setGPPConsent(...) so apps can read the raw GPP string, read structured GPP sections, and update GPP consent from JS.
  • Emits onGppSectionChange events to JS and adds listener lifecycle handling so apps receive updates when a GPP section changes.
  • Adds JS models for GPP data and payload, a GPP testing screen in the sample app to view/set GPP values and observe events, and unit tests covering the new GPP methods and event wiring.

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:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

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:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

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.

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
Copy link

codeant-ai bot commented Mar 10, 2026

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 ·
Reddit ·
LinkedIn

@qodo-code-review
Copy link

Review Summary by Qodo

Add Global Privacy Platform (GPP) support to React Native SDK

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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"]
Loading

Grey Divider

File Changes

1. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt ✨ Enhancement +84/-0

Wire GPP bridge methods and event listener for Android

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt


2. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt ✨ Enhancement +15/-0

Define abstract GPP method signatures in module spec

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt


3. android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt ✨ Enhancement +36/-0

Serialize GPP data structures to React Native WritableMap

android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt


View more (21)
4. android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt 🐞 Bug fix +0/-1

Remove deprecated resurfacePeriodEnded field from serialization

android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt


5. ios/Extensions/GppData+Dict.swift ✨ Enhancement +12/-0

Add GppData extension for dictionary serialization

ios/Extensions/GppData+Dict.swift


6. ios/Extensions/GppSectionChangePayload+Dict.swift ✨ Enhancement +10/-0

Add GppSectionChangePayload extension for dictionary serialization

ios/Extensions/GppSectionChangePayload+Dict.swift


7. ios/Manager/UsercentricsManager.swift ✨ Enhancement +20/-0

Define GPP protocol methods in UsercentricsManager interface

ios/Manager/UsercentricsManager.swift


8. ios/RNUsercentricsModule.swift ✨ Enhancement +37/-1

Implement GPP methods and event listener on iOS platform

ios/RNUsercentricsModule.swift


9. ios/RNUsercentricsModule.mm ✨ Enhancement +10/-0

Export GPP bridge methods to Objective-C module interface

ios/RNUsercentricsModule.mm


10. ios/RNUsercentricsModuleSpec.h ✨ Enhancement +9/-0

Define GPP method signatures in iOS module specification

ios/RNUsercentricsModuleSpec.h


11. example/ios/exampleTests/Fake/FakeUsercentricsManager.swift 🧪 Tests +25/-0

Add GPP mock methods to example test fake manager

example/ios/exampleTests/Fake/FakeUsercentricsManager.swift


12. sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift 🧪 Tests +25/-0

Add GPP mock methods to sample test fake manager

sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift


13. src/NativeUsercentrics.ts ✨ Enhancement +7/-0

Add GPP method signatures to TurboModule interface

src/NativeUsercentrics.ts


14. src/fabric/NativeUsercentricsModule.ts ✨ Enhancement +6/-0

Add GPP method signatures to Fabric module interface

src/fabric/NativeUsercentricsModule.ts


15. src/models/GppData.tsx ✨ Enhancement +15/-0

Define GppData TypeScript model class

src/models/GppData.tsx


16. src/models/GppSectionChangePayload.tsx ✨ Enhancement +7/-0

Define GppSectionChangePayload TypeScript model class

src/models/GppSectionChangePayload.tsx


17. src/models/index.tsx ✨ Enhancement +2/-0

Export new GPP model classes from models barrel file

src/models/index.tsx


18. src/Usercentrics.tsx ✨ Enhancement +23/-1

Implement GPP methods and event listener in public API

src/Usercentrics.tsx


19. src/__tests__/index.test.ts 🧪 Tests +34/-1

Add unit tests for all GPP methods and event handling

src/tests/index.test.ts


20. src/__tests__/mocks.ts 🧪 Tests +16/-0

Add GPP data mock examples for testing

src/tests/mocks.ts


21. sample/src/App.tsx ✨ Enhancement +5/-3

Add GPP testing screen to sample app navigation

sample/src/App.tsx


22. sample/src/screens/GppTestingScreen.tsx ✨ Enhancement +89/-0

Create GPP testing UI screen with all GPP operations

sample/src/screens/GppTestingScreen.tsx


23. sample/src/screens/Home.tsx ✨ Enhancement +5/-6

Add navigation button to GPP testing screen

sample/src/screens/Home.tsx


24. sample/src/screens/index.tsx ✨ Enhancement +1/-0

Export GppTestingScreen from screens barrel file

sample/src/screens/index.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 10, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Null GPP consent ignored 🐞 Bug ✓ Correctness
Description
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).
Code

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[R141-144]

+    override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
+        val parsedValue = readableMapValueToAny(value) ?: return
+        usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
+    }
Evidence
On Android, ReadableType.Null maps to null, and setGPPConsent returns early on null, dropping the
update. iOS forwards the wrapped value even when it is null (NSNull), so platform behavior diverges.

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[140-144]
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[275-284]
ios/RNUsercentricsModule.swift[157-160]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


2. GPP composite values lost 🐞 Bug ✓ Correctness
Description
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.
Code

android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt[R10-20]

+    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())
+            }
Evidence
The Android bridge accepts composite values (ReadableType.Map/Array) and normalizes them into nested
Map/List types, but getGPPData serialization converts unknown types to strings instead of preserving
structure. The repo already has recursive Map/List -> WritableMap/WritableArray utilities that would
correctly serialize nested structures.

android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt[8-23]
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[277-294]
android/src/main/java/com/usercentrics/reactnative/extensions/ReadableMapExtensions.kt[74-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


3. Missing resurfacePeriodEnded key 🐞 Bug ✓ Correctness
Description
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.
Code

android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt[R219-223]

       "disabledSpecialFeatures" to disabledSpecialFeatures,
       "firstLayerShowDescriptions" to firstLayerShowDescriptions,
       "hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer,
-        "resurfacePeriodEnded" to resurfacePeriodEnded,
       "resurfacePurposeChanged" to resurfacePurposeChanged,
       "resurfaceVendorAdded" to resurfaceVendorAdded,
Evidence
The Android serialization map jumps from hideNonIabOnFirstLayer directly to resurfacePurposeChanged,
omitting resurfacePeriodEnded. The TS model defines resurfacePeriodEnded as a required boolean, and
iOS serialization still includes it, confirming the contract mismatch is Android-only.

android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt[219-223]
src/models/TCF2Settings.tsx[36-40]
ios/Extensions/UsercentricsCMPData+Dict.swift[209-213]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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


View more (1)
4. iOS EventEmitter bridge mismatch🐞 Bug ⛯ Reliability
Description
The iOS module now subclasses RCTEventEmitter, but the ObjC extern module still declares it as an
NSObject-based module. This mismatch can prevent React Native from treating the module as an event
emitter, breaking onGppSectionChange subscriptions created via NativeEventEmitter.
Code

ios/RNUsercentricsModule.swift[20]

+class RNUsercentricsModule: RCTEventEmitter {
Evidence
Swift implements RCTEventEmitter behaviors (supportedEvents/startObserving/stopObserving) but the
ObjC bridge registers the module as NSObject, while JS constructs NativeEventEmitter over the
module. If the bridge doesn’t reflect the event-emitter superclass, listener lifecycle may not be
wired correctly and events may not be delivered.

ios/RNUsercentricsModule.swift[19-52]
ios/RNUsercentricsModule.mm[10-12]
src/Usercentrics.tsx[23-26]
src/Usercentrics.tsx[165-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
iOS Swift module subclasses `RCTEventEmitter`, but `RNUsercentricsModule.mm` still declares it as an `NSObject` module, which can break event-emitter wiring.
### Issue Context
JS uses `new NativeEventEmitter(RNUsercentricsModule)` and subscribes to `onGppSectionChange`, relying on proper event-emitter integration.
### Fix Focus Areas
- ios/RNUsercentricsModule.swift[19-52]
- ios/RNUsercentricsModule.mm[8-12]
- src/Usercentrics.tsx[23-26]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Android Native GPP API
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt, android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt
Added methods getGPPData, getGPPString, setGPPConsent, event listener management (addListener, removeListeners, invalidate), and event subscription/emit logic for GPP section changes.
Android GPP Serialization
android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt
New extensions to serialize GppData and GppSectionChangePayload into WritableMap for RN bridging.
Android CMP Serialization tweak
android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
Removed resurfacePeriodEnded from TCF2Settings serialization.
iOS Native GPP API & Bridge
ios/RNUsercentricsModule.mm, ios/RNUsercentricsModule.swift, ios/RNUsercentricsModuleSpec.h
Exposed getGPPData, getGPPString, setGPPConsent to RN; module now extends RCTEventEmitter and implements event lifecycle (supportedEvents, startObserving, stopObserving) to emit onGppSectionChange.
iOS GPP Serialization
ios/Extensions/GppData+Dict.swift, ios/Extensions/GppSectionChangePayload+Dict.swift
Added toDictionary() helpers to convert GPP types to NSDictionary for the bridge.
iOS Manager Integration
ios/Manager/UsercentricsManager.swift
Protocol and implementation extended with getGPPData, getGPPString, setGPPConsent, and onGppSectionChange delegating to core/event APIs.
TypeScript models & exports
src/models/GppData.tsx, src/models/GppSectionChangePayload.tsx, src/models/index.tsx
Added GppData and GppSectionChangePayload classes and re-exported them from models index.
JS Public API & Fabric module
src/NativeUsercentrics.ts, src/Usercentrics.tsx, src/fabric/NativeUsercentricsModule.ts
Added getGPPData, getGPPString, setGPPConsent, and event wiring (onGppSectionChange, addListener, removeListeners) plus NativeEventEmitter integration.
Tests & Mocks
src/__tests__/index.test.ts, src/__tests__/mocks.ts, example/ios/exampleTests/Fake/FakeUsercentricsManager.swift, sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift
Added GPP examples/mocks (gppDataExample, gppSectionChangePayloadExample), mocks for new methods, and fake manager implementations including disposable event for section changes.
Sample app updates
sample/src/App.tsx, sample/src/screens/Home.tsx, sample/src/screens/GppTestingScreen.tsx, sample/src/screens/index.tsx
Added GppTestingScreen, navigation route, UI actions to call new GPP APIs, removed unused logger import, and minor Home screen UI changes.
iOS CMP Serialization tweak
ios/Extensions/UsercentricsCMPData+Dict.swift
Removed resurfacePeriodEnded entry from TCF2Settings dictionary output.

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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

size:XXL

Suggested reviewers

  • uc-brunosouza
  • islameldesoky95
  • rodrigo-leal-usercentrics

Poem

🐰 I hop and I listen, a GPP tune in my chest,

Data and events now pass every test,
Consent set with a flourish, payloads leap and sing,
From Android to iOS the rabbit bells ring,
Hooray for privacy—let joyful changes spring! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[MSDK-3287] GPP implementation' clearly and concisely summarizes the main change: adding Global Privacy Platform (GPP) support to the React Native SDK.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/MSDK-3287

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai bot added the size:L This PR changes 100-499 lines, ignoring generated files label Mar 10, 2026
@codeant-ai
Copy link

codeant-ai bot commented Mar 10, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

@pantoaibot
Copy link

pantoaibot bot commented Mar 10, 2026

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:

  • New public JS APIs:
    • Usercentrics.getGPPData(), getGPPString(), setGPPConsent(section, field, value)
    • Usercentrics.onGppSectionChange(callback) (event subscription via addListener/removeListeners)
    • Added models: GppData, GppSectionChangePayload and exported them.
  • Android:
    • RNUsercentricsModule: added getGPPData, getGPPString, setGPPConsent, addListener/removeListeners, event emission (onGppSectionChange) and lifecycle cleanup.
    • Helpers to convert ReadableMap to native Any for setGPPConsent and number normalization.
    • New extensions: GppData.serializeGppData() and GppSectionChangePayload.serializeGppPayload().
    • Updated RNUsercentricsModuleSpec to declare new native methods.
  • iOS:
    • UsercentricsManager protocol/implementation updated to include getGPPData/getGPPString/setGPPConsent and onGppSectionChange.
    • RNUsercentricsModule (Swift) now subclasses RCTEventEmitter and implements supportedEvents/startObserving/stopObserving to forward onGppSectionChange events to JS.
    • RNUsercentricsModule.mm and RNUsercentricsModuleSpec.h updated to expose new native methods.
    • Added GppData+Dict and GppSectionChangePayload+Dict extensions for bridging.
  • Native bridging / TurboModule updates:
    • NativeUsercentrics (TS) and fabric TurboModule spec updated to include GPP methods and addListener/removeListeners.
  • Tests & samples:
    • Unit test mocks and tests added for getGPPData, getGPPString and setGPPConsent.
    • Sample app: added GPPTesting screen and navigation entry demonstrating GPP APIs and event handling.
  • Misc:
    • Minor cleanup in CMP data extension (removed unused mapping entry).

Compatibility notes:

  • Adds new methods and events (non-breaking for existing calls). iOS RNUsercentricsModule implementation now inherits RCTEventEmitter to support events — internal change but enables event delivery to JS.

Reviewed by Panto AI

Comment on lines +140 to +144
@ReactMethod
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
val parsedValue = readableMapValueToAny(value) ?: return
usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Suggested change
@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
Copy link

codeant-ai bot commented Mar 10, 2026

CodeAnt AI finished reviewing your PR.

Comment on lines +8 to +29
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)
    }
}

Comment on lines +275 to +300
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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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)
}

Comment on lines +4 to +11
extension GppData {
func toDictionary() -> NSDictionary {
return [
"gppString": self.gppString,
"applicableSections": self.applicableSections,
"sections": self.sections,
]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
        ]
    }
}

Comment on lines +63 to +65
- (void)setGPPConsent:(NSString *)sectionName
fieldName:(NSString *)fieldName
value:(id)value;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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;

@pantoaibot
Copy link

pantoaibot bot commented Mar 10, 2026

Reviewed up to commit:a7bea02ac299987074920df6e7a918f60597186f

Reviewed by Panto AI

Comment on lines +141 to +144
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
val parsedValue = readableMapValueToAny(value) ?: return
usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines +10 to +20
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())
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Comment on lines 219 to 223
"disabledSpecialFeatures" to disabledSpecialFeatures,
"firstLayerShowDescriptions" to firstLayerShowDescriptions,
"hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer,
"resurfacePeriodEnded" to resurfacePeriodEnded,
"resurfacePurposeChanged" to resurfacePurposeChanged,
"resurfaceVendorAdded" to resurfaceVendorAdded,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

@qodo-code-review
Copy link

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: test-ios

Failed stage: Run tests [❌]

Failed test name: ""

Failure summary:

The GitHub Action failed during the iOS build/test step because Xcode could not compile Swift
sources for the CocoaPods target react-native-usercentrics, which caused the build to fail and tests
to be cancelled (exit code 65).

Key compile errors reported in the log:
- Cannot find type 'GppSectionChangePayload' in scope
(repeated multiple times), indicating a missing/undefined Swift type used by the
react-native-usercentrics code (notably involving GppSectionChangePayload+Dict.swift).
- Overriding
declaration requires an 'override' keyword (Swift compilation error).
- Redundant conformance of
'RNUsercentricsModule' to protocol 'RCTBridgeModule' (Swift error/warning treated as failure in this
build).

The failing build step is explicitly:
- Command SwiftCompile failed with a nonzero exit code
-
Failed build commands include EmitSwiftModule normal arm64 / SwiftEmitModule normal arm64 /
SwiftCompile normal arm64 for target react-native-usercentrics (from project Pods), compiling files
including /ios/Extensions/GppSectionChangePayload+Dict.swift and /ios/RNUsercentricsModule.swift.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

960:  Installing React-RCTText (0.81.4)
961:  Installing React-RCTVibration (0.81.4)
962:  Installing React-RuntimeApple (0.81.4)
963:  Installing React-RuntimeCore (0.81.4)
964:  Installing React-RuntimeHermes (0.81.4)
965:  Installing React-callinvoker (0.81.4)
966:  Installing React-cxxreact (0.81.4)
967:  Installing React-debug (0.81.4)
968:  Installing React-defaultsnativemodule (0.81.4)
969:  Installing React-domnativemodule (0.81.4)
970:  Installing React-featureflags (0.81.4)
971:  Installing React-featureflagsnativemodule (0.81.4)
972:  Installing React-graphics (0.81.4)
973:  Installing React-hermes (0.81.4)
974:  Installing React-idlecallbacksnativemodule (0.81.4)
975:  Installing React-jserrorhandler (0.81.4)
976:  Installing React-jsi (0.81.4)
...

1214:  ▸ Copying ExecutionContextManager.h
1215:  ▸ Copying ExecutionContext.h
1216:  ▸ Copying ConsoleMessage.h
1217:  ▸ Copying Base64.h
1218:  ▸ Copying React-jsiexecutor-umbrella.h
1219:  ▸ Copying JSINativeModules.h
1220:  ▸ Copying JSIExecutor.h
1221:  ▸ Copying threadsafe.h
1222:  ▸ Copying jsilib.h
1223:  ▸ Copying jsi.h
1224:  ▸ Copying jsi-inl.h
1225:  ▸ Copying instrumentation.h
1226:  ▸ Copying decorator.h
1227:  ▸ Copying React-jsi-umbrella.h
1228:  ▸ Copying JSIDynamic.h
1229:  ▸ Copying React-jserrorhandler-umbrella.h
1230:  ▸ Copying React-idlecallbacksnativemodule-umbrella.h
1231:  ▸ Copying React-hermes-umbrella.h
1232:  ▸ Copying HermesExecutorFactory.h
1233:  ▸ Copying React-graphics-umbrella.h
1234:  ▸ Copying React-featureflagsnativemodule-umbrella.h
1235:  ▸ Copying React-featureflags-umbrella.h
1236:  ▸ Copying React-domnativemodule-umbrella.h
1237:  ▸ Copying React-defaultsnativemodule-umbrella.h
1238:  ▸ Copying React-debug-umbrella.h
1239:  ▸ Copying TraceSection.h
1240:  ▸ Copying SystraceSection.h
1241:  ▸ Copying SharedProxyCxxModule.h
1242:  ▸ Copying RecoverableError.h
1243:  ▸ Copying ReactNativeVersion.h
...

1246:  ▸ Copying RAMBundleRegistry.h
1247:  ▸ Copying NativeToJsBridge.h
1248:  ▸ Copying NativeModule.h
1249:  ▸ Copying MoveWrapper.h
1250:  ▸ Copying ModuleRegistry.h
1251:  ▸ Copying MethodCall.h
1252:  ▸ Copying MessageQueueThread.h
1253:  ▸ Copying JsArgumentHelpers.h
1254:  ▸ Copying JsArgumentHelpers-inl.h
1255:  ▸ Copying JSModulesUnbundle.h
1256:  ▸ Copying JSIndexedRAMBundle.h
1257:  ▸ Copying JSExecutor.h
1258:  ▸ Copying JSBundleType.h
1259:  ▸ Copying JSBigString.h
1260:  ▸ Copying Instance.h
1261:  ▸ Copying ErrorUtils.h
1262:  ▸ Copying CxxNativeModule.h
...

1605:  ▸ Copying RCTI18nUtil.h
1606:  ▸ Copying RCTI18nManager.h
1607:  ▸ Copying RCTHTTPRequestHandler.h
1608:  ▸ Copying RCTGIFImageDecoder.h
1609:  ▸ Copying RCTFrameUpdate.h
1610:  ▸ Copying RCTFrameAnimation.h
1611:  ▸ Copying RCTFont.h
1612:  ▸ Copying RCTFileRequestHandler.h
1613:  ▸ Copying RCTFileReaderModule.h
1614:  ▸ Copying RCTFPSGraph.h
1615:  ▸ Copying RCTExceptionsManager.h
1616:  ▸ Copying RCTEventEmitter.h
1617:  ▸ Copying RCTEventDispatcherProtocol.h
1618:  ▸ Copying RCTEventDispatcher.h
1619:  ▸ Copying RCTEventAnimation.h
1620:  ▸ Copying RCTErrorInfo.h
1621:  ▸ Copying RCTErrorCustomizer.h
1622:  ▸ Copying RCTDynamicTypeRamp.h
...

1851:  ▸ Compiling SocketRocket-dummy.m
1852:  ▸ Compiling SRWebSocket.m
1853:  ▸ Compiling SRURLUtilities.m
1854:  ▸ Compiling SRSecurityPolicy.m
1855:  ▸ Compiling SRSIMDHelpers.m
1856:  ▸ Compiling SRRunLoopThread.m
1857:  ▸ Compiling SRRandom.m
1858:  ▸ Compiling SRProxyConnect.m
1859:  ▸ Compiling SRPinningSecurityPolicy.m
1860:  ▸ Compiling SRMutex.m
1861:  ▸ Compiling SRLog.m
1862:  ▸ Compiling SRIOConsumerPool.m
1863:  ▸ Compiling SRIOConsumer.m
1864:  ▸ Compiling SRHash.m
1865:  ▸ Compiling SRHTTPConnectMessage.m
1866:  ▸ Compiling SRError.m
1867:  ▸ Compiling SRDelegateController.m
...

2207:  ▸ Compiling RCTModalManager.m
2208:  ▸ Compiling RCTModalHostViewManager.m
2209:  ▸ Compiling RCTModalHostViewController.m
2210:  ▸ Compiling RCTModalHostView.m
2211:  ▸ Compiling RCTLayoutAnimationGroup.m
2212:  ▸ Compiling RCTLayoutAnimation.m
2213:  ▸ Compiling RCTLayout.m
2214:  ▸ Compiling RCTJSThread.m
2215:  ▸ Compiling RCTKeyCommands.m
2216:  ▸ Compiling RCTJSStackFrame.m
2217:  ▸ Compiling RCTImageSource.m
2218:  ▸ Compiling RCTI18nUtil.m
2219:  ▸ Compiling RCTEventEmitter.m
2220:  ▸ Compiling RCTFrameUpdate.m
2221:  ▸ Compiling RCTEventDispatcher.m
2222:  ▸ Compiling RCTErrorInfo.m
2223:  ▸ Compiling RCTDisplayLink.m
...

2277:  ▸ Compiling RCTRawTextShadowView.mm
2278:  ▸ Compiling RCTMultilineTextInputViewManager.mm
2279:  ▸ Compiling RCTMultilineTextInputView.mm
2280:  ▸ Compiling RCTInputAccessoryViewManager.mm
2281:  ▸ Compiling RCTInputAccessoryViewContent.mm
2282:  ▸ Compiling RCTInputAccessoryView.mm
2283:  ▸ Compiling RCTInputAccessoryShadowView.mm
2284:  ▸ Compiling RCTDynamicTypeRamp.mm
2285:  ▸ Compiling RCTConvert+Text.mm
2286:  ▸ Compiling RCTBaseTextViewManager.mm
2287:  ▸ Compiling RCTBaseTextShadowView.mm
2288:  ▸ Compiling RCTBaseTextInputViewManager.mm
2289:  ▸ Compiling RCTBaseTextInputView.mm
2290:  ▸ Compiling RCTBaseTextInputShadowView.mm
2291:  ▸ Compiling RCTBackedTextInputDelegateAdapter.mm
2292:  ▸ Processing React-jserrorhandler-Info.plist
2293:  ▸ Compiling RCTText_vers.c
2294:  ▸ Compiling StackTraceParser.cpp
2295:  ▸ Compiling JsErrorHandler.cpp
2296:  ▸ Compiling React-RCTText-dummy.m
2297:  ▸ Compiling NSTextStorage+FontScaling.m
2298:  ▸ Compiling React_jserrorhandler_vers.c
2299:  ▸ Compiling React-jserrorhandler-dummy.m
2300:  ▸ Running script 'Create Symlinks to Header Folders'
2301:  ▸ Touching reacthermes.framework (in target 'React-hermes' from project 'Pods')
2302:  ▸ Processing React-Core-Info.plist
2303:  ▸ Touching React_graphics.framework (in target 'React-graphics' from project 'Pods')
2304:  ▸ Compiling RCTTypedModuleConstants.mm
2305:  ▸ Compiling RCTConvertHelpers.mm
2306:  ▸ Compiling RCTTypeSafety_vers.c
2307:  ▸ Running script 'Create Symlinks to Header Folders'
2308:  ▸ Touching React_jserrorhandler.framework (in target 'React-jserrorhandler' from project 'Pods')
2309:  ▸ Touching React.framework (in target 'React-Core' from project 'Pods')
...

2844:  Skipping duplicate build file in Compile Sources build phase: /Users/runner/work/react-native-sdk/react-native-sdk/sample/ios/build/generated/ios/react/renderer/components/safeareacontext/States.cpp (in target 'ReactCodegen' from project 'Pods')
2845:  Skipping duplicate build file in Compile Sources build phase: /Users/runner/work/react-native-sdk/react-native-sdk/sample/ios/build/generated/ios/react/renderer/components/safeareacontext/States.cpp (in target 'ReactCodegen' from project 'Pods')
2846:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'ReactCodegen' from project 'Pods')
2847:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-utils' from project 'Pods')
2848:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-runtimescheduler' from project 'Pods')
2849:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-runtimeexecutor' from project 'Pods')
2850:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-rendererdebug' from project 'Pods')
2851:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-renderercss' from project 'Pods')
2852:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-rendererconsistency' from project 'Pods')
2853:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-performancetimeline' from project 'Pods')
2854:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-microtasksnativemodule' from project 'Pods')
2855:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-jsitooling' from project 'Pods')
2856:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-jsinspectortracing' from project 'Pods')
2857:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-jsinspectornetwork' from project 'Pods')
2858:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-jsinspectorcdp' from project 'Pods')
2859:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-jserrorhandler' from project 'Pods')
2860:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-idlecallbacksnativemodule' from project 'Pods')
2861:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-graphics' from project 'Pods')
2862:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-featureflagsnativemodule' from project 'Pods')
2863:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-featureflags' from project 'Pods')
2864:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-domnativemodule' from project 'Pods')
2865:  Testing failed:
2866:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-defaultsnativemodule' from project 'Pods')
...

2874:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-RCTFBReactNativeSpec' from project 'Pods')
2875:  Cannot find type 'GppSectionChangePayload' in scope
2876:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-NativeModulesApple' from project 'Pods')
2877:  Cannot find type 'GppSectionChangePayload' in scope
2878:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-Mapbuffer' from project 'Pods')
2879:  Overriding declaration requires an 'override' keyword
2880:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-ImageManager' from project 'Pods')
2881:  Overriding declaration requires an 'override' keyword
2882:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-FabricImage' from project 'Pods')
2883:  Redundant conformance of 'RNUsercentricsModule' to protocol 'RCTBridgeModule'
2884:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-FabricComponents' from project 'Pods')
2885:  Cannot find type 'GppSectionChangePayload' in scope
2886:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'React-Fabric' from project 'Pods')
2887:  Cannot find type 'GppSectionChangePayload' in scope
2888:  Run script build phase 'Create Symlinks to Header Folders' will be run during every build because it does not specify any outputs. To address this issue, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'RCT-Folly' from project 'Pods')
2889:  Command SwiftCompile failed with a nonzero exit code
2890:  Testing cancelled because the build failed.
2891:  ** TEST FAILED **
2892:  The following build commands failed:
2893:  EmitSwiftModule normal arm64 (in target 'react-native-usercentrics' from project 'Pods')
2894:  SwiftEmitModule normal arm64 Emitting\ module\ for\ react_native_usercentrics (in target 'react-native-usercentrics' from project 'Pods')
2895:  SwiftCompile normal arm64 Compiling\ BannerSettings+Dict.swift,\ CCPAData+Dict.swift,\ DispatchQueueManager.swift,\ GppData+Dict.swift,\ GppSectionChangePayload+Dict.swift,\ PresentationViewController.swift,\ ReadyStatus+Dict.swift,\ RNUsercentricsModule.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Extensions/BannerSettings+Dict.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Extensions/CCPAData+Dict.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Manager/DispatchQueueManager.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Extensions/GppData+Dict.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Extensions/GppSectionChangePayload+Dict.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Manager/PresentationViewController.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/Extensions/ReadyStatus+Dict.swift /Users/runner/work/react-native-sdk/react-native-sdk/ios/RNUsercentricsModule.swift (in target 'react-native-usercentrics' from project 'Pods')
2896:  Testing workspace sample with scheme sample
2897:  (4 failures)
2898:  ##[error]Process completed with exit code 65.
2899:  ##[group]Run actions/upload-artifact@v4

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when block only handles primitives (Boolean, Int, Double, String, null). Since GppData.sections contains Map<String, Any>, fields can hold arrays or nested objects. These will fall back to toString(), corrupting complex data. The codebase already has List<*>.serialize() and Map<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 GppData is semantically less precise than an interface since Usercentrics.getGPPData() returns raw bridge objects without calling this constructor. However, all 45+ other models in src/models/ consistently use the export class pattern. Changing only GppData to an interface would break this established architectural consistency.

This is not blocking and carries minimal risk (no instanceof checks 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6434203 and a7bea02.

📒 Files selected for processing (24)
  • android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
  • android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt
  • android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt
  • android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
  • example/ios/exampleTests/Fake/FakeUsercentricsManager.swift
  • ios/Extensions/GppData+Dict.swift
  • ios/Extensions/GppSectionChangePayload+Dict.swift
  • ios/Manager/UsercentricsManager.swift
  • ios/RNUsercentricsModule.mm
  • ios/RNUsercentricsModule.swift
  • ios/RNUsercentricsModuleSpec.h
  • sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift
  • sample/src/App.tsx
  • sample/src/screens/GppTestingScreen.tsx
  • sample/src/screens/Home.tsx
  • sample/src/screens/index.tsx
  • src/NativeUsercentrics.ts
  • src/Usercentrics.tsx
  • src/__tests__/index.test.ts
  • src/__tests__/mocks.ts
  • src/fabric/NativeUsercentricsModule.ts
  • src/models/GppData.tsx
  • src/models/GppSectionChangePayload.tsx
  • src/models/index.tsx
💤 Files with no reviewable changes (1)
  • android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt

Comment on lines +141 to +143
override fun setGPPConsent(sectionName: String, fieldName: String, value: ReadableMap) {
val parsedValue = readableMapValueToAny(value) ?: return
usercentricsProxy.instance.setGPPConsent(sectionName, fieldName, parsedValue)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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).

Comment on lines +38 to +52
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
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +63 to +65
- (void)setGPPConsent:(NSString *)sectionName
fieldName:(NSString *)fieldName
value:(id)value;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A3 -B1 '\bsetGPPConsent\b' ios/RNUsercentricsModuleSpec.h ios/RNUsercentricsModule.mm ios/RNUsercentricsModule.swift

Repository: 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.

Suggested change
- (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
Copy link

codeant-ai bot commented Mar 10, 2026

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 ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Mar 10, 2026
@codeant-ai
Copy link

codeant-ai bot commented Mar 10, 2026

Sequence Diagram

This 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
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link

codeant-ai bot commented Mar 10, 2026

CodeAnt AI Incremental review completed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
ios/RNUsercentricsModule.swift (1)

38-52: ⚠️ Potential issue | 🟠 Major

Register the GPP observer synchronously in startObserving.

queue.async still leaves a window where JS can subscribe and immediately call setGPPConsent before gppSectionChangeSubscription exists, so the first onGppSectionChange event 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

📥 Commits

Reviewing files that changed from the base of the PR and between a7bea02 and 04cfb1c.

📒 Files selected for processing (2)
  • ios/Extensions/UsercentricsCMPData+Dict.swift
  • ios/RNUsercentricsModule.swift
💤 Files with no reviewable changes (1)
  • ios/Extensions/UsercentricsCMPData+Dict.swift

Copy link
Collaborator

@islameldesoky95 islameldesoky95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants