MSDK-3293: type mismatch in turbo module#183
Conversation
|
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 · |
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review Summary by QodoFix type mismatch in turbo module by converting int to double
WalkthroughsDescription• 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 Diagramflowchart 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"]
File Changes1. android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt
|
Sequence DiagramThis 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
Generated by CodeAnt AI |
|
PR Summary: Summary: Changes:
Files modified:
Impact:
|
| - (void)saveDecisionsForTCF:(NSDictionary *)tcfDecisions | ||
| fromLayer:(NSInteger)fromLayer | ||
| fromLayer:(double)fromLayer | ||
| saveDecisions:(NSArray<NSDictionary *> *)saveDecisions |
There was a problem hiding this comment.
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.| 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 finished reviewing your PR. |
| override fun setCMPId(id: Double) { | ||
| usercentricsProxy.instance.setCMPId(id.toInt()) | ||
| } |
There was a problem hiding this comment.
[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)
}| 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() |
There was a problem hiding this comment.
[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())
}| 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() | ||
| ) |
There was a problem hiding this comment.
[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())
}| @@ -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) { | |||
There was a problem hiding this comment.
[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)
}
}| RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut | ||
| consentType:(NSInteger)consentType | ||
| consentType:(double)consentType | ||
| resolve:(RCTPromiseResolveBlock)resolve |
There was a problem hiding this comment.
[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)| @objc func setCMPId(_ id: Double) -> Void { | ||
| usercentricsManager.setCMPId(id: Int32(id)) | ||
| } |
There was a problem hiding this comment.
[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))
}| @@ -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()) | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
[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())
}| - (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; |
There was a problem hiding this comment.
[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) { /* ... */ }|
Reviewed up to commit:935755be90f9499a9beb56b911c73bab0de1d421 Additional Suggestionandroid/src/androidTest/java/com/usercentrics/reactnative/RNUsercentricsModuleTest.kt, line:341-350Unit 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' });
}); |
Code Review by Qodo
1. values()[*.toInt()] unchecked
|
| 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() |
There was a problem hiding this comment.
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
| - (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; |
There was a problem hiding this comment.
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
| RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut | ||
| consentType:(NSInteger)consentType | ||
| consentType:(double)consentType | ||
| resolve:(RCTPromiseResolveBlock)resolve | ||
| reject:(RCTPromiseRejectBlock)reject) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)andInt(Double.infinity)cause a Swift runtime trapSwift's
Int(_:)initializer for floating-point values traps (fatal precondition failure) when the value isNaN, ±Infinity, or outside theIntrepresentable range. EveryInt(fromLayer),Int(consentType),Int(event), andInt32(id)conversion is unguarded against these special values.While the React Native bridge won't ordinarily forward
NaNorInfinityfrom well-typed JS callers, a missing validation makes the module fragile for unexpected inputs. Consider adding aisFinite/isNaNguard 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 deprecatedEnum.values()withEnum.entriesThe codebase targets Kotlin 2.0.21+ (and 1.9.22+ for examples), where
values()is deprecated in favour of.entries. Usingvalues()creates a new array on every call, which is unnecessary for indexed access. Theentriesproperty 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.
| 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()]) |
There was a problem hiding this comment.
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.
| RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut | ||
| consentType:(NSInteger)consentType | ||
| consentType:(double)consentType | ||
| resolve:(RCTPromiseResolveBlock)resolve | ||
| reject:(RCTPromiseRejectBlock)reject) |
There was a problem hiding this comment.
🧩 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 -A3Repository: 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.hRepository: 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.
| 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 is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR 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)
Generated by CodeAnt AI |
|
CodeAnt AI Incremental review completed. |
User description
Summary by CodeRabbit
Release Notes
CodeAnt-AI Description
Fix TurboModule numeric parameter handling so JS numbers are accepted by consent APIs
What Changed
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:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.