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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ internal class RNUsercentricsModule(
}

@ReactMethod
override fun setCMPId(id: Int) {
usercentricsProxy.instance.setCMPId(id)
override fun setCMPId(id: Double) {
usercentricsProxy.instance.setCMPId(id.toInt())
}
Comment on lines +98 to 100
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)
}


@ReactMethod
Expand Down Expand Up @@ -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()
Comment on lines +134 to 147
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())
}

)
}

@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()
Comment on lines +134 to 165

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 +152 to 166
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())
}

}

@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) {
Comment on lines 98 to 206
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)
    }
}

usercentricsProxy.instance.track(UsercentricsAnalyticsEventType.values()[event.toInt()])
Comment on lines +134 to +207
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.

}

@ReactMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
abstract fun getABTestingVariant(promise: Promise)

@ReactMethod
abstract fun setCMPId(id: Int)
abstract fun setCMPId(id: Double)

@ReactMethod
abstract fun setABTestingVariant(variant: String)
Expand All @@ -64,34 +64,34 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
abstract fun changeLanguage(language: String, promise: Promise)

@ReactMethod
abstract fun acceptAll(consentType: Int, promise: Promise)
abstract fun acceptAll(consentType: Double, promise: Promise)

@ReactMethod
abstract fun acceptAllForTCF(fromLayer: Int, consentType: Int, promise: Promise)
abstract fun acceptAllForTCF(fromLayer: Double, consentType: Double, promise: Promise)

@ReactMethod
abstract fun denyAll(consentType: Int, promise: Promise)
abstract fun denyAll(consentType: Double, promise: Promise)

@ReactMethod
abstract fun denyAllForTCF(fromLayer: Int, consentType: Int, unsavedPurposeLIDecisions: ReadableArray, promise: Promise)
abstract fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise)

@ReactMethod
abstract fun saveDecisions(decisions: ReadableArray, consentType: Int, promise: Promise)
abstract fun saveDecisions(decisions: ReadableArray, consentType: Double, promise: Promise)

@ReactMethod
abstract fun saveDecisionsForTCF(
tcfDecisions: ReadableMap,
fromLayer: Int,
fromLayer: Double,
saveDecisions: ReadableArray,
consentType: Int,
consentType: Double,
promise: Promise
)

@ReactMethod
abstract fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Int, promise: Promise)
abstract fun saveOptOutForCCPA(isOptedOut: Boolean, consentType: Double, promise: Promise)

@ReactMethod
abstract fun track(event: Int)
abstract fun track(event: Double)

companion object {
const val NAME = "RNUsercentricsModule"
Expand Down
24 changes: 12 additions & 12 deletions ios/RNUsercentricsModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -60,47 +60,47 @@ @interface RCT_EXTERN_MODULE(RNUsercentricsModule, NSObject)
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(acceptAllForTCF:(NSInteger *)fromLayer
consentType:(NSInteger)consentType
RCT_EXTERN_METHOD(acceptAllForTCF:(double)fromLayer
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(acceptAll:(NSInteger *)consentType
RCT_EXTERN_METHOD(acceptAll:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(denyAllForTCF:(NSInteger *)fromLayer
consentType:(NSInteger)consentType
RCT_EXTERN_METHOD(denyAllForTCF:(double)fromLayer
consentType:(double)consentType
unsavedPurposeLIDecisions:(NSArray)unsavedPurposeLIDecisions
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(denyAll:(NSInteger *)consentType
RCT_EXTERN_METHOD(denyAll:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(saveDecisionsForTCF:(NSDictionary *)tcfDecisions
fromLayer:(NSInteger)fromLayer
fromLayer:(double)fromLayer
serviceDecisions:(NSArray)serviceDecisions
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(saveDecisions:(NSArray)serviceDecisions
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)

RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
Comment on lines 94 to 96
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)

reject:(RCTPromiseRejectBlock)reject)
Comment on lines 94 to 97

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

Comment on lines 94 to 97
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.


RCT_EXTERN_METHOD(setCMPId:(NSInteger *)id)
RCT_EXTERN_METHOD(setCMPId:(double)id)

RCT_EXTERN_METHOD(setABTestingVariant:(NSString *)variant)

RCT_EXTERN_METHOD(track:(NSInteger *)event)
RCT_EXTERN_METHOD(track:(double)event)

RCT_EXTERN_METHOD(clearUserSession:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
Expand Down
44 changes: 22 additions & 22 deletions ios/RNUsercentricsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class RNUsercentricsModule: NSObject {
}
}

@objc func setCMPId(_ id: Int) -> Void {
@objc func setCMPId(_ id: Double) -> Void {
usercentricsManager.setCMPId(id: Int32(id))
}
Comment on lines +84 to 86
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))
}


Expand Down Expand Up @@ -139,24 +139,24 @@ class RNUsercentricsModule: NSObject {
}
}

@objc func acceptAllForTCF(_ fromLayer: Int,
consentType: Int,
@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())
}

@objc func denyAllForTCF(_ fromLayer: Int,
consentType: Int,
@objc func denyAllForTCF(_ fromLayer: Double,
consentType: Double,
unsavedPurposeLIDecisions: [NSDictionary],
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) -> Void {
Expand All @@ -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())
}

Comment on lines 142 to 176
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())
}

@objc func denyAll(_ consentType: Int,
@objc func denyAll(_ consentType: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) -> Void {
let services = usercentricsManager.denyAll(consentType: .initialize(from: consentType))
let services = usercentricsManager.denyAll(consentType: .initialize(from: Int(consentType)))
resolve(services.toListOfDictionary())
}

@objc func saveDecisionsForTCF(_ tcfDecisions: NSDictionary,
fromLayer: Int,
fromLayer: Double,
serviceDecisions: [NSDictionary],
consentType: Int,
consentType: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) -> Void {

let services = usercentricsManager.saveDecisionsForTCF(
tcfDecisions: TCFUserDecisions(from: tcfDecisions),
fromLayer: .initialize(from: fromLayer),
fromLayer: .initialize(from: Int(fromLayer)),
serviceDecisions: serviceDecisions.compactMap { UserDecision(from: $0) },
consentType: .initialize(from: consentType))
consentType: .initialize(from: Int(consentType)))
resolve(services.toListOfDictionary())

}

@objc func saveDecisions(_ decisions: [NSDictionary],
consentType: Int,
consentType: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) -> Void {
let services = usercentricsManager.saveDecisions(decisions: decisions.compactMap { UserDecision.init(from: $0) }, consentType: .initialize(from: consentType))
let services = usercentricsManager.saveDecisions(decisions: decisions.compactMap { UserDecision.init(from: $0) }, consentType: .initialize(from: Int(consentType)))
resolve(services.toListOfDictionary())
}

@objc func saveOptOutForCCPA(_ isOptedOut: Bool,
consentType: Int,
consentType: Double,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock) -> Void {
let services = usercentricsManager.saveOptOutForCCPA(isOptedOut: isOptedOut, consentType: .initialize(from: consentType))
let services = usercentricsManager.saveOptOutForCCPA(isOptedOut: isOptedOut, consentType: .initialize(from: Int(consentType)))
resolve(services.toListOfDictionary())
}

@objc func track(_ event: Int) -> Void {
guard let usercentricsAnalyticsEventType = UsercentricsAnalyticsEventType.initialize(from: event) else { return }
@objc func track(_ event: Double) -> Void {
guard let usercentricsAnalyticsEventType = UsercentricsAnalyticsEventType.initialize(from: Int(event)) else { return }
usercentricsManager.track(event: usercentricsAnalyticsEventType)
}

Expand Down
24 changes: 12 additions & 12 deletions ios/RNUsercentricsModuleSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,51 +52,51 @@ NS_ASSUME_NONNULL_BEGIN
reject:(RCTPromiseRejectBlock)reject;

// Configuration Setters
- (void)setCMPId:(NSInteger)cmpId;
- (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
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.
👍 | 👎

consentType:(NSInteger)consentType
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;
Comment on lines 86 to 91

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


- (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;
Comment on lines +55 to +99
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) { /* ... */ }


@end

Expand Down