Skip to content

[MSDK-3955] Expose ConsentOrPay settings in React Native bridge for P…#212

Open
asadraza-usercentrics wants to merge 1 commit into
masterfrom
feature/MSDK-3780-cop-bridge-react-native
Open

[MSDK-3955] Expose ConsentOrPay settings in React Native bridge for P…#212
asadraza-usercentrics wants to merge 1 commit into
masterfrom
feature/MSDK-3780-cop-bridge-react-native

Conversation

@asadraza-usercentrics
Copy link
Copy Markdown
Contributor

@asadraza-usercentrics asadraza-usercentrics commented May 20, 2026

User description

Summary by CodeRabbit

  • New Features
    • Added Consent or Pay configuration support to TCF2 settings, enabling control over feature enablement, vendor toggle visibility, publisher restrictions, and special features management across all supported platforms.

Review Change Stack


CodeAnt-AI Description

Expose Consent or Pay settings in React Native TCF2 data

What Changed

  • React Native apps now receive Consent or Pay settings as part of TCF2 settings on both Android and iOS
  • The new data includes whether Consent or Pay is enabled, whether vendor toggles are shown, and the publisher restriction and special feature settings
  • Apps can now read these settings directly from the bridge instead of missing them

Impact

✅ Consistent Consent or Pay setup across iOS and Android
✅ Fewer missing CMP settings in React Native apps
✅ Clearer control over vendor and special feature display

💡 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.

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 20, 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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR adds support for a "consent or pay" configuration feature across the TCF2 settings model. A new TCF2ConsentOrPaySettings class is introduced in TypeScript and wired into TCF2Settings, with corresponding serialization implementations on Android and iOS platforms that output the configuration to map and dictionary structures respectively.

Changes

TCF2 Consent-or-Pay Configuration

Layer / File(s) Summary
TypeScript model contract and wiring
src/models/TCF2Settings.tsx
TCF2Settings adds an optional consentOrPay field typed as TCF2ConsentOrPaySettings. A new exported TCF2ConsentOrPaySettings class defines the structure with enableConsentOrPay and showTogglesForVendors boolean flags, plus publisherRestrictions and specialFeatures Record<string, string> maps, initialized via constructor.
Android TCF2 serialization
android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
Android imports ConsentOrPaySettings and extends TCF2Settings.serialize() to include a consentOrPay entry. A new private ConsentOrPaySettings.serialize() extension converts the four configuration fields into a Map<String, Any?>.
iOS TCF2 serialization
ios/Extensions/UsercentricsCMPData+Dict.swift
iOS extends TCF2Settings.toDictionary() to include an optional consentOrPay key. A new ConsentOrPaySettings.toDictionary() extension returns a dictionary payload with the same four configuration fields.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • Usercentrics/react-native-sdk#200: Both PRs modify the serialization output of TCF2Settings across Android, iOS, and TypeScript; the prior PR updated the resurface-period field while this PR adds the new consentOrPay field to the same settings payload structure.

Suggested labels

size:L

Suggested reviewers

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

Poem

🐰 A consent-or-pay feature hops into view,
Across three platforms, old logic made new,
TypeScript defines it, Android and iOS comply,
Serialization maps dance through the sky,
Settings now richer, with toggles and strings! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: exposing ConsentOrPay settings in the React Native bridge, which aligns with the file changes across Android, iOS, and TypeScript models.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/MSDK-3780-cop-bridge-react-native

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/models/TCF2Settings.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

TypeError: prettier.resolveConfig.sync is not a function
Occurred while linting /src/models/TCF2Settings.tsx:1
Rule: "prettier/prettier"
at Program (/node_modules/eslint-plugin-prettier/eslint-plugin-prettier.js:138:40)
at ruleErrorHandler (/node_modules/eslint/lib/linter/linter.js:1076:28)
at /node_modules/eslint/lib/linter/safe-emitter.js:45:58
at Array.forEach ()
at Object.emit (/node_modules/eslint/lib/linter/safe-emitter.js:45:38)
at NodeEventGenerator.applySelector (/node_modules/eslint/lib/linter/node-event-generator.js:297:26)
at NodeEventGenerator.applySelectors (/node_modules/eslint/lib/linter/node-event-generator.js:326:22)
at NodeEventGenerator.enterNode (/node_modules/eslint/lib/linter/node-event-generator.js:340:14)
at CodePathAnalyzer.enterNode (/node_modules/eslint/lib/linter/code-path-analysis/code-path-analyzer.js:803:23)
at /node_modules/eslint/lib/linter/linter.js:1111:32


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.

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Expose ConsentOrPay settings in React Native bridge

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Expose ConsentOrPay settings in React Native bridge
• Add serialization for ConsentOrPaySettings on Android
• Add dictionary conversion for ConsentOrPaySettings on iOS
• Create TypeScript model for TCF2ConsentOrPaySettings
Diagram
flowchart LR
  A["ConsentOrPaySettings<br/>Native Models"] -->|"Serialize"| B["Android<br/>WritableMap"]
  A -->|"Convert"| C["iOS<br/>Dictionary"]
  B -->|"Bridge"| D["React Native<br/>TypeScript Models"]
  C -->|"Bridge"| D
  D -->|"Expose"| E["TCF2ConsentOrPaySettings<br/>Class"]

Loading

File Changes

1. android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt ✨ Enhancement +9/-0

Android ConsentOrPaySettings serialization

• Import ConsentOrPaySettings class
• Add consentOrPay field serialization to TCF2Settings
• Create serialize() extension function for ConsentOrPaySettings
• Map enableConsentOrPay, showTogglesForVendors, publisherRestrictions, and specialFeatures

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


2. ios/Extensions/UsercentricsCMPData+Dict.swift ✨ Enhancement +11/-0

iOS ConsentOrPaySettings dictionary conversion

• Add consentOrPay field to TCF2Settings dictionary conversion
• Create toDictionary() extension for ConsentOrPaySettings
• Map all ConsentOrPaySettings properties to dictionary format

ios/Extensions/UsercentricsCMPData+Dict.swift


3. src/models/TCF2Settings.tsx ✨ Enhancement +25/-0

TypeScript TCF2ConsentOrPaySettings model

• Add consentOrPay optional property to TCF2Settings class
• Update TCF2Settings constructor to accept consentOrPay parameter
• Create new TCF2ConsentOrPaySettings class with four properties
• Document publisherRestrictions and specialFeatures as TCF Purpose/Feature ID mappings

src/models/TCF2Settings.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 20, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Remediation recommended

1. Unbridged consentOrPay maps 🐞 Bug ☼ Reliability
Description
On iOS, ConsentOrPaySettings.toDictionary() forwards publisherRestrictions/specialFeatures as-is,
without the recursive bridging/normalization used elsewhere (e.g., GPP) to ensure values are
Foundation containers (NSDictionary/NSArray) suitable for the React Native bridge. This can leak
non-bridgeable nested types into the getCMPData() payload and cause runtime serialization failures
or missing fields in JS.
Code

ios/Extensions/UsercentricsCMPData+Dict.swift[R239-251]

Evidence
The new iOS serializer for consentOrPay directly returns the SDK’s map fields without any recursive
normalization, while the repo already contains explicit bridging utilities for nested
dictionaries/arrays in the GPP serializer—indicating this normalization is required in this codebase
when returning nested structures to React Native.

ios/Extensions/UsercentricsCMPData+Dict.swift[176-253]
ios/Extensions/GppData+Dict.swift[4-45]

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

### Issue description
`ConsentOrPaySettings.toDictionary()` returns `publisherRestrictions`/`specialFeatures` without converting nested collections/values into Foundation bridge-safe containers. The codebase already uses a dedicated recursive bridging approach for similarly-shaped nested data (GPP), but `consentOrPay` does not.

### Issue Context
- `TCF2Settings.toDictionary()` now includes `"consentOrPay": self.consentOrPay?.toDictionary() as Any`.
- `ConsentOrPaySettings.toDictionary()` currently forwards `publisherRestrictions` and `specialFeatures` directly.
- `GppData+Dict.swift` includes `bridgeValue`/`bridgeDictionary` to normalize nested dictionaries/arrays before passing them across the RN bridge.

### Fix Focus Areas
- ios/Extensions/UsercentricsCMPData+Dict.swift[239-253]
- ios/Extensions/GppData+Dict.swift[4-31]

### What to change
1. Reuse the existing recursive bridging logic for `publisherRestrictions` and `specialFeatures` (either by:
  - moving `bridgeValue`/`bridgeDictionary` into a shared helper accessible from both files, or
  - duplicating a minimal equivalent in `UsercentricsCMPData+Dict.swift`).
2. Ensure the returned payload uses only RN-bridge-safe types (NSDictionary/NSArray, NSNumber/NSString, NSNull where needed) for those nested fields.
3. Keep the public JS schema unchanged (keys/values as plain JS objects).

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


Grey Divider

Qodo Logo

@pantoaibot
Copy link
Copy Markdown

pantoaibot Bot commented May 20, 2026

PR Summary:

Add ConsentOrPay settings to the React Native bridge so JS can read them from CMP settings.

  • Android (UsercentricsCMPDataExtensions.kt)

    • Imported ConsentOrPaySettings and added "consentOrPay" to UsercentricsSettings.serialize().
    • Implemented ConsentOrPaySettings.serialize() returning enableConsentOrPay, showTogglesForVendors, publisherRestrictions, specialFeatures.
  • iOS (UsercentricsCMPData+Dict.swift)

    • Included "consentOrPay" in settings dictionary.
    • Added ConsentOrPaySettings.toDictionary() producing the same four fields.
  • JS/TS model (src/models/TCF2Settings.tsx)

    • Added optional consentOrPay?: TCF2ConsentOrPaySettings to TCF2Settings and constructor.
    • Added TCF2ConsentOrPaySettings class with enableConsentOrPay, showTogglesForVendors, publisherRestrictions, specialFeatures (publisherRestrictions/specialFeatures are string-keyed maps).

Impact and notes

  • Behavior: settings objects returned to JS now include an optional consentOrPay object when available.
  • Backwards compatible: field is optional; no breaking changes expected.
  • No dependency or performance changes.

Reviewed by Panto AI

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label May 20, 2026
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
ios/Extensions/UsercentricsCMPData+Dict.swift (1)

244-253: 💤 Low value

Consider using NSDictionary as return type for consistency.

Most other toDictionary() extensions in this file return NSDictionary (e.g., TCF2Settings.toDictionary() at line 177, CCPASettings.toDictionary() at line 153). This extension returns [String: Any], which is functionally equivalent since it's cast to Any at the call site (line 239), but the inconsistency might confuse future maintainers.

♻️ Optional refactor for consistency
 extension ConsentOrPaySettings {
-    func toDictionary() -> [String: Any] {
+    func toDictionary() -> NSDictionary {
         return [
             "enableConsentOrPay": self.enableConsentOrPay,
             "showTogglesForVendors": self.showTogglesForVendors,
             "publisherRestrictions": self.publisherRestrictions,
             "specialFeatures": self.specialFeatures
         ]
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/Extensions/UsercentricsCMPData`+Dict.swift around lines 244 - 253, Change
ConsentOrPaySettings.toDictionary() to return NSDictionary for consistency with
other extensions; update the signature to func toDictionary() -> NSDictionary
and return an NSDictionary instance (either by constructing NSDictionary with
the same key/value pairs or by casting the literal to NSDictionary) so callers
and other toDictionary() implementations (e.g., TCF2Settings.toDictionary,
CCPASettings.toDictionary) use the same type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ios/Extensions/UsercentricsCMPData`+Dict.swift:
- Around line 244-253: Change ConsentOrPaySettings.toDictionary() to return
NSDictionary for consistency with other extensions; update the signature to func
toDictionary() -> NSDictionary and return an NSDictionary instance (either by
constructing NSDictionary with the same key/value pairs or by casting the
literal to NSDictionary) so callers and other toDictionary() implementations
(e.g., TCF2Settings.toDictionary, CCPASettings.toDictionary) use the same type.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 299aeff8-a7db-4fef-952c-52eee10a0768

📥 Commits

Reviewing files that changed from the base of the PR and between 11f1dde and 20c2e3e.

📒 Files selected for processing (3)
  • android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
  • ios/Extensions/UsercentricsCMPData+Dict.swift
  • src/models/TCF2Settings.tsx

"changedPurposes" to changedPurposes?.serialize(),
"acmV2Enabled" to acmV2Enabled,
"selectedATPIds" to selectedATPIds,
"consentOrPay" to consentOrPay?.serialize(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[VALIDATION] You added "consentOrPay" to the TCF2Settings map (new line 250). Ensure the nested ConsentOrPaySettings data is serialized into a shape accepted by toWritableMap() (and ultimately the RN bridge) — specifically: 1) handle nulls safely (use consentOrPay?.let { ... } ), 2) normalize key types for nested maps (publisherRestrictions/specialFeatures) to string keys (JS requires string keys), and 3) convert nested structures to WritableMap/Array where applicable. Also update related Android unit-test expected maps so they include the new "consentOrPay" entry (see android/src/androidTest/java/com/usercentrics/reactnative/mock/GetCMPDataMock.kt expectedTCF2Settings around lines 448-509).

private fun TCF2Settings.serialize(): WritableMap {
    return mapOf(
        // ...existing fields...
        "selectedATPIds" to selectedATPIds,
        "consentOrPay" to consentOrPay?.serialize(),
    ).toWritableMap()
}

private fun ConsentOrPaySettings.serialize(): WritableMap = mapOf(
    "enableConsentOrPay" to enableConsentOrPay,
    "showTogglesForVendors" to showTogglesForVendors,
    // ensure keys are strings for JS
    "publisherRestrictions" to publisherRestrictions
        ?.mapKeys { it.key.toString() }
        ?.toMap(),
    "specialFeatures" to specialFeatures
        ?.mapKeys { it.key.toString() }
        ?.toMap(),
).toWritableMap()

Comment on lines +254 to +259
private fun ConsentOrPaySettings.serialize(): Map<String, Any?> = mapOf(
"enableConsentOrPay" to enableConsentOrPay,
"showTogglesForVendors" to showTogglesForVendors,
"publisherRestrictions" to publisherRestrictions,
"specialFeatures" to specialFeatures
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[REFACTORING] The new private fun ConsentOrPaySettings.serialize() (lines 254-259) currently returns a Kotlin Map<String,Any?> while other serializer helpers in this file return WritableMap (or rely on .toWritableMap()). For consistency and to avoid subtle nested conversion bugs, make this return a WritableMap (e.g. build a map and call .toWritableMap()). Also explicitly convert publisherRestrictions and specialFeatures into JS-friendly structures (Map keys -> String, values -> primitives). Example: publisherRestrictions?.mapKeys { it.key.toString() }?.toMap() and then include .toWritableMap(). This reduces runtime surprises across Android RN bridge conversions.

private fun ConsentOrPaySettings.serialize(): WritableMap = mapOf(
    "enableConsentOrPay" to enableConsentOrPay,
    "showTogglesForVendors" to showTogglesForVendors,
    "publisherRestrictions" to publisherRestrictions
        ?.mapKeys { it.key.toString() }
        ?.toMap(),
    "specialFeatures" to specialFeatures
        ?.mapKeys { it.key.toString() }
        ?.toMap(),
).toWritableMap()

"acmV2Enabled": self.acmV2Enabled,
"selectedATPIds": self.selectedATPIds,
"resurfacePeriod": self.resurfacePeriod,
"consentOrPay": self.consentOrPay?.toDictionary() as Any,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[VALIDATION] You added mapping "consentOrPay": self.consentOrPay?.toDictionary() (line 239). Ensure the toDictionary() produces JS-bridge-safe types: convert KotlinBoolean to Bool (.boolValue) where needed and convert any Kotlin map/dictionary keys to Swift String keys before returning. If consentOrPay fields can be nil, keep the optional handling (as you already do) but confirm callers expect null vs empty object.

extension TCF2Settings {
    func toDictionary() -> NSDictionary {
        return [
            // ...existing fields...
            "selectedATPIds": self.selectedATPIds,
            "resurfacePeriod": self.resurfacePeriod,
            "consentOrPay": self.consentOrPay?.toDictionary() as Any,
        ]
    }
}

extension ConsentOrPaySettings {
    func toDictionary() -> [String: Any] {
        return [
            "enableConsentOrPay": self.enableConsentOrPay.boolValue,
            "showTogglesForVendors": self.showTogglesForVendors.boolValue,
            "publisherRestrictions": self.publisherRestrictions as [String: Any],
            "specialFeatures": self.specialFeatures as [String: Any],
        ]
    }
}

Comment on lines +244 to +253
extension ConsentOrPaySettings {
func toDictionary() -> [String: Any] {
return [
"enableConsentOrPay": self.enableConsentOrPay,
"showTogglesForVendors": self.showTogglesForVendors,
"publisherRestrictions": self.publisherRestrictions,
"specialFeatures": self.specialFeatures
]
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL_BUG] The new extension ConsentOrPaySettings.toDictionary() returns fields directly (lines 244-253). This may cause compile/runtime issues: Kotlin booleans from the KMP bindings are often KotlinBoolean and need to be converted with .boolValue (see pattern in ios/Extensions/TCFData+Dict.swift lines ~85-115 where consent?.boolValue is used). Also publisherRestrictions and specialFeatures may be Kotlin map types — convert them to native [String: Any] (map keys to String) and ensure values are JS-serializable. Update to mirror existing conversion patterns (use .boolValue for KotlinBoolean and explicit dictionary transforms) to avoid crashes or incorrect values in JS.

extension ConsentOrPaySettings {
    func toDictionary() -> [String: Any] {
        return [
            "enableConsentOrPay": self.enableConsentOrPay.boolValue,
            "showTogglesForVendors": self.showTogglesForVendors.boolValue,
            // Ensure keys are Strings and values are JSON-serializable
            "publisherRestrictions": self.publisherRestrictions.reduce(into: [String: String]()) { result, entry in
                if let key = entry.key as? String, let value = entry.value as? String {
                    result[key] = value
                }
            },
            "specialFeatures": self.specialFeatures.reduce(into: [String: String]()) { result, entry in
                if let key = entry.key as? String, let value = entry.value as? String {
                    result[key] = value
                }
            },
        ]
    }
}

@pantoaibot
Copy link
Copy Markdown

pantoaibot Bot commented May 20, 2026

Reviewed up to commit:20c2e3ea766c19069cef61bc60e50fb7bb62bb37

Additional Suggestion
src/models/TCF2Settings.tsx, line:214-234 Consider tightening the types for publisherRestrictions and specialFeatures in TCF2ConsentOrPaySettings (lines 214-234). Right now they're Record. If the value can only be the literal 'flexible' (per comment), use a more precise type like Record or an enum/union. That improves type-safety across the codebase and makes platform serializers clearer about permitted values.
export class TCF2ConsentOrPaySettings {

    enableConsentOrPay: boolean
    showTogglesForVendors: boolean
    /** Maps TCF Purpose ID (as string) to "flexible". Absent entries are mandatory. */
    publisherRestrictions: Record<string, "flexible">
    /** Maps Special Feature ID (as string) to "flexible". Absent entries are mandatory. */
    specialFeatures: Record<string, "flexible">

    constructor(
        enableConsentOrPay: boolean,
        showTogglesForVendors: boolean,
        publisherRestrictions: Record<string, "flexible">,
        specialFeatures: Record<string, "flexible">,
    ) {
        this.enableConsentOrPay = enableConsentOrPay
        this.showTogglesForVendors = showTogglesForVendors
        this.publisherRestrictions = publisherRestrictions
        this.specialFeatures = specialFeatures
    }
}
Others - Update Android unit tests/mocks that construct expected TCF2Settings objects and TCF data (e.g. GetCMPDataMock.kt expectedTCF2Settings around lines 448-509 and any TCF mock builders) to include the new consentOrPay structure (or cover null case). Without updating tests, CI will fail or the new field will be unexpectedly missing from assertions.
// android/src/androidTest/java/com/usercentrics/reactnative/mock/GetCMPDataMock.kt
private val expectedTCF2Settings = hashMapOf(
    // ...existing expectations...
    "scope" to 1,
    "changedPurposes" to mapOf(
        "purposes" to listOf(1, 2, 3),
        "legIntPurposes" to listOf(1, 2, 3),
    ),
    // new field: consentOrPay
    "consentOrPay" to mapOf(
        "enableConsentOrPay" to false,
        "showTogglesForVendors" to false,
        "publisherRestrictions" to emptyMap<String, String>(),
        "specialFeatures" to emptyMap<String, String>(),
    ),
)
  • Update iOS sample/test mocks (e.g. sample/ios/sampleTests/Mock/CMPData+Mock.swift lines ~287-351) to include consentOrPay in TCF2Settings.mock() and update expected dictionaries in tests so they assert the new keys and shapes. Mirror the new fields and example values to keep tests in sync with SDK changes.
extension TCF2Settings {
  static func mock() -> TCF2Settings {
    let consentOrPay = ConsentOrPaySettings(
      enableConsentOrPay: KotlinBoolean(bool: true),
      showTogglesForVendors: KotlinBoolean(bool: false),
      publisherRestrictions: ["1": "flexible", "2": "flexible"],
      specialFeatures: ["1": "flexible"]
    )

    return .init(
      firstLayerTitle: "firstLayerTitle",
      secondLayerTitle: "secondLayerTitle",
      tabsPurposeLabel: "tabsPurposeLabel",
      tabsVendorsLabel: "tabsVendorsLabel",
      labelsFeatures: "labelsFeatures",
      labelsIabVendors: "labelsIabVendors",
      labelsNonIabPurposes: "labelsNonIabPurposes",
      labelsNonIabVendors: "labelsNonIabVendors",
      labelsPurposes: "labelsPurposes",
      vendorFeatures: "vendorFeatures",
      vendorLegitimateInterestPurposes: "vendorLegitimateInterestPurposes",
      vendorPurpose: "vendorPurpose",
      vendorSpecialFeatures: "vendorSpecialFeatures",
      vendorSpecialPurposes: "vendorSpecialPurposes",
      togglesConsentToggleLabel: "togglesConsentToggleLabel",
      togglesLegIntToggleLabel: "togglesLegIntToggleLabel",
      buttonsAcceptAllLabel: "buttonsAcceptAllLabel",
      buttonsDenyAllLabel: "buttonsDenyAllLabel",
      buttonsSaveLabel: "buttonsSaveLabel",
      linksManageSettingsLabel: "linksManageSettingsLabel",
      linksVendorListLinkLabel: "linksVendorListLinkLabel",
      togglesSpecialFeaturesToggleOn: "togglesSpecialFeaturesToggleOn",
      togglesSpecialFeaturesToggleOff: "togglesSpecialFeaturesToggleOff",
      firstLayerMobileVariant: .full,
      firstLayerHideToggles: true,
      secondLayerHideToggles: true,
      hideLegitimateInterestToggles: true,
      categoriesOfDataLabel: "categoriesOfDataLabel",
      dataRetentionPeriodLabel: "dataRetentionPeriodLabel",
      legitimateInterestLabel: "legitimateInterestLabel",
      version: "version",
      examplesLabel: "examplesLabel",
      cmpId: 123,
      cmpVersion: 123,
      showDataSharedOutsideEUText: true,
      dataSharedOutsideEUText: "dataSharedOutsideEUText",
      vendorIdsOutsideEUList: [1,2,3],
      firstLayerHideButtonDeny: true,
      hideButtonManageSettings: false,
      secondLayerHideButtonDeny: true,
      publisherCountryCode: "publisherCountryCode",
      purposeOneTreatment: true,
      selectedVendorIds: [1,2,3],
      gdprApplies: true,
      selectedStacks: [1,2,3],
      scope: .global,
      disabledSpecialFeatures: [1,2,3],
      firstLayerShowDescriptions: true,
      hideNonIabOnFirstLayer: true,
      resurfacePeriod: 1,
      resurfacePurposeChanged: true,
      resurfaceVendorAdded: true,
      firstLayerDescription: "firstLayerDescription",
      firstLayerAdditionalInfo: "firstLayerAdditionalInfo",
      secondLayerDescription: "secondLayerDescription",
      appLayerNoteResurface: "appLayerNoteResurface",
      firstLayerNoteResurface: "firstLayerNoteResurface",
      changedPurposes: .mock(),
      acmV2Enabled: true,
      selectedATPIds: [43,46,55],
      resurfaceATPListChanged: false,
      atpListTitle: "Google Providers",
      consentOrPay: consentOrPay
    )
  }
}

Reviewed by Panto AI

@qodo-code-review
Copy link
Copy Markdown

CI Feedback 🧐

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

Action: test-android

Failed stage: Run SDK Unit Tests [❌]

Failed test name: ""

Failure summary:

The action failed because the Android Gradle build crashed during Kotlin compilation for the module
:react-native-usercentrics.
- Gradle task :react-native-usercentrics:compileDebugKotlin failed due
to Kotlin compilation errors in
android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt.
-
The primary error is an unresolved symbol: Unresolved reference ConsentOrPaySettings (e.g., at
UsercentricsCMPDataExtensions.kt:15:46), along with multiple subsequent Overload resolution
ambiguity and Cannot infer type errors triggered by that missing type/member (also includes
unresolved references like consentOrPay, enableConsentOrPay, showTogglesForVendors, etc., around
lines ~250–258).
- Because compilation failed, Gradle exited with code 1, causing the GitHub Action
step to fail.

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

257:  NODE_JS_VERSION: 20.19.5
258:  GRADLE_VERSION: 8.13
259:  JAVA_HOME: /Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/arm64/Contents/Home
260:  JAVA_HOME_17_ARM64: /Users/runner/hostedtoolcache/Java_Temurin-Hotspot_jdk/17.0.18-8/arm64/Contents/Home
261:  ##[endgroup]
262:  Downloading https://services.gradle.org/distributions/gradle-8.13-all.zip
263:  ......................10%......................20%......................30%......................40%......................50%......................60%......................70%.......................80%......................90%......................100%
264:  Welcome to Gradle 8.13!
265:  Here are the highlights of this release:
266:  - Daemon JVM auto-provisioning
267:  - Enhancements for Scala plugin and JUnit testing
268:  - Improvements for build authors and plugin developers
269:  For more details see https://docs.gradle.org/8.13/release-notes.html
270:  Starting a Gradle Daemon (subsequent builds will be faster)
271:  Configuration on demand is an incubating feature.
272:  > Task :gradle-plugin:react-native-gradle-plugin:checkKotlinGradlePluginConfigurationErrors SKIPPED
273:  > Task :gradle-plugin:shared:checkKotlinGradlePluginConfigurationErrors SKIPPED
274:  > Task :gradle-plugin:react-native-gradle-plugin:pluginDescriptors
...

282:  > Task :gradle-plugin:react-native-gradle-plugin:compileJava NO-SOURCE
283:  > Task :gradle-plugin:react-native-gradle-plugin:classes
284:  > Task :gradle-plugin:react-native-gradle-plugin:jar
285:  > Configure project :react-native-usercentrics
286:  WARNING: The option setting 'android.enableResourceOptimizations=false' is deprecated.
287:  The current default is 'true'.
288:  It will be removed in version 9.0 of the Android Gradle plugin.
289:  WARNING: The option setting 'android.defaults.buildfeatures.buildconfig=true' is deprecated.
290:  The current default is 'false'.
291:  It will be removed in version 10.0 of the Android Gradle plugin.
292:  To keep using this feature, add the following to your module-level build.gradle files:
293:  android.buildFeatures.buildConfig = true
294:  or from Android Studio, click: `Refactor` > `Migrate BuildConfig to Gradle Build Files`.
295:  w: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/build.gradle.kts:27:9: 'targetSdk: Int?' is deprecated. Will be removed from library DSL in v9.0. Use testOptions.targetSdk or/and lint.targetSdk instead
296:  w: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/build.gradle.kts:58:15: 'getter for buildDir: File!' is deprecated. Deprecated in Java
297:  > Task :react-native-usercentrics:checkKotlinGradlePluginConfigurationErrors SKIPPED
298:  > Task :react-native-usercentrics:generateCodegenSchemaFromJavaScript
...

305:  > Task :react-native-usercentrics:packageDebugResources
306:  > Task :react-native-usercentrics:preDebugUnitTestBuild
307:  > Task :react-native-usercentrics:javaPreCompileDebug
308:  > Task :react-native-usercentrics:javaPreCompileDebugUnitTest
309:  > Task :react-native-usercentrics:parseDebugLocalResources
310:  > Task :react-native-usercentrics:koverFindJar
311:  > Task :react-native-usercentrics:preReleaseBuild
312:  > Task :react-native-usercentrics:generateReleaseBuildConfig
313:  > Task :react-native-usercentrics:generateReleaseResValues FROM-CACHE
314:  > Task :react-native-usercentrics:generateReleaseResources
315:  > Task :react-native-usercentrics:generateDebugRFile
316:  > Task :react-native-usercentrics:packageReleaseResources
317:  > Task :react-native-usercentrics:compileDebugKotlin
318:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:15:46 Unresolved reference 'ConsentOrPaySettings'.
319:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:29:37 Overload resolution ambiguity between candidates:
320:  > Task :react-native-usercentrics:compileDebugKotlin FAILED
321:  fun UsercentricsSettings.serialize(): WritableMap
322:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
323:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:30:39 Cannot infer type for this parameter. Specify it explicitly.
324:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:30:48 Overload resolution ambiguity between candidates:
325:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
326:  fun UsercentricsService.serialize(): WritableMap
327:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:31:43 Cannot infer type for this parameter. Specify it explicitly.
328:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:31:52 Overload resolution ambiguity between candidates:
329:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
330:  fun UsercentricsCategory.serialize(): WritableMap
331:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:33:45 Overload resolution ambiguity between candidates:
332:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
333:  fun UsercentricsLocation.serialize(): WritableMap
334:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:34:41 Overload resolution ambiguity between candidates:
335:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
336:  fun LegalBasisLocalization.serialize(): WritableMap
337:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:40:28 Overload resolution ambiguity between candidates:
338:  fun UsercentricsLabels.serialize(): Map<String, Any?>
339:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
340:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:44:38 Overload resolution ambiguity between candidates:
341:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
342:  fun SecondLayer.serialize(): WritableMap
343:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:46:25 Overload resolution ambiguity between candidates:
344:  fun TCF2Settings.serialize(): WritableMap
345:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
346:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:47:25 Overload resolution ambiguity between candidates:
347:  fun CCPASettings.serialize(): WritableMap
348:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
349:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:49:37 Overload resolution ambiguity between candidates:
350:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
351:  fun FirstLayer.serialize(): WritableMap
352:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:61:43 Overload resolution ambiguity between candidates:
353:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
354:  fun UsercentricsCustomization.serialize(): WritableMap
355:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:62:33 Overload resolution ambiguity between candidates:
356:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
357:  fun VariantsSettings?.serialize(): Any?
358:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:65:43 Cannot infer type for this parameter. Specify it explicitly.
359:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:65:43 Not enough information to infer type argument for 'R'.
360:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:65:52 Overload resolution ambiguity between candidates:
361:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
362:  fun PublishedApp.serialize(): Any
363:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:247:47 Overload resolution ambiguity between candidates:
364:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
365:  fun TCF2ChangedPurposes?.serialize(): Any?
366:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:250:27 Unresolved reference 'consentOrPay'.
367:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:254:13 Unresolved reference 'ConsentOrPaySettings'.
368:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:255:29 Unresolved reference 'enableConsentOrPay'.
369:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:256:32 Unresolved reference 'showTogglesForVendors'.
370:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:257:32 Unresolved reference 'publisherRestrictions'.
371:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:258:26 Unresolved reference 'specialFeatures'.
372:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:301:27 Overload resolution ambiguity between candidates:
373:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
374:  fun CustomizationColor.serialize(): WritableMap
375:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:302:25 Overload resolution ambiguity between candidates:
376:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
377:  fun CustomizationFont.serialize(): WritableMap
378:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:401:42 Overload resolution ambiguity between candidates:
379:  fun ConsentDisclosureObject?.serialize(): Any?
380:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
381:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:456:53 Cannot infer type for this parameter. Specify it explicitly.
382:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:456:62 Overload resolution ambiguity between candidates:
383:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
384:  fun AdTechProvider.serialize(): WritableMap
385:  e: file:///Users/runner/work/react-native-sdk/react-native-sdk/android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:471:37 Overload resolution ambiguity between candidates:
386:  fun <ERROR TYPE REF: Symbol not found for ConsentOrPaySettings>.serialize(): Map<String, Any?>
387:  fun TranslationAriaLabels.serialize(): WritableMap
388:  > Task :react-native-usercentrics:generateDebugUnitTestStubRFile
389:  23 actionable tasks: 22 executed, 1 from cache
390:  FAILURE: Build failed with an exception.
391:  * What went wrong:
392:  Execution failed for task ':react-native-usercentrics:compileDebugKotlin'.
393:  > A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
394:  > Compilation error. See log for more details
395:  * Try:
396:  > Run with --stacktrace option to get the stack trace.
397:  > Run with --info or --debug option to get more log output.
398:  > Run with --scan to get full insights.
399:  > Get more help at https://help.gradle.org.
400:  BUILD FAILED in 3m 14s
401:  ##[error]Process completed with exit code 1.
402:  ##[group]Run actions/upload-artifact@v4

Comment on lines +257 to +258
"publisherRestrictions" to publisherRestrictions,
"specialFeatures" to specialFeatures
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: publisherRestrictions and specialFeatures are forwarded as raw nested maps, but the React Native serializer in this module only safely handles nested maps with string keys and primitive JS-compatible values. These Consent-or-Pay maps are keyed by IDs and can contain non-string/non-primitive values from the SDK, which can cause runtime cast/serialization failures or silently dropped entries. Normalize both maps before exporting (stringify keys and map values to bridge-safe primitives). [type error]

Severity Level: Major ⚠️
- ❌ Android `getCMPData` can crash when Consent-or-Pay maps complex.
- ⚠️ JS cannot reliably read Consent-or-Pay restrictions on Android.
Steps of Reproduction ✅
1. From JS, call `Usercentrics.getCMPData()` as in
`sample/src/screens/CustomUI.tsx:12-15`, which invokes the exported React Native method to
fetch CMP data.

2. On Android, this is bridged to `getCMPData` in
`android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt:85-87`, which
executes `usercentricsProxy.instance.getCMPData().serialize()` and returns the result to
JS.

3. `UsercentricsCMPData.serialize()` in
`android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt:27-36`
calls `UsercentricsSettings.serialize()` and then `TCF2Settings.serialize()` (lines
189-251), which includes `"consentOrPay" to consentOrPay?.serialize()` (line 250) and,
inside `ConsentOrPaySettings.serialize()` (lines 254-259), forwards
`"publisherRestrictions" to publisherRestrictions` and `"specialFeatures" to
specialFeatures` without any normalization.

4. The resulting `Map<String, Any?>` is converted to a React Native `WritableMap` via
`Map<String, Any?>.toWritableMap()` in
`android/src/main/java/com/usercentrics/reactnative/extensions/ReadableMapExtensions.kt:15-56`,
where nested maps are handled by `is Map<*, *>` and cast to `Map<String, Any>` (line
36-38) and only primitive / JS-safe value types are supported in the `when` branches. If
`ConsentOrPaySettings.publisherRestrictions` or `specialFeatures` (coming from the native
SDK) use non-String keys or non-primitive values (e.g. enums or other objects), this cast
or value handling will either throw at runtime (ClassCastException on keys) or silently
drop entries, resulting in broken or incomplete `consentOrPay` data returned by
`getCMPData()` to JS.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** android/src/main/java/com/usercentrics/reactnative/extensions/UsercentricsCMPDataExtensions.kt
**Line:** 257:258
**Comment:**
	*Type Error: `publisherRestrictions` and `specialFeatures` are forwarded as raw nested maps, but the React Native serializer in this module only safely handles nested maps with string keys and primitive JS-compatible values. These Consent-or-Pay maps are keyed by IDs and can contain non-string/non-primitive values from the SDK, which can cause runtime cast/serialization failures or silently dropped entries. Normalize both maps before exporting (stringify keys and map values to bridge-safe primitives).

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.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +249 to +250
"publisherRestrictions": self.publisherRestrictions,
"specialFeatures": self.specialFeatures
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The Consent-or-Pay dictionaries are exposed directly to JS without converting keys/values to RN-safe JSON primitives. If these SDK maps use numeric keys or non-primitive value types, bridging to JavaScript can fail or produce unusable objects. Convert them into [String: String] (or another explicit JS-safe shape) before putting them into the exported dictionary. [api mismatch]

Severity Level: Major ⚠️
- ❌ iOS bridge may fail when Consent-or-Pay maps non-primitive.
- ⚠️ JS clients may see malformed Consent-or-Pay data on iOS.
Steps of Reproduction ✅
1. From JS, call `Usercentrics.getCMPData()` as demonstrated in
`sample/src/screens/CustomUI.tsx:12-15`; on iOS this is bridged to the native module's
`getCMPData` (verified in
`sample/ios/sampleTests/RNUsercentricsModuleTests.swift:529-533`, which expects an
`NSDictionary` result).

2. The native module builds that `NSDictionary` via `UsercentricsCMPData.toDictionary()`
in `ios/Extensions/UsercentricsCMPData+Dict.swift:4-13`, which embeds
`self.settings.toDictionary()`; `UsercentricsSettings.toDictionary()` (lines 17-50) then
includes `"tcf2": self.tcf2?.toDictionary() as Any` (line 26).

3. `TCF2Settings.toDictionary()` in
`ios/Extensions/UsercentricsCMPData+Dict.swift:176-241` includes `"consentOrPay":
self.consentOrPay?.toDictionary() as Any` (line 239), which calls
`ConsentOrPaySettings.toDictionary()` defined at lines 244-252.

4. `ConsentOrPaySettings.toDictionary()` currently forwards `"publisherRestrictions":
self.publisherRestrictions` and `"specialFeatures": self.specialFeatures` directly (lines
247-250). If the underlying `ConsentOrPaySettings` in the Usercentrics iOS SDK represents
these as dictionaries with non-String keys or non-primitive values (e.g. enums/objects),
the React Native bridge—which only safely transports
NSString/NSNumber/NSArray/NSDictionary/NSNull—can fail to marshal them correctly, leading
to runtime bridging errors or JS receiving unusable structures where TypeScript expects
`Record<string, string>` (`src/models/TCF2Settings.tsx:15-22`).

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** ios/Extensions/UsercentricsCMPData+Dict.swift
**Line:** 249:250
**Comment:**
	*Api Mismatch: The Consent-or-Pay dictionaries are exposed directly to JS without converting keys/values to RN-safe JSON primitives. If these SDK maps use numeric keys or non-primitive value types, bridging to JavaScript can fail or produce unusable objects. Convert them into `[String: String]` (or another explicit JS-safe shape) before putting them into the exported dictionary.

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.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented May 20, 2026

CodeAnt AI finished reviewing your PR.

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

Labels

size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant