Skip to content

MSDK-3293: type mismatch in turbo module#183

Merged
uc-brunosouza merged 5 commits intomasterfrom
MSDK-3293/type-mismatch-in-turboModule
Feb 20, 2026
Merged

MSDK-3293: type mismatch in turbo module#183
uc-brunosouza merged 5 commits intomasterfrom
MSDK-3293/type-mismatch-in-turboModule

Conversation

@uc-brunosouza
Copy link
Collaborator

@uc-brunosouza uc-brunosouza commented Feb 19, 2026

User description

Summary by CodeRabbit

Release Notes

  • Refactor
    • Improved parameter type handling in consent decision processing and tracking operations across mobile platforms for enhanced stability and consistency.

CodeAnt-AI Description

Fix TurboModule numeric parameter handling so JS numbers are accepted by consent APIs

What Changed

  • Bridge methods that previously required integer arguments now accept JavaScript numbers (double) for CMP ID, consent-type, TCF layer, analytics events, and related consent actions on both Android and iOS
  • Numeric values received from JS are converted to the integer types expected by native SDKs before use, preventing type mismatch failures
  • No change to JavaScript API usage — callers can continue passing numbers and promise-based responses behave the same

Impact

✅ Fewer TurboModule type errors when calling consent APIs
✅ Fewer runtime crashes from numeric argument mismatches
✅ Consistent acceptance of JS numeric arguments across Android and iOS

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

@uc-brunosouza uc-brunosouza self-assigned this Feb 19, 2026
@codeant-ai
Copy link

codeant-ai bot commented Feb 19, 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

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

This PR systematically updates React Native bridge method signatures across Android and iOS implementations, changing numeric parameter types from Int/NSInteger to Double. Internal type conversions (toInt(), Int()) are applied where needed for enum initialization and downstream method calls, preserving existing functionality.

Changes

Cohort / File(s) Summary
Android Bridge Spec
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt
Updated 9 public abstract method signatures to accept Double instead of Int parameters for CMP ID, consent actions, TCF operations, and tracking.
Android Bridge Implementation
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
Implemented corresponding methods with Double parameter types, converting to Int via toInt() for enum indexing and internal method calls while preserving logic flow.
iOS Objective-C Bridge
ios/RNUsercentricsModule.mm
Updated 9 RCT_EXTERN_METHOD declarations to change parameter types from NSInteger*/BOOL* to double for CMP configuration, consent operations, and analytics methods.
iOS Swift Bridge Implementation
ios/RNUsercentricsModule.swift
Implemented corresponding @objc methods accepting Double parameters, converting to Int where needed for internal type construction and maintaining existing error handling.
iOS Bridge Spec
ios/RNUsercentricsModuleSpec.h
Updated public protocol method signatures to specify double instead of NSInteger for numeric parameters across consent and tracking operations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~20 minutes

Possibly related PRs

  • Usercentrics/react-native-sdk#177: Modifies the same React Native bridge method signatures (denyAllForTCF, acceptAll, setCMPId, track, etc.) across the same module files.

Suggested labels

Review effort 1/5, size:XL

Suggested reviewers

  • uc-brunosilva
  • islameldesoky95

Poem

🐰 Through Android and iOS we hop with cheer,
Double parameters, crystal-clear!
From Int to Double, the bridge now sings,
With conversions tight—oh, what it brings!
A hop, a skip, the types align,
React Native flows, everything's fine! ✨

🚥 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 clearly identifies the specific issue being addressed (type mismatch in turbo module) and references the ticket ID for context.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch MSDK-3293/type-mismatch-in-turboModule

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

Review Summary by Qodo

Fix type mismatch in turbo module by converting int to double

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Convert integer parameters to double across React Native bridge
• Add explicit type conversions from double to int in implementations
• Ensure type compatibility between JavaScript and native layers
• Update method signatures in Android and iOS modules consistently
Diagram
flowchart LR
  JS["JavaScript Layer<br/>sends Double values"]
  Bridge["React Native Bridge<br/>expects Double parameters"]
  Android["Android Implementation<br/>converts Double to Int"]
  iOS["iOS Implementation<br/>converts Double to Int"]
  JS -->|"Double parameters"| Bridge
  Bridge -->|"Double"| Android
  Bridge -->|"Double"| iOS
  Android -->|"toInt()"| NativeAndroid["Native Android API<br/>expects Int"]
  iOS -->|"Int()"| NativeIOS["Native iOS API<br/>expects Int32"]
Loading

Grey Divider

File Changes

1. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt 🐞 Bug fix +20/-20

Convert int parameters to double with conversions

• Changed 11 method parameter types from Int to Double
• Added .toInt() conversions for all double parameters before passing to native APIs
• Methods affected: setCMPId, acceptAllForTCF, acceptAll, denyAllForTCF, denyAll,
 saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA, track

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


2. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt 🐞 Bug fix +10/-10

Update abstract method signatures to double

• Updated abstract method signatures to use Double instead of Int
• Affects 9 methods: setCMPId, acceptAll, acceptAllForTCF, denyAll, denyAllForTCF,
 saveDecisions, saveDecisionsForTCF, saveOptOutForCCPA, track

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


3. ios/RNUsercentricsModule.swift 🐞 Bug fix +22/-22

Convert int parameters to double with conversions

• Changed 10 method parameter types from Int to Double
• Added Int() type conversions for all double parameters before passing to native APIs
• Methods affected: setCMPId, acceptAllForTCF, acceptAll, denyAllForTCF, denyAll,
 saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA, track

ios/RNUsercentricsModule.swift


View more (2)
4. ios/RNUsercentricsModule.mm 🐞 Bug fix +12/-12

Update extern method declarations to double

• Updated RCT_EXTERN_METHOD declarations to use double instead of NSInteger
• Affects 9 methods: setCMPId, acceptAll, acceptAllForTCF, denyAll, denyAllForTCF,
 saveDecisions, saveDecisionsForTCF, saveOptOutForCCPA, track

ios/RNUsercentricsModule.mm


5. ios/RNUsercentricsModuleSpec.h 🐞 Bug fix +12/-12

Update header method signatures to double

• Changed method parameter types from NSInteger to double in header file
• Affects 9 methods: setCMPId, acceptAll, acceptAllForTCF, denyAll, denyAllForTCF,
 saveDecisions, saveDecisionsForTCF, saveOptOutForCCPA, track

ios/RNUsercentricsModuleSpec.h


Grey Divider

Qodo Logo

@codeant-ai
Copy link

codeant-ai bot commented Feb 19, 2026

Sequence Diagram

This PR changes React Native bridge method signatures from integer to double to match JavaScript Number types in TurboModules, then converts those doubles to native integers before calling the Usercentrics SDK. The diagram shows the main success path for a numeric consent action (e.g., acceptAll).

sequenceDiagram
    participant JS as JavaScript
    participant Bridge as React Native TurboModule
    participant Native as Native Module (Android/iOS)
    participant SDK as Usercentrics SDK

    JS->>Bridge: call acceptAll(consentType: Number)
    Bridge->>Native: receives consentType as Double (TurboModule)
    Native->>Native: convert Double -> Int (consentType.toInt())
    Native->>SDK: call acceptAll(consentType: Int)
    SDK-->>Native: services/result
    Native-->>Bridge: resolved array/result
    Bridge-->>JS: promise resolved with services/result
Loading

Generated by CodeAnt AI

@pantoaibot
Copy link

pantoaibot bot commented Feb 19, 2026

PR Summary:

Summary:
Fixes TurboModule type mismatches by changing React-Native-exposed integer parameters to Double (RN JS numbers) and converting them to native integer types inside Android/iOS implementations.

Changes:

  • Converted RN-exposed integer params to Double across Android and iOS turbo module/specs:
    • Methods affected: setCMPId, acceptAll, acceptAllForTCF, denyAll, denyAllForTCF, saveDecisions, saveDecisionsForTCF, saveOptOutForCCPA, track (and related spec declarations).
  • Android (Kotlin):
    • RNUsercentricsModule.kt & RNUsercentricsModuleSpec.kt: method parameter types changed from Int -> Double; calls now use .toInt() when mapping to enums or native APIs (setCMPId uses toInt()).
  • iOS (Objective-C/Swift):
    • RNUsercentricsModule.mm & RNUsercentricsModuleSpec.h: changed NSInteger/NSInteger* signatures to double.
    • RNUsercentricsModule.swift: updated method signatures to accept Double and convert to Int/Int32 where needed (Int(from: Int(double)), Int32 for CMP id). Unsaved purpose LI decisions mapping preserved.
  • No functional logic changes besides explicit numeric casts when calling native enums/APIs.
  • Behavior/compatibility notes:
    • JS should continue to pass numbers; decimals will be truncated when converted to Int on native side—ensure integers are passed where intended.
    • No dependency updates.

Files modified:

  • android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
  • android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt
  • ios/RNUsercentricsModule.mm
  • ios/RNUsercentricsModule.swift
  • ios/RNUsercentricsModuleSpec.h

Impact:

  • Prevents type mismatch/crash issues with TurboModule and JS numeric types. Minor risk of truncation if JS passes non-integer numbers; callers should pass integers for enum/index parameters.

Reviewed by Panto AI

@codeant-ai codeant-ai bot added the size:M This PR changes 30-99 lines, ignoring generated files label Feb 19, 2026
- (void)saveDecisionsForTCF:(NSDictionary *)tcfDecisions
fromLayer:(NSInteger)fromLayer
fromLayer:(double)fromLayer
saveDecisions:(NSArray<NSDictionary *> *)saveDecisions
Copy link

Choose a reason for hiding this comment

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

Suggestion: The TurboModule spec method saveDecisionsForTCF uses the selector label saveDecisions: for its third parameter, but the Swift implementation exposes this parameter as serviceDecisions:, so the Objective‑C selector in the protocol does not match the Swift method; this mismatch breaks the NativeUsercentricsSpec contract and will prevent TurboModule calls to this method from binding correctly at runtime. [logic error]

Severity Level: Critical 🚨
- ❌ New-arch iOS build fails when compiling RNUsercentricsModule.
- ❌ TurboModule NativeUsercentricsSpec cannot be generated or registered.
- ❌ JS saveDecisionsForTCF API unusable on iOS new architecture.
- ⚠️ TCF consent saving flows in CustomUI screens are blocked.
Suggested change
saveDecisions:(NSArray<NSDictionary *> *)saveDecisions
serviceDecisions:(NSArray<NSDictionary *> *)serviceDecisions
Steps of Reproduction ✅
1. Enable the new React Native architecture so that `RCT_NEW_ARCH_ENABLED` is defined
during the iOS build; in this configuration, `ios/RNUsercentricsModule.swift` imports
`RNUsercentricsModuleSpec` and declares conformance to `NativeUsercentricsSpec` in the
extension at lines 232–236.

2. Build the iOS target that includes this SDK; the compiler loads the Objective‑C
protocol `NativeUsercentricsSpec` from `ios/RNUsercentricsModuleSpec.h:5–101`, which
declares `- (void)saveDecisionsForTCF:fromLayer:saveDecisions:consentType:resolve:reject;`
at lines 86–91.

3. The Swift implementation `@objc func saveDecisionsForTCF(_ tcfDecisions: NSDictionary,
fromLayer: Double, serviceDecisions: [NSDictionary], consentType: Double, resolve:
@escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock)` at
`ios/RNUsercentricsModule.swift:184–189` exposes the Objective‑C selector
`saveDecisionsForTCF:fromLayer:serviceDecisions:consentType:resolve:reject:`, which does
not match the protocol's third-parameter label `saveDecisions:`.

4. During compilation, Swift checks that `RNUsercentricsModule` (extension at
`ios/RNUsercentricsModule.swift:232`) satisfies all `NativeUsercentricsSpec` requirements;
because the selector expected by the protocol (`…saveDecisions:…`) differs from the
selector provided by the implementation (`…serviceDecisions:…`), the build fails with a
missing/incorrect method implementation error, preventing the TurboModule from being built
and thus blocking calls from JS such as `Usercentrics.saveDecisionsForTCF(...)` defined at
`src/Usercentrics.tsx:119–121` and used in multiple UI screens (e.g.,
`sample/src/screens/CustomUI.tsx:274`).
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** ios/RNUsercentricsModuleSpec.h
**Line:** 88:88
**Comment:**
	*Logic Error: The TurboModule spec method `saveDecisionsForTCF` uses the selector label `saveDecisions:` for its third parameter, but the Swift implementation exposes this parameter as `serviceDecisions:`, so the Objective‑C selector in the protocol does not match the Swift method; this mismatch breaks the NativeUsercentricsSpec contract and will prevent TurboModule calls to this method from binding correctly at runtime.

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 Feb 19, 2026

CodeAnt AI finished reviewing your PR.

Comment on lines +98 to 100
override fun setCMPId(id: Double) {
usercentricsProxy.instance.setCMPId(id.toInt())
}
Copy link

Choose a reason for hiding this comment

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

[VALIDATION] setCMPId parameter changed to Double and converted with id.toInt() (lines 98-100). Validate the incoming Double before converting: check for NaN, infinity and whether it represents an integer within the expected range. If JS can send non-integer numbers, decide on rounding/truncation semantics or reject the call with a descriptive error to avoid silent incorrect CMP ids.

@ReactMethod
override fun setCMPId(id: Double) {
    val intId = id.toInt()
    if (!id.isFinite() || id != intId.toDouble() || intId < 0) {
        // consider exposing a Promise here to propagate an error;
        // if signature must stay void, at least guard against bad input
        return
    }
    usercentricsProxy.instance.setCMPId(intId)
}

Comment on lines +134 to 147
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType]
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun acceptAll(consentType: Int, promise: Promise) {
override fun acceptAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
Copy link

Choose a reason for hiding this comment

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

[CRITICAL_BUG] acceptAllForTCF / acceptAll now accept Double and convert toInt() then index into enum arrays (lines 134-147). Indexing enum arrays like TCFDecisionUILayer.values()[fromLayer.toInt()] and UsercentricsConsentType.values()[consentType.toInt()] can throw ArrayIndexOutOfBoundsException for out-of-range or negative values (or if toInt() produces an unexpected value). Add explicit validation of the integer index (0 <= idx < enum.values().size) and handle invalid values by rejecting the Promise or returning a safe default. Wrap conversions and enum lookups in try/catch and return meaningful errors instead of letting the native layer crash.

@ReactMethod
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
    val fromIdx = fromLayer.toInt()
    val consentIdx = consentType.toInt()

    if (!fromLayer.isFinite() || fromLayer != fromIdx.toDouble() ||
        fromIdx !in TCFDecisionUILayer.values().indices
    ) {
        promise.reject("E_INVALID_TCF_LAYER", "Invalid fromLayer index: $fromLayer")
        return
    }

    if (!consentType.isFinite() || consentType != consentIdx.toDouble() ||
        consentIdx !in UsercentricsConsentType.values().indices
    ) {
        promise.reject("E_INVALID_CONSENT_TYPE", "Invalid consentType index: $consentType")
        return
    }

    val result = usercentricsProxy.instance.acceptAllForTCF(
        TCFDecisionUILayer.values()[fromIdx],
        UsercentricsConsentType.values()[consentIdx]
    )
    promise.resolve(result.toWritableArray())
}

@ReactMethod
override fun acceptAll(consentType: Double, promise: Promise) {
    val consentIdx = consentType.toInt()
    if (!consentType.isFinite() || consentType != consentIdx.toDouble() ||
        consentIdx !in UsercentricsConsentType.values().indices
    ) {
        promise.reject("E_INVALID_CONSENT_TYPE", "Invalid consentType index: $consentType")
        return
    }

    val result = usercentricsProxy.instance.acceptAll(
        UsercentricsConsentType.values()[consentIdx]
    )
    promise.resolve(result.toWritableArray())
}

Comment on lines +152 to 166
override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
).toWritableArray()
)
}

@ReactMethod
override fun denyAll(consentType: Int, promise: Promise) {
override fun denyAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
Copy link

Choose a reason for hiding this comment

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

[CRITICAL_BUG] denyAllForTCF / denyAll changed to accept Double and convert toInt() (lines 152-166). The code uses the converted integer to index enums and to deserialize maps. Validate the converted indices before using them (bounds check) and ensure deserializePurposeLIDecisionsMap() handles empty or malformed arrays safely. If invalid input is detected, reject the Promise with a clear error message instead of allowing a crash.

@ReactMethod
override fun denyAllForTCF(
    fromLayer: Double,
    consentType: Double,
    unsavedPurposeLIDecisions: ReadableArray,
    promise: Promise
) {
    val fromIdx = fromLayer.toInt()
    val consentIdx = consentType.toInt()

    if (!fromLayer.isFinite() || fromLayer != fromIdx.toDouble() ||
        fromIdx !in TCFDecisionUILayer.values().indices
    ) {
        promise.reject("E_INVALID_TCF_LAYER", "Invalid fromLayer index: $fromLayer")
        return
    }

    if (!consentType.isFinite() || consentType != consentIdx.toDouble() ||
        consentIdx !in UsercentricsConsentType.values().indices
    ) {
        promise.reject("E_INVALID_CONSENT_TYPE", "Invalid consentType index: $consentType")
        return
    }

    val decisionsMap = try {
        unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
    } catch (e: Exception) {
        promise.reject("E_INVALID_TCF_DECISIONS", "Malformed unsavedPurposeLIDecisions array", e)
        return
    }

    val result = usercentricsProxy.instance.denyAllForTCF(
        TCFDecisionUILayer.values()[fromIdx],
        UsercentricsConsentType.values()[consentIdx],
        decisionsMap
    )
    promise.resolve(result.toWritableArray())
}

Comment on lines 98 to 206
@@ -131,80 +131,80 @@ internal class RNUsercentricsModule(
}

@ReactMethod
override fun acceptAllForTCF(fromLayer: Int, consentType: Int, promise: Promise) {
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType]
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun acceptAll(consentType: Int, promise: Promise) {
override fun acceptAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun denyAllForTCF(fromLayer: Int, consentType: Int, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
).toWritableArray()
)
}

@ReactMethod
override fun denyAll(consentType: Int, promise: Promise) {
override fun denyAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveDecisionsForTCF(
tcfDecisions: ReadableMap,
fromLayer: Int,
fromLayer: Double,
saveDecisions: ReadableArray,
consentType: Int,
consentType: Double,
promise: Promise
) {
promise.resolve(
usercentricsProxy.instance.saveDecisionsForTCF(
tcfDecisions.deserializeTCFUserDecisions(),
TCFDecisionUILayer.values()[fromLayer],
TCFDecisionUILayer.values()[fromLayer.toInt()],
saveDecisions.deserializeUserDecision(),
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveDecisions(decisions: ReadableArray, consentType: Int, promise: Promise) {
override fun saveDecisions(decisions: ReadableArray, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.saveDecisions(
decisions.deserializeUserDecision(), UsercentricsConsentType.values()[consentType]
decisions.deserializeUserDecision(), UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Int, promise: Promise) {
override fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.saveOptOutForCCPA(
isOptedOut, UsercentricsConsentType.values()[consentType]
isOptedOut, UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun track(event: Int) {
usercentricsProxy.instance.track(UsercentricsAnalyticsEventType.values()[event])
override fun track(event: Double) {
Copy link

Choose a reason for hiding this comment

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

[REFACTORING] There are many places where Double parameters are converted to Int via .toInt() before being used as enum indices or passed to the SDK (e.g. setCMPId, acceptAllForTCF, acceptAll, denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA, track). Extract a single helper (e.g. fun validateAndConvertIndex(value: Double, maxExclusive: Int): Int?) that: checks for finite value, checks range and integer-ness (or documents rounding), returns an Int or null. Use it to centralize validation, improve error messaging, and reduce duplication (makes future changes safer).

private fun Double.toValidIndex(maxExclusive: Int, paramName: String): Int {
    if (!isFinite()) {
        throw IllegalArgumentException("$paramName must be a finite number")
    }
    val intValue = toInt()
    if (this != intValue.toDouble()) {
        throw IllegalArgumentException("$paramName must be an integer, got $this")
    }
    if (intValue !in 0 until maxExclusive) {
        throw IndexOutOfBoundsException("$paramName index out of range: $intValue (size=$maxExclusive)")
    }
    return intValue
}

@ReactMethod
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
    try {
        val fromIdx = fromLayer.toValidIndex(TCFDecisionUILayer.values().size, "fromLayer")
        val consentIdx = consentType.toValidIndex(UsercentricsConsentType.values().size, "consentType")

        val result = usercentricsProxy.instance.acceptAllForTCF(
            TCFDecisionUILayer.values()[fromIdx],
            UsercentricsConsentType.values()[consentIdx]
        )
        promise.resolve(result.toWritableArray())
    } catch (e: IllegalArgumentException) {
        promise.reject("E_INVALID_ARGUMENT", e.message, e)
    } catch (e: IndexOutOfBoundsException) {
        promise.reject("E_INVALID_INDEX", e.message, e)
    }
}

Comment on lines 94 to 96
RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
Copy link

Choose a reason for hiding this comment

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

[CRITICAL_BUG] Signature mismatch: RNUsercentricsModule.mm still declares saveOptOutForCCPA parameter as (BOOL )isOptedOut (pointer) while RNUsercentricsModuleSpec.h uses (BOOL)isOptedOut (non-pointer) and consentType was changed to double. The BOOL is incorrect and will cause a build/ABI mismatch. Change the mm declaration to use plain BOOL (and ensure all parameter types in .mm match the .h declarations exactly).

RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL)isOptedOut
                  consentType:(double)consentType
                     resolve:(RCTPromiseResolveBlock)resolve
                      reject:(RCTPromiseRejectBlock)reject)

Comment on lines +84 to 86
@objc func setCMPId(_ id: Double) -> Void {
usercentricsManager.setCMPId(id: Int32(id))
}
Copy link

Choose a reason for hiding this comment

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

[VALIDATION] setCMPId signature changed to accept Double and converts with Int32(id) (lines 84-86). Validate incoming Double before converting to Int32: check that it is finite and within Int32 range, and clarify rounding/truncation rules. If invalid, either early return with an error or clamp/round according to documented behavior to avoid integer overflow or unexpected negative values.

@objc func setCMPId(_ id: Double) -> Void {
    guard id.isFinite,
          id >= Double(Int32.min),
          id <= Double(Int32.max) else {
        // Optionally log or surface an error here
        return
    }

    usercentricsManager.setCMPId(id: Int32(id))
}

Comment on lines 142 to 176
@@ -170,51 +170,51 @@ class RNUsercentricsModule: NSObject {
}
}
}
let services = usercentricsManager.denyAllForTCF(fromLayer: .initialize(from: fromLayer), consentType: .initialize(from: consentType), unsavedPurposeLIDecisions: decisions)
let services = usercentricsManager.denyAllForTCF(fromLayer: .initialize(from: Int(fromLayer)), consentType: .initialize(from: Int(consentType)), unsavedPurposeLIDecisions: decisions)
resolve(services.toListOfDictionary())
}

Copy link

Choose a reason for hiding this comment

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

[VALIDATION] Multiple methods (acceptAllForTCF, acceptAll, denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA) now accept Double indices and immediately convert via Int(...). Converting JS numbers (Double) directly to Int without validation can cause truncation or invalid enum initializers. Add explicit checks that Doubles are finite integers and within enum index ranges. If your enum initializers (e.g. TCFDecisionUILayer.initialize(from:)) return optional, handle nil cases by rejecting the promise or returning a clear error instead of proceeding silently or crashing.

@objc func acceptAllForTCF(_ fromLayer: Double,
                           consentType: Double,
                           resolve: @escaping RCTPromiseResolveBlock,
                           reject: @escaping RCTPromiseRejectBlock) -> Void {
    guard fromLayer.isFinite,
          consentType.isFinite,
          fromLayer.rounded() == fromLayer,
          consentType.rounded() == consentType else {
        reject("usercentrics_reactNative_invalid_argument",
               "fromLayer and consentType must be finite integer values",
               nil)
        return
    }

    let fromLayerInt = Int(fromLayer)
    let consentTypeInt = Int(consentType)

    guard let fromLayerEnum = TCFDecisionUILayer.initialize(from: fromLayerInt),
          let consentTypeEnum = UsercentricsConsentType.initialize(from: consentTypeInt) else {
        reject("usercentrics_reactNative_invalid_enum",
               "Invalid fromLayer or consentType enum index",
               nil)
        return
    }

    let services = usercentricsManager.acceptAllForTCF(fromLayer: fromLayerEnum,
                                                       consentType: consentTypeEnum)
    resolve(services.toListOfDictionary())
}

Comment on lines +55 to +99
- (void)setCMPId:(double)cmpId;
- (void)setABTestingVariant:(NSString *)variant;
- (void)changeLanguage:(NSString *)language
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

// Consent Actions
- (void)acceptAll:(NSInteger)consentType
- (void)acceptAll:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)acceptAllForTCF:(NSInteger)fromLayer
consentType:(NSInteger)consentType
- (void)acceptAllForTCF:(double)fromLayer
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)denyAll:(NSInteger)consentType
- (void)denyAll:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)denyAllForTCF:(NSInteger)fromLayer
consentType:(NSInteger)consentType
- (void)denyAllForTCF:(double)fromLayer
consentType:(double)consentType
unsavedPurposeLIDecisions:(NSArray<NSDictionary *> *)unsavedPurposeLIDecisions
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)saveDecisions:(NSArray<NSDictionary *> *)decisions
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)saveDecisionsForTCF:(NSDictionary *)tcfDecisions
fromLayer:(NSInteger)fromLayer
fromLayer:(double)fromLayer
saveDecisions:(NSArray<NSDictionary *> *)saveDecisions
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)saveOptOutForCCPA:(BOOL)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

// Analytics
- (void)track:(NSInteger)event;
- (void)track:(double)event;
Copy link

Choose a reason for hiding this comment

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

[REFACTORING] You changed many method parameter types to double in the objective-c spec (lines 55-99). Ensure all generated bridging code and the turbo module codegen are updated accordingly (re-run codegen / rebuild iOS project). Also confirm that JS consumers are aware that numeric arguments will be received as Double and may need to be integers logically — document the expected integer behaviour in the JS API comments or TypeScript types used by consumers.

// TypeScript surface (example)
export interface NativeUsercentricsModule {
  setCMPId(cmpId: number): void; // integer semantics, passed as JS Number

  acceptAll(consentType: number): Promise<ConsentResult[]>; // consentType is enum index

  acceptAllForTCF(fromLayer: number, consentType: number): Promise<ConsentResult[]>;

  denyAll(consentType: number): Promise<ConsentResult[]>;

  denyAllForTCF(
    fromLayer: number,
    consentType: number,
    unsavedPurposeLIDecisions: Array<{ id: number; legitimateInterestConsent: boolean }>,
  ): Promise<ConsentResult[]>;

  saveDecisions(
    decisions: UserDecision[],
    consentType: number,
  ): Promise<ConsentResult[]>;

  saveDecisionsForTCF(
    tcfDecisions: TCFUserDecisions,
    fromLayer: number,
    saveDecisions: UserDecision[],
    consentType: number,
  ): Promise<ConsentResult[]>;

  saveOptOutForCCPA(isOptedOut: boolean, consentType: number): Promise<ConsentResult[]>;

  track(event: number): void; // enum index
}

// JS doc example
/**
 * Sets the CMP id.
 *
 * @param cmpId Integer CMP identifier (passed as JS Number). Values are truncated to int on native side.
 */
function setCMPId(cmpId: number) { /* ... */ }

@pantoaibot
Copy link

pantoaibot bot commented Feb 19, 2026

Reviewed up to commit:935755be90f9499a9beb56b911c73bab0de1d421

Additional Suggestion
android/src/androidTest/java/com/usercentrics/reactnative/RNUsercentricsModuleTest.kt, line:341-350 Unit tests call module.setCMPId(123) and other methods with Int arguments (line ~341). After the change the native spec methods accept Double — update the tests to call with Double (e.g. 123.0) where appropriate, or ensure Kotlin/Java test call sites compile as-is. Run the test suite to catch any type mismatches introduced by the signature changes.
@Test
fun testSetCMPId() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every { usercentricsSDK.setCMPId(123) }.returns(Unit)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    module.setCMPId(123.0)

    verify(exactly = 1) { usercentricsSDK.setCMPId(123) }
}

@Test
fun testAcceptAllForTCF() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.acceptAllForTCF(
            any(), any()
        )
    }.returns(GetConsentsMock.fakeWithData)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.acceptAllForTCF(0.0, 0.0, promise)

    verify(exactly = 1) {
        usercentricsSDK.acceptAllForTCF(
            TCFDecisionUILayer.FIRST_LAYER, UsercentricsConsentType.EXPLICIT
        )
    }

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testDenyAllForTCF() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.denyAllForTCF(
            any(), any(), any()
        )
    }.returns(GetConsentsMock.fakeWithData)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    val emptyArray = JavaOnlyArray()
    module.denyAllForTCF(0.0, 0.0, emptyArray, promise)

    verify(exactly = 1) {
        usercentricsSDK.denyAllForTCF(
            TCFDecisionUILayer.FIRST_LAYER, UsercentricsConsentType.EXPLICIT, null
        )
    }

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testAcceptAll() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.acceptAll(
            any(),
        )
    }.returns(GetConsentsMock.fakeWithData)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.acceptAll(0.0, promise)

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testDenyAll() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.denyAll(
            any(),
        )
    }.returns(GetConsentsMock.fakeWithData)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.denyAll(0.0, promise)

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testSaveDecisions() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.saveDecisions(
            any(), any()
        )
    }.returns(GetConsentsMock.fakeWithData)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.saveDecisions(SaveDecisionsMock.callDecisions.serialize(), 0.0, promise)

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testSaveDecisionsForTCF() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.saveDecisionsForTCF(any(), any(), any(), any())
    }.returns(SaveDecisionsForTCFMock.fake)

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.saveDecisionsForTCF(
        tcfDecisions = SaveDecisionsForTCFMock.call,
        fromLayer = 0.0,
        saveDecisions = SaveDecisionsMock.callDecisions.serialize(),
        consentType = 0.0,
        promise = promise
    )

    val result = promise.resolveValue as WritableArray
    assertEquals(SaveDecisionsForTCFMock.expected.serialize(), result)
}

@Test
fun testSaveOptOutForCCPA() {
    val usercentricsSDK = mockk<UsercentricsSDK>()
    every {
        usercentricsSDK.saveOptOutForCCPA(any(), any())
    }.returns(
        GetConsentsMock.fakeWithData
    )

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    val promise = FakePromise()
    module.saveOptOutForCCPA(
        false, 0.0, promise
    )

    val result = promise.resolveValue as WritableArray

    assertEquals(GetConsentsMock.expectedWithData.serialize(), result)
}

@Test
fun testTrackMethod() {
    val trackedEvents = mutableListOf<UsercentricsAnalyticsEventType>()

    val usercentricsSdk = mockk<UsercentricsSDK>()
    every { usercentricsSdk.track(any()) } answers {
        trackedEvents.add(arg(0))
    }

    val usercentricsProxy = FakeUsercentricsProxy(usercentricsSdk)
    val contextMock = mockk<ReactApplicationContext>(relaxed = true)
    val module =
        RNUsercentricsModule(contextMock, usercentricsProxy, ReactContextProviderMock())

    module.track(UsercentricsAnalyticsEventType.ACCEPT_ALL_FIRST_LAYER.ordinal.toDouble())

    assertEquals(1, trackedEvents.size)
    assertEquals(UsercentricsAnalyticsEventType.ACCEPT_ALL_FIRST_LAYER, trackedEvents[0])
}
Others - Overall: these changes were likely made to accommodate TurboModule / JS number -> native mappings (JS numbers come as Double). That's correct, but please run full Android and iOS builds (including Fresh Xcode clean / pod install) and the codegen script (generate-codegen-jni.js) to ensure all generated interfaces are in sync. Add unit/integration tests asserting that invalid numeric inputs are handled gracefully (e.g. fractional, NaN, out-of-range).
# After changing native method signatures ensure codegen + builds are in sync

# 1. Regenerate Android JNI bindings
node scripts/generate-codegen-jni.js

# 2. Clean & build Android
cd android
./gradlew clean assembleDebug

# 3. Clean & build iOS
cd ../ios
rm -rf ~/Library/Developer/Xcode/DerivedData
pod install --repo-update
xcodebuild -workspace sample.xcworkspace -scheme sample -configuration Debug -sdk iphonesimulator build
// Example Jest test (JS side) to assert invalid numeric input handling
it('rejects invalid consentType index', async () => {
  await expect(
    NativeModules.RNUsercentricsModule.acceptAll(Number.POSITIVE_INFINITY)
  ).rejects.toMatchObject({ code: 'E_INVALID_ARGUMENT' });
});

Reviewed by Panto AI

@qodo-code-review
Copy link

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. values()[*.toInt()] unchecked 📘 Rule violation ⛯ Reliability
Description
Several React-exposed methods convert external Double inputs to Int and immediately index enum
values() without validating finiteness, integer-ness, or bounds, which can crash the app. This
violates required edge-case management and input validation for externally supplied inputs.
Code

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[R134-165]

+    override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
        promise.resolve(
            usercentricsProxy.instance.acceptAllForTCF(
-                TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType]
+                TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
            ).toWritableArray()
        )
    }

    @ReactMethod
-    override fun acceptAll(consentType: Int, promise: Promise) {
+    override fun acceptAll(consentType: Double, promise: Promise) {
        promise.resolve(
            usercentricsProxy.instance.acceptAll(
-                UsercentricsConsentType.values()[consentType]
+                UsercentricsConsentType.values()[consentType.toInt()]
            ).toWritableArray()
        )
    }

    @ReactMethod
-    override fun denyAllForTCF(fromLayer: Int, consentType: Int, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
+    override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
        promise.resolve(
            usercentricsProxy.instance.denyAllForTCF(
-                TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
+                TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
            ).toWritableArray()
        )
    }

    @ReactMethod
-    override fun denyAll(consentType: Int, promise: Promise) {
+    override fun denyAll(consentType: Double, promise: Promise) {
        promise.resolve(
            usercentricsProxy.instance.denyAll(
-                UsercentricsConsentType.values()[consentType]
+                UsercentricsConsentType.values()[consentType.toInt()]
            ).toWritableArray()
Evidence
PR Compliance ID 3 and 6 require handling null/edge/boundary cases and validating external inputs;
the new code directly truncates Double to Int and uses the result as an array index
(enum.values()[index]) with no checks or error handling, which can throw
IndexOutOfBoundsException (or mis-map values) for invalid or non-integer inputs coming from JS.

Rule 3: Generic: Robust Error Handling and Edge Case Management
Rule 6: Generic: Security-First Input Validation and Data Handling
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[134-139]
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[143-147]
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[161-165]

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

## Issue description
React Native methods accept `Double` parameters and immediately convert them using `toInt()` and index `Enum.values()[index]` without any validation. Invalid JS inputs (e.g., NaN, Infinity, non-integer doubles, negative values, or out-of-range values) can crash the app or map to an unintended enum value.

## Issue Context
These values come from the JS bridge (external input). Compliance requires explicit edge-case handling and input validation to prevent crashes and unpredictable behavior.

## Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[134-165]

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


2. iOS selector mismatch 🐞 Bug ✓ Correctness
Description
The new-architecture iOS spec declares saveDecisionsForTCF:...saveDecisions:..., but the Swift
implementation uses the label serviceDecisions:. This breaks protocol conformance/bridge method
lookup under the new architecture.
Code

ios/RNUsercentricsModuleSpec.h[R86-91]

- (void)saveDecisionsForTCF:(NSDictionary *)tcfDecisions
-                  fromLayer:(NSInteger)fromLayer
+                  fromLayer:(double)fromLayer
              saveDecisions:(NSArray<NSDictionary *> *)saveDecisions
-                consentType:(NSInteger)consentType
+                consentType:(double)consentType
                    resolve:(RCTPromiseResolveBlock)resolve
                     reject:(RCTPromiseRejectBlock)reject;
Evidence
The Objective-C protocol selector segment is part of the method signature; the header uses
saveDecisions: while Swift defines serviceDecisions: and the legacy extern uses
serviceDecisions: too, creating a mismatch for new-arch codegen/protocol conformance.

ios/RNUsercentricsModuleSpec.h[86-91]
ios/RNUsercentricsModule.swift[184-196]
ios/RNUsercentricsModule.mm[82-87]

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 new-architecture protocol expects `saveDecisionsForTCF:fromLayer:saveDecisions:consentType:...` but Swift exposes `...serviceDecisions:...`, which is a different Objective-C selector segment.

### Issue Context
- `RNUsercentricsModuleSpec.h` is used for new-arch protocol/codegen.
- `RNUsercentricsModule.mm` exports the legacy bridge signature.
- Swift method labels determine the Objective-C selector.

### Fix Focus Areas
- ios/RNUsercentricsModuleSpec.h[86-91]
- ios/RNUsercentricsModule.swift[184-196]
- ios/RNUsercentricsModule.mm[82-88]

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


3. iOS BOOL pointer mismatch 🐞 Bug ✓ Correctness
Description
The ObjC extern for saveOptOutForCCPA still declares isOptedOut as BOOL * while the spec and
Swift implementation use a non-pointer Bool/BOOL. This can break bridging/registration for that
method.
Code

ios/RNUsercentricsModule.mm[R94-97]

RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
-                  consentType:(NSInteger)consentType
+                  consentType:(double)consentType
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
Evidence
The extern declaration type differs from both the protocol header and the Swift implementation, so
the bridge metadata may not match the actual method signature.

ios/RNUsercentricsModule.mm[94-97]
ios/RNUsercentricsModuleSpec.h[93-96]
ios/RNUsercentricsModule.swift[208-214]

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

### Issue description
`RNUsercentricsModule.mm` declares `saveOptOutForCCPA:(BOOL *)isOptedOut`, but the Swift implementation and new-arch spec use a non-pointer boolean.

### Issue Context
React Native method registration relies on consistent signatures across extern/spec/implementation.

### Fix Focus Areas
- ios/RNUsercentricsModule.mm[94-98]
- ios/RNUsercentricsModuleSpec.h[93-96]
- ios/RNUsercentricsModule.swift[208-214]

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



Remediation recommended

4. setCMPId() truncates Double 📘 Rule violation ⛨ Security
Description
setCMPId now truncates a Double to Int with no range/validity checks, which can silently
change the CMP ID (e.g., 1.9 becomes 1) or overflow. This lacks required validation for external
inputs.
Code

android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[R98-99]

+    override fun setCMPId(id: Double) {
+        usercentricsProxy.instance.setCMPId(id.toInt())
Evidence
PR Compliance ID 6 requires validating external inputs; the new Double parameter is converted via
toInt() without checking whether it is a whole number, finite, positive, or within the supported
range for the underlying API.

Rule 6: Generic: Security-First Input Validation and Data Handling
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[98-99]

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

## Issue description
`setCMPId` converts `Double` to `Int` using `toInt()` without validation, which may silently truncate or overflow and set an unintended CMP ID.

## Issue Context
The value is provided through the React Native bridge. Compliance requires validating external inputs before use.

## Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[98-99]

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


5. Int(fromLayer) unchecked 📘 Rule violation ⛯ Reliability
Description
iOS bridge methods now accept Double and cast to Int without validating finiteness,
integer-ness, or bounds, which can lead to incorrect enum mapping or downstream errors. This weakens
required edge-case handling and external input validation.
Code

ios/RNUsercentricsModule.swift[R142-155]

+    @objc func acceptAllForTCF(_ fromLayer: Double,
+                               consentType: Double,
                               resolve: @escaping RCTPromiseResolveBlock,
                               reject: @escaping RCTPromiseRejectBlock) -> Void {
-        let services = usercentricsManager.acceptAllForTCF(fromLayer: TCFDecisionUILayer.initialize(from: fromLayer),
-                                                           consentType: UsercentricsConsentType.initialize(from: consentType))
+        let services = usercentricsManager.acceptAllForTCF(fromLayer: TCFDecisionUILayer.initialize(from: Int(fromLayer)),
+                                                           consentType: UsercentricsConsentType.initialize(from: Int(consentType)))
        resolve(services.toListOfDictionary())
    }
    
-    @objc func acceptAll(_ consentType: Int,
+    @objc func acceptAll(_ consentType: Double,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) -> Void {
-        let services = usercentricsManager.acceptAll(consentType: UsercentricsConsentType.initialize(from: consentType))
+        let services = usercentricsManager.acceptAll(consentType: UsercentricsConsentType.initialize(from: Int(consentType)))
        resolve(services.toListOfDictionary())
Evidence
PR Compliance ID 3 and 6 require explicit edge-case management and validation of external inputs;
the new code casts externally supplied Double to Int and uses it to initialize consent/layer
types without verifying that the value is valid and within the expected enum range.

Rule 3: Generic: Robust Error Handling and Edge Case Management
Rule 6: Generic: Security-First Input Validation and Data Handling
ios/RNUsercentricsModule.swift[142-148]
ios/RNUsercentricsModule.swift[151-155]

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 bridge methods now accept `Double` values and convert them with `Int(...)` without checking for NaN/Infinity, fractional values, or bounds. This can cause incorrect enum mapping or failures further down the call chain.

## Issue Context
These values originate from the JS bridge (external input). Compliance requires explicit edge-case handling and validation.

## Fix Focus Areas
- ios/RNUsercentricsModule.swift[142-155]

ⓘ 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

Comment on lines +134 to 165
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType]
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun acceptAll(consentType: Int, promise: Promise) {
override fun acceptAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun denyAllForTCF(fromLayer: Int, consentType: Int, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
).toWritableArray()
)
}

@ReactMethod
override fun denyAll(consentType: Int, promise: Promise) {
override fun denyAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()

Choose a reason for hiding this comment

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

Action required

1. values()[*.toint()] unchecked 📘 Rule violation ⛯ Reliability

Several React-exposed methods convert external Double inputs to Int and immediately index enum
values() without validating finiteness, integer-ness, or bounds, which can crash the app. This
violates required edge-case management and input validation for externally supplied inputs.
Agent Prompt
## Issue description
React Native methods accept `Double` parameters and immediately convert them using `toInt()` and index `Enum.values()[index]` without any validation. Invalid JS inputs (e.g., NaN, Infinity, non-integer doubles, negative values, or out-of-range values) can crash the app or map to an unintended enum value.

## Issue Context
These values come from the JS bridge (external input). Compliance requires explicit edge-case handling and input validation to prevent crashes and unpredictable behavior.

## Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[134-165]

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

Comment on lines 86 to 91
- (void)saveDecisionsForTCF:(NSDictionary *)tcfDecisions
fromLayer:(NSInteger)fromLayer
fromLayer:(double)fromLayer
saveDecisions:(NSArray<NSDictionary *> *)saveDecisions
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

Choose a reason for hiding this comment

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

Action required

2. Ios selector mismatch 🐞 Bug ✓ Correctness

The new-architecture iOS spec declares saveDecisionsForTCF:...saveDecisions:..., but the Swift
implementation uses the label serviceDecisions:. This breaks protocol conformance/bridge method
lookup under the new architecture.
Agent Prompt
### Issue description
iOS new-architecture protocol expects `saveDecisionsForTCF:fromLayer:saveDecisions:consentType:...` but Swift exposes `...serviceDecisions:...`, which is a different Objective-C selector segment.

### Issue Context
- `RNUsercentricsModuleSpec.h` is used for new-arch protocol/codegen.
- `RNUsercentricsModule.mm` exports the legacy bridge signature.
- Swift method labels determine the Objective-C selector.

### Fix Focus Areas
- ios/RNUsercentricsModuleSpec.h[86-91]
- ios/RNUsercentricsModule.swift[184-196]
- ios/RNUsercentricsModule.mm[82-88]

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

Comment on lines 94 to 97
RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

Choose a reason for hiding this comment

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

Action required

3. Ios bool pointer mismatch 🐞 Bug ✓ Correctness

The ObjC extern for saveOptOutForCCPA still declares isOptedOut as BOOL * while the spec and
Swift implementation use a non-pointer Bool/BOOL. This can break bridging/registration for that
method.
Agent Prompt
### Issue description
`RNUsercentricsModule.mm` declares `saveOptOutForCCPA:(BOOL *)isOptedOut`, but the Swift implementation and new-arch spec use a non-pointer boolean.

### Issue Context
React Native method registration relies on consistent signatures across extern/spec/implementation.

### Fix Focus Areas
- ios/RNUsercentricsModule.mm[94-98]
- ios/RNUsercentricsModuleSpec.h[93-96]
- ios/RNUsercentricsModule.swift[208-214]

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

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/RNUsercentricsModule.swift (1)

84-217: ⚠️ Potential issue | 🟡 Minor

Int(Double.nan) and Int(Double.infinity) cause a Swift runtime trap

Swift's Int(_:) initializer for floating-point values traps (fatal precondition failure) when the value is NaN, ±Infinity, or outside the Int representable range. Every Int(fromLayer), Int(consentType), Int(event), and Int32(id) conversion is unguarded against these special values.

While the React Native bridge won't ordinarily forward NaN or Infinity from well-typed JS callers, a missing validation makes the module fragile for unexpected inputs. Consider adding a isFinite / isNaN guard before conversion:

🛡️ Proposed guard pattern
 `@objc` func acceptAllForTCF(_ fromLayer: Double,
                            consentType: Double,
                            resolve: `@escaping` RCTPromiseResolveBlock,
                            reject: `@escaping` RCTPromiseRejectBlock) -> Void {
+    guard !fromLayer.isNaN, !fromLayer.isInfinite,
+          !consentType.isNaN, !consentType.isInfinite else {
+        reject("usercentrics_reactNative_acceptAllForTCF_error", "Invalid numeric parameter", nil)
+        return
+    }
     let services = usercentricsManager.acceptAllForTCF(...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/RNUsercentricsModule.swift` around lines 84 - 217, Multiple places (e.g.,
setCMPId -> setCMPId(_:), acceptAllForTCF(_:consentType:),
denyAllForTCF(_:consentType:...), denyAll(_:), saveDecisions(_:consentType:),
saveDecisionsForTCF(...), saveOptOutForCCPA(_:consentType:), track(_:), and any
Int32(id) conversions) convert Double to Int/Int32 without guarding against
NaN/±Infinity/out-of-range values; add a small validation helper or inline
guards that checks Float.isFinite (and optionally bounds) before converting and
either: (a) for promise-based methods call the provided reject with a
descriptive error code/message when the input is invalid, or (b) for void
methods simply return early (or no-op) if conversion fails; update each
referenced function (setCMPId, getTCFData caller where conversion exists,
acceptAllForTCF, acceptAll, denyAllForTCF, denyAll, saveDecisionsForTCF,
saveDecisions, saveOptOutForCCPA, track) to perform this guard prior to calling
usercentricsManager so the Int/Int32 initializers never receive
NaN/Infinity/out-of-range values.
🧹 Nitpick comments (1)
android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt (1)

137-207: Replace deprecated Enum.values() with Enum.entries

The codebase targets Kotlin 2.0.21+ (and 1.9.22+ for examples), where values() is deprecated in favour of .entries. Using values() creates a new array on every call, which is unnecessary for indexed access. The entries property is available for all three enums in this file (TCFDecisionUILayer, UsercentricsConsentType, UsercentricsAnalyticsEventType) and supports direct index access.

All 9 occurrences across lines 137–207 should be refactored:

♻️ Proposed refactor (example)
-    TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
+    TCFDecisionUILayer.entries[fromLayer.toInt()], UsercentricsConsentType.entries[consentType.toInt()]
🤖 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 137 - 207, Replace all uses of the deprecated Enum.values()[index]
with Enum.entries[index] across the module: update calls in methods like
acceptAll, denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions,
saveOptOutForCCPA, and track to use
TCFDecisionUILayer.entries[fromLayer.toInt()] and
UsercentricsConsentType.entries[consentType.toInt()] and
UsercentricsAnalyticsEventType.entries[event.toInt()] respectively (keeping the
same toWritableArray()/deserialize... logic); this avoids allocating a new array
on each call while preserving the current indexed lookup behavior.
🤖 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 134-207: Several places (acceptAllForTCF, acceptAll,
denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA,
track) convert Double parameters to enum indexes via
X.values()[someDouble.toInt()] which misbehaves for NaN/Infinity/negative
values; add a small defensive helper (e.g., safeEnumIndex(value: Double,
enumSize: Int): Int) that checks for NaN/infinity and that the converted int is
within 0 until enumSize, and use it instead of toInt() directly (reject the
Promise or throw a clear IllegalArgumentException on invalid input); replace
each direct values()[...toInt()] call for TCFDecisionUILayer,
UsercentricsConsentType, UsercentricsAnalyticsEventType with a call to this
validator before indexing.

In `@ios/RNUsercentricsModule.mm`:
- Around line 94-97: The declaration of saveOptOutForCCPA currently uses a BOOL
pointer for isOptedOut; change the parameter type from (BOOL *)isOptedOut to
(BOOL)isOptedOut so it matches the RNUsercentricsModuleSpec and Swift
implementation and is compatible with the React Native bridge; update the
RCT_EXTERN_METHOD signature for saveOptOutForCCPA accordingly and ensure the
parameter name and ordering (consentType, resolve, reject) remain unchanged.

---

Outside diff comments:
In `@ios/RNUsercentricsModule.swift`:
- Around line 84-217: Multiple places (e.g., setCMPId -> setCMPId(_:),
acceptAllForTCF(_:consentType:), denyAllForTCF(_:consentType:...), denyAll(_:),
saveDecisions(_:consentType:), saveDecisionsForTCF(...),
saveOptOutForCCPA(_:consentType:), track(_:), and any Int32(id) conversions)
convert Double to Int/Int32 without guarding against NaN/±Infinity/out-of-range
values; add a small validation helper or inline guards that checks
Float.isFinite (and optionally bounds) before converting and either: (a) for
promise-based methods call the provided reject with a descriptive error
code/message when the input is invalid, or (b) for void methods simply return
early (or no-op) if conversion fails; update each referenced function (setCMPId,
getTCFData caller where conversion exists, acceptAllForTCF, acceptAll,
denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA,
track) to perform this guard prior to calling usercentricsManager so the
Int/Int32 initializers never receive NaN/Infinity/out-of-range values.

---

Nitpick comments:
In `@android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt`:
- Around line 137-207: Replace all uses of the deprecated Enum.values()[index]
with Enum.entries[index] across the module: update calls in methods like
acceptAll, denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions,
saveOptOutForCCPA, and track to use
TCFDecisionUILayer.entries[fromLayer.toInt()] and
UsercentricsConsentType.entries[consentType.toInt()] and
UsercentricsAnalyticsEventType.entries[event.toInt()] respectively (keeping the
same toWritableArray()/deserialize... logic); this avoids allocating a new array
on each call while preserving the current indexed lookup behavior.

Comment on lines +134 to +207
override fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType]
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun acceptAll(consentType: Int, promise: Promise) {
override fun acceptAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.acceptAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun denyAllForTCF(fromLayer: Int, consentType: Int, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAllForTCF(
TCFDecisionUILayer.values()[fromLayer], UsercentricsConsentType.values()[consentType], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap()
).toWritableArray()
)
}

@ReactMethod
override fun denyAll(consentType: Int, promise: Promise) {
override fun denyAll(consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.denyAll(
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveDecisionsForTCF(
tcfDecisions: ReadableMap,
fromLayer: Int,
fromLayer: Double,
saveDecisions: ReadableArray,
consentType: Int,
consentType: Double,
promise: Promise
) {
promise.resolve(
usercentricsProxy.instance.saveDecisionsForTCF(
tcfDecisions.deserializeTCFUserDecisions(),
TCFDecisionUILayer.values()[fromLayer],
TCFDecisionUILayer.values()[fromLayer.toInt()],
saveDecisions.deserializeUserDecision(),
UsercentricsConsentType.values()[consentType]
UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveDecisions(decisions: ReadableArray, consentType: Int, promise: Promise) {
override fun saveDecisions(decisions: ReadableArray, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.saveDecisions(
decisions.deserializeUserDecision(), UsercentricsConsentType.values()[consentType]
decisions.deserializeUserDecision(), UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Int, promise: Promise) {
override fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Double, promise: Promise) {
promise.resolve(
usercentricsProxy.instance.saveOptOutForCCPA(
isOptedOut, UsercentricsConsentType.values()[consentType]
isOptedOut, UsercentricsConsentType.values()[consentType.toInt()]
).toWritableArray()
)
}

@ReactMethod
override fun track(event: Int) {
usercentricsProxy.instance.track(UsercentricsAnalyticsEventType.values()[event])
override fun track(event: Double) {
usercentricsProxy.instance.track(UsercentricsAnalyticsEventType.values()[event.toInt()])
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

Double.NaN.toInt() silently yields 0; out-of-range doubles cause ArrayIndexOutOfBoundsException

All enum index lookups follow the pattern SomeEnum.values()[someDouble.toInt()] with no pre-access validation. Key edge cases introduced by widening from Int to Double:

Input toInt() result Outcome
Double.NaN 0 silently selects first enum constant — wrong, no crash
Double.POSITIVE_INFINITY Int.MAX_VALUE ArrayIndexOutOfBoundsException
Double.NEGATIVE_INFINITY Int.MIN_VALUE ArrayIndexOutOfBoundsException
Any negative double negative index ArrayIndexOutOfBoundsException

The NaN→0 case is the most subtle: it produces incorrect results instead of a visible failure.

🛡️ Proposed defensive helper
+    private fun Double.toSafeEnumIndex(maxIndex: Int): Int? {
+        if (isNaN() || isInfinite()) return null
+        val index = toInt()
+        return if (index in 0 until maxIndex) index else null
+    }

Then at each call-site, e.g.:

-    TCFDecisionUILayer.values()[fromLayer.toInt()]
+    val layerIdx = fromLayer.toSafeEnumIndex(TCFDecisionUILayer.values().size)
+        ?: return promise.reject(IllegalArgumentException("Invalid fromLayer: $fromLayer"))
+    TCFDecisionUILayer.values()[layerIdx]
🤖 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 134 - 207, Several places (acceptAllForTCF, acceptAll,
denyAllForTCF, denyAll, saveDecisionsForTCF, saveDecisions, saveOptOutForCCPA,
track) convert Double parameters to enum indexes via
X.values()[someDouble.toInt()] which misbehaves for NaN/Infinity/negative
values; add a small defensive helper (e.g., safeEnumIndex(value: Double,
enumSize: Int): Int) that checks for NaN/infinity and that the converted int is
within 0 until enumSize, and use it instead of toInt() directly (reject the
Promise or throw a clear IllegalArgumentException on invalid input); replace
each direct values()[...toInt()] call for TCFDecisionUILayer,
UsercentricsConsentType, UsercentricsAnalyticsEventType with a call to this
validator before indexing.

Comment on lines 94 to 97
RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
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
# Find and display the saveOptOutForCCPA declarations in both files
echo "=== Searching for saveOptOutForCCPA in Objective-C and Swift files ==="
rg -n "saveOptOutForCCPA" --type=objc --type=swift -A3

Repository: Usercentrics/react-native-sdk

Length of output: 4516


🏁 Script executed:

#!/bin/bash
# Check if the .mm file exists and list all .mm files in ios directory
echo "=== Looking for .mm files in ios directory ==="
fd -e mm ios/

echo ""
echo "=== Checking if RNUsercentricsModule.mm exists ==="
if [ -f "ios/RNUsercentricsModule.mm" ]; then
  echo "File exists. Reading lines 90-100:"
  sed -n '90,100p' ios/RNUsercentricsModule.mm
else
  echo "File does not exist"
fi

echo ""
echo "=== Also checking the spec file for comparison ==="
sed -n '90,100p' ios/RNUsercentricsModuleSpec.h

Repository: Usercentrics/react-native-sdk

Length of output: 1390


Fix type mismatch in saveOptOutForCCPA declaration: change (BOOL *) to (BOOL)

The Objective-C method in ios/RNUsercentricsModule.mm (line 94) declares isOptedOut as (BOOL *) (pointer), but the spec file ios/RNUsercentricsModuleSpec.h (line 93) and the Swift implementation ios/RNUsercentricsModule.swift (line 208) both declare it as a value type: (BOOL) and Bool respectively. For React Native bridge compatibility, especially in the TurboModule path, the Objective-C declaration must match the spec and use the value type.

Fix
-RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
+RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL)isOptedOut
                  consentType:(double)consentType
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
📝 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
RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL)isOptedOut
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/RNUsercentricsModule.mm` around lines 94 - 97, The declaration of
saveOptOutForCCPA currently uses a BOOL pointer for isOptedOut; change the
parameter type from (BOOL *)isOptedOut to (BOOL)isOptedOut so it matches the
RNUsercentricsModuleSpec and Swift implementation and is compatible with the
React Native bridge; update the RCT_EXTERN_METHOD signature for
saveOptOutForCCPA accordingly and ensure the parameter name and ordering
(consentType, resolve, reject) remain unchanged.

@codeant-ai
Copy link

codeant-ai bot commented Feb 20, 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
Copy link

codeant-ai bot commented Feb 20, 2026

Sequence Diagram

This PR changes several TurboModule method signatures to accept JS numbers (double) and converts them to integer IDs/enums before invoking native SDK methods, preventing type-mismatch errors when calling consent APIs from JavaScript.

sequenceDiagram
    participant JS
    participant TurboModule
    participant NativeManager

    JS->>TurboModule: call acceptAll(consentType: Number, promise)
    TurboModule->>TurboModule: Convert Double -> Int (consentType.toInt())
    TurboModule->>NativeManager: invoke acceptAll(enum/index from Int)
    NativeManager-->>TurboModule: services (array)
    TurboModule-->>JS: resolve(promise, services)
Loading

Generated by CodeAnt AI

@codeant-ai codeant-ai bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Feb 20, 2026
@codeant-ai
Copy link

codeant-ai bot commented Feb 20, 2026

CodeAnt AI Incremental review completed.

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.

LGTM!

@uc-brunosouza uc-brunosouza merged commit 8aef055 into master Feb 20, 2026
4 checks passed
@uc-brunosouza uc-brunosouza deleted the MSDK-3293/type-mismatch-in-turboModule branch February 25, 2026 15:16
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.

5 participants