-
Notifications
You must be signed in to change notification settings - Fork 14
MSDK-3293: type mismatch in turbo module #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
90a4c93
840aafa
5e0b7d7
935755b
ba39411
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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()) | |||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @ReactMethod | |||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. values()[*.toint()] unchecked 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
|
|||||||||||||||||
| ) | |||||||||||||||||
|
Comment on lines
+152
to
166
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
All enum index lookups follow the pattern
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 |
|||||||||||||||||
| } | |||||||||||||||||
|
|
|||||||||||||||||
| @ReactMethod | |||||||||||||||||
|
|
|||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Ios bool pointer mismatch 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
Comment on lines
94
to
97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 The Objective-C method in Fix-RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL *)isOptedOut
+RCT_EXTERN_METHOD(saveOptOutForCCPA:(BOOL)isOptedOut
consentType:(double)consentType
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| 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) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))
} |
||
|
|
||
|
|
@@ -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 { | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The TurboModule spec method 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
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Ios selector mismatch 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
|
||||||
|
|
||||||
| - (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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
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.