-
Notifications
You must be signed in to change notification settings - Fork 14
feat(MSDK-3297): pass unsavedVendorLIDecisions through denyAllForTCF #190
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -149,10 +149,10 @@ internal class RNUsercentricsModule( | |
| } | ||
|
|
||
| @ReactMethod | ||
| override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) { | ||
| override fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, unsavedVendorLIDecisions: ReadableArray, promise: Promise) { | ||
| promise.resolve( | ||
| usercentricsProxy.instance.denyAllForTCF( | ||
| TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap() | ||
| TCFDecisionUILayer.values()[fromLayer.toInt()], UsercentricsConsentType.values()[consentType.toInt()], unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap(), unsavedVendorLIDecisions.deserializePurposeLIDecisionsMap() | ||
| ).toWritableArray() | ||
|
Comment on lines
+152
to
156
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. 4. Android tests signature mismatch Android instrumentation tests still call RNUsercentricsModule.denyAllForTCF with the old parameter list and mock UsercentricsSDK.denyAllForTCF with only 3 parameters, while the module now requires/passes unsavedVendorLIDecisions. This will fail androidTest compilation. Agent Prompt
Comment on lines
+152
to
156
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 reuse deserializePurposeLIDecisionsMap() for unsavedVendorLIDecisions (line ~155). If the deserializer is purpose-specific it may be misleading; either rename the helper to a generic deserializeLIDecisionsMap() if it handles both purposes and vendors, or add a separate vendor-specific deserializer for clarity. This avoids confusion for future maintainers and potential subtle bugs if vendor/purpose payloads differ. @ReactMethod
override fun denyAllForTCF(
fromLayer: Double,
consentType: Double,
unsavedPurposeLIDecisions: ReadableArray,
unsavedVendorLIDecisions: ReadableArray,
promise: Promise
) {
promise.resolve(
usercentricsProxy.instance.denyAllForTCF(
TCFDecisionUILayer.values()[fromLayer.toInt()],
UsercentricsConsentType.values()[consentType.toInt()],
unsavedPurposeLIDecisions.deserializePurposeLIDecisionsMap(),
unsavedVendorLIDecisions.deserializeVendorLIDecisionsMap()
).toWritableArray()
)
} |
||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -73,7 +73,7 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli | |||||
| abstract fun denyAll(consentType: Double, promise: Promise) | ||||||
|
|
||||||
| @ReactMethod | ||||||
| abstract fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, promise: Promise) | ||||||
| abstract fun denyAllForTCF(fromLayer: Double, consentType: Double, unsavedPurposeLIDecisions: ReadableArray, unsavedVendorLIDecisions: ReadableArray, promise: Promise) | ||||||
|
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: This signature change introduces a native contract mismatch: other integration points (notably the TurboModule codegen spec in Severity Level: Critical 🚨- ❌ TCF deny-all can fail on TurboModule path.
- ❌ iOS bridge declarations mismatch Swift method signature.
- ⚠️ Vendor LI decisions may be silently ignored.
Suggested change
Steps of Reproduction ✅1. Build the module with New Architecture enabled; `package.json:105-108` sets codegen
source to `./src/fabric`.
2. Inspect `src/fabric/NativeUsercentricsModule.ts:36`: `denyAllForTCF` still declares
only one LI array, while Android native spec now expects two arrays at
`android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt:76`.
3. Trigger TCF deny-all from JS via `src/Usercentrics.tsx:110-113`, which always calls
`RNUsercentricsModule.denyAllForTCF(fromLayer, consentType, unsavedPurposeLIDecisions,
unsavedVendorLIDecisions)`.
4. Runtime bridge path is `src/NativeUsercentrics.ts:66` (`TurboModuleRegistry.get`
first), so argument contract comes from codegen spec; mismatch can cause call failure or
dropped `unsavedVendorLIDecisions`.
5. iOS is also inconsistent: bridge extern/header expose one array
(`ios/RNUsercentricsModule.mm:72-76`, `ios/RNUsercentricsModuleSpec.h:75-20`), but Swift
implementation requires two (`ios/RNUsercentricsModule.swift:160-170`), confirming
cross-layer API drift is real.Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModuleSpec.kt
**Line:** 76:76
**Comment:**
*Possible Bug: This signature change introduces a native contract mismatch: other integration points (notably the TurboModule codegen spec in `src/fabric/NativeUsercentricsModule.ts` and Objective-C extern declarations) still expose `denyAllForTCF` with only one LI decisions array. In new-architecture builds this causes argument-count/signature inconsistency, so the call can fail at runtime or silently drop the new vendor decisions argument. Keep the old signature here until all bridge specs are updated atomically in the same PR.
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. |
||||||
|
|
||||||
| @ReactMethod | ||||||
| abstract fun saveDecisions(decisions: ReadableArray, consentType: Double, promise: Promise) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ public protocol UsercentricsManager { | |
| func acceptAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] | ||
| func acceptAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] | ||
|
|
||
| func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] | ||
| func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] | ||
| func denyAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] | ||
|
|
||
| func saveDecisionsForTCF(tcfDecisions: TCFUserDecisions, | ||
|
|
@@ -112,8 +112,8 @@ final class UsercentricsManagerImplementation: UsercentricsManager { | |
| return UsercentricsCore.shared.acceptAll(consentType: consentType) | ||
| } | ||
|
|
||
| func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] { | ||
| return UsercentricsCore.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType, unsavedPurposeLIDecisions: unsavedPurposeLIDecisions) | ||
| func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent] { | ||
| return UsercentricsCore.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType, unsavedPurposeLIDecisions: unsavedPurposeLIDecisions, unsavedVendorLIDecisions: unsavedVendorLIDecisions) | ||
| } | ||
|
Comment on lines
+115
to
117
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
# Description: Check the Usercentrics SDK version requirement and any documentation for denyAllForTCF signature
# Find Podspec or Package.swift to see SDK version constraints
echo "=== Checking Podspec for Usercentrics dependency version ==="
fd -e podspec --exec cat {}
echo ""
echo "=== Checking Package.swift for Usercentrics dependency ==="
fd "Package.swift" --exec cat {}
echo ""
echo "=== Checking Podfile for Usercentrics version ==="
fd "Podfile" --exec cat {}Repository: Usercentrics/react-native-sdk Length of output: 50385 🏁 Script executed: #!/bin/bash
# Find package.json to confirm the version
fd "package.json" --max-results 5 -x cat {}Repository: Usercentrics/react-native-sdk Length of output: 284 🏁 Script executed: #!/bin/bash
# Find and read package.json
fd "package.json" | head -1 | xargs catRepository: Usercentrics/react-native-sdk Length of output: 2395 🏁 Script executed: #!/bin/bash
# Look for the main package definition with iosPackageVersion
fd "package.json" | xargs grep -l "iosPackageVersion" 2>/dev/nullRepository: Usercentrics/react-native-sdk Length of output: 85 🏁 Script executed: #!/bin/bash
# Find all package.json files and check for iosPackageVersion
find . -name "package.json" -type f | while read f; do
if grep -q "iosPackageVersion" "$f"; then
echo "=== $f ==="
cat "$f"
fi
doneRepository: Usercentrics/react-native-sdk Length of output: 3211 🏁 Script executed: #!/bin/bash
# Check if there's a CHANGELOG or release notes mentioning unsavedVendorLIDecisions
fd -e md -e txt | xargs grep -l "unsavedVendorLIDecisions\|2.25.1" 2>/dev/null | head -10Repository: Usercentrics/react-native-sdk Length of output: 85 🏁 Script executed: #!/bin/bash
# Read the CHANGELOG and look for references to unsavedVendorLIDecisions or denyAllForTCF
cat CHANGELOG.md | head -200Repository: Usercentrics/react-native-sdk Length of output: 7641 🏁 Script executed: #!/bin/bash
# Look at the implementation to understand the signature
fd "UsercentricsManager.swift" -x cat {}Repository: Usercentrics/react-native-sdk Length of output: 21193 Fix FakeUsercentricsManager mock to match protocol signature. The protocol Update FakeUsercentricsManager.denyAllForTCF signature:// Current (incorrect):
func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent]
// Should be:
func denyAllForTCF(fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?, unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?) -> [UsercentricsServiceConsent]Also add a corresponding property to store the vendor LI decisions: var denyAllForTCFUnsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?🤖 Prompt for AI Agents |
||
|
|
||
| func denyAll(consentType: UsercentricsConsentType) -> [UsercentricsServiceConsent] { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -160,20 +160,26 @@ class RNUsercentricsModule: NSObject { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @objc func denyAllForTCF(_ fromLayer: Double, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| consentType: Double, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsavedPurposeLIDecisions: [NSDictionary], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsavedVendorLIDecisions: [NSDictionary], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve: @escaping RCTPromiseResolveBlock, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reject: @escaping RCTPromiseRejectBlock) -> Void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
160
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. 5. Ios example tests outdated The iOS example test still calls denyAllForTCF without the new unsavedVendorLIDecisions parameter, but RNUsercentricsModule.denyAllForTCF now requires it. This will fail compiling the example test target. Agent Prompt
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var decisions: [KotlinInt: KotlinBoolean]? = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !unsavedPurposeLIDecisions.isEmpty { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decisions = [:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for dict in unsavedPurposeLIDecisions { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let id = dict["id"] as? Int, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let consent = dict["legitimateInterestConsent"] as? Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decisions?[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let services = usercentricsManager.denyAllForTCF( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fromLayer: .initialize(from: Int(fromLayer)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| consentType: .initialize(from: Int(consentType)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsavedPurposeLIDecisions: extractLIDecisionsMap(unsavedPurposeLIDecisions), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unsavedVendorLIDecisions: extractLIDecisionsMap(unsavedVendorLIDecisions) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve(services.toListOfDictionary()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
qodo-code-review[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
160
to
+173
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 new Severity Level: Critical 🚨- ❌ iOS build breaks on denyAllForTCF manager call.
- ❌ Bridge selector mismatch blocks denyAllForTCF native invocation.
- ⚠️ TCF deny-all flow unavailable for iOS consumers.
Suggested change
Steps of Reproduction ✅1. Build the iOS module after this PR change; `RNUsercentricsModule.swift:166-170` calls
`usercentricsManager.denyAllForTCF(..., unsavedVendorLIDecisions: ...)`.
2. Open `ios/Manager/UsercentricsManager.swift:31` and `:115-116`; protocol/implementation
only define `denyAllForTCF(fromLayer, consentType, unsavedPurposeLIDecisions)` with no
vendor argument.
3. Compiler cannot resolve that call signature in `RNUsercentricsModule.swift`, so iOS
build fails before runtime.
4. Even if compiled with manual workarounds, bridge contracts still mismatch:
`ios/RNUsercentricsModule.mm:72-77` and `ios/RNUsercentricsModuleSpec.h:75-79` export old
3-argument selector, while Swift exposes a 4-argument selector at
`ios/RNUsercentricsModule.swift:160-165`, breaking RN invocation path.Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** ios/RNUsercentricsModule.swift
**Line:** 160:173
**Comment:**
*Possible Bug: The new `denyAllForTCF` Swift selector now requires `unsavedVendorLIDecisions`, but the iOS bridge/spec and manager contract still expose the old 3-argument method. This breaks the native contract and can fail at runtime with argument-count/selector mismatch. Keep this method signature backward-compatible until all bridge/spec/manager layers are updated together.
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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func extractLIDecisionsMap(_ decisions: [NSDictionary]) -> [KotlinInt: KotlinBoolean]? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard !decisions.isEmpty else { return nil } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decisions.reduce(into: [:]) { result, dict in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let id = dict["id"] as? Int, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let consent = dict["legitimateInterestConsent"] as? Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+175
to
+180
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. Keep missing LI flags consistent with Android. This helper drops entries when Suggested fix private func extractLIDecisionsMap(_ decisions: [NSDictionary]) -> [KotlinInt: KotlinBoolean]? {
guard !decisions.isEmpty else { return nil }
return decisions.reduce(into: [:]) { result, dict in
- if let id = dict["id"] as? Int,
- let consent = dict["legitimateInterestConsent"] as? Bool {
- result[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent)
- }
+ guard let id = dict["id"] as? Int else { return }
+ let consent = (dict["legitimateInterestConsent"] as? Bool) ?? false
+ result[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent)
}
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
160
to
181
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] You added a new parameter unsavedVendorLIDecisions and now call usercentricsManager.denyAllForTCF(...) with both unsavedPurposeLIDecisions and unsavedVendorLIDecisions (see new lines ~160-171). The UsercentricsManager protocol and its implementations still expose denyAllForTCF(fromLayer:..., consentType:..., unsavedPurposeLIDecisions:...) (reference: ios/Manager/UsercentricsManager.swift lines 16-46 and 100-131). This will cause a compile error. Actionable steps: update the UsercentricsManager protocol to accept the new unsavedVendorLIDecisions parameter, update all concrete implementations / fake implementations (example/sample projects and tests), and run a clean build. Also ensure the new helper extractLIDecisionsMap handles edge cases and returns nil when appropriate (you already guard emptiness, good). // ios/Manager/UsercentricsManager.swift
protocol UsercentricsManaging {
// ... existing methods ...
func denyAllForTCF(
fromLayer: TCFDecisionUILayer,
consentType: UsercentricsConsentType,
unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?,
unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?
) -> [UsercentricsServiceConsent]
}
final class UsercentricsManager: UsercentricsManaging {
// ... existing impl ...
func denyAllForTCF(
fromLayer: TCFDecisionUILayer,
consentType: UsercentricsConsentType,
unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?,
unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?
) -> [UsercentricsServiceConsent] {
return UsercentricsCore.shared.denyAllForTCF(
fromLayer: fromLayer,
consentType: consentType,
unsavedPurposeLIDecisions: unsavedPurposeLIDecisions,
unsavedVendorLIDecisions: unsavedVendorLIDecisions
)
}
}
// example/ios/exampleTests/Fake/FakeUsercentricsManager.swift
class FakeUsercentricsManager: UsercentricsManaging {
// ... existing fakes ...
var denyAllForTCFConsentType: UsercentricsConsentType?
var denyAllForTCFFromLayer: TCFDecisionUILayer?
var denyAllForTCFUnsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?
var denyAllForTCFUnsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?
var denyAllForTCFResponse: [UsercentricsServiceConsent]?
func denyAllForTCF(
fromLayer: TCFDecisionUILayer,
consentType: UsercentricsConsentType,
unsavedPurposeLIDecisions: [KotlinInt: KotlinBoolean]?,
unsavedVendorLIDecisions: [KotlinInt: KotlinBoolean]?
) -> [UsercentricsServiceConsent] {
self.denyAllForTCFConsentType = consentType
self.denyAllForTCFFromLayer = fromLayer
self.denyAllForTCFUnsavedPurposeLIDecisions = unsavedPurposeLIDecisions
self.denyAllForTCFUnsavedVendorLIDecisions = unsavedVendorLIDecisions
return denyAllForTCFResponse!
}
}
// ios/RNUsercentricsModule.mm
RCT_EXTERN_METHOD(denyAllForTCF:(double)fromLayer
consentType:(double)consentType
unsavedPurposeLIDecisions:(NSArray)unsavedPurposeLIDecisions
unsavedVendorLIDecisions:(NSArray)unsavedVendorLIDecisions
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
// ios/RNUsercentricsModuleSpec.h
- (void)denyAllForTCF:(double)fromLayer
consentType:(double)consentType
unsavedPurposeLIDecisions:(NSArray<NSDictionary *> *)unsavedPurposeLIDecisions
unsavedVendorLIDecisions:(NSArray<NSDictionary *> *)unsavedVendorLIDecisions
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;
Comment on lines
+178
to
181
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: Severity Level: Major
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let id = dict["id"] as? Int, | |
| let consent = dict["legitimateInterestConsent"] as? Bool { | |
| result[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent) | |
| } | |
| if let id = dict["id"] as? Int { | |
| let consent = (dict["legitimateInterestConsent"] as? Bool) ?? false | |
| result[KotlinInt(int: Int32(id))] = KotlinBoolean(bool: consent) | |
| } |
Steps of Reproduction ✅
1. Call public API `Usercentrics.denyAllForTCF(...)` from JS
(`src/Usercentrics.tsx:110-112`) with objects that include `id` but omit
`legitimateInterestConsent`.
2. This input is valid by type design because `legitimateInterestConsent` is optional in
`src/models/TCFUserDecisions.tsx:30` and `:55`.
3. iOS path reaches `extractLIDecisionsMap` (`ios/RNUsercentricsModule.swift:175-181`),
where missing `legitimateInterestConsent` fails the `if let` and drops that entry.
4. Android path for the same payload defaults missing value to `false`
(`android/.../UserDecisionExtensions.kt:34-35`), so platform behavior diverges and iOS
silently loses decisions.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** ios/RNUsercentricsModule.swift
**Line:** 178:181
**Comment:**
*Logic Error: `extractLIDecisionsMap` currently drops entries when `legitimateInterestConsent` is missing, which can silently lose user decisions; Android defaults missing values to `false`. Use the same fallback to keep behavior consistent and avoid accidental data loss.
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.| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { | |||||||||||||
| TCFData, | ||||||||||||||
| TCFDecisionUILayer, | ||||||||||||||
| TCFUserDecisionOnPurpose, | ||||||||||||||
| TCFUserDecisionOnVendor, | ||||||||||||||
| TCFUserDecisions, | ||||||||||||||
| UsercentricsAnalyticsEventType, | ||||||||||||||
| UsercentricsCMPData, | ||||||||||||||
|
|
@@ -106,9 +107,9 @@ export const Usercentrics = { | |||||||||||||
| return RNUsercentricsModule.denyAll(consentType); | ||||||||||||||
| }, | ||||||||||||||
|
|
||||||||||||||
| denyAllForTCF: async (fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: TCFUserDecisionOnPurpose[] = []): Promise<Array<UsercentricsServiceConsent>> => { | ||||||||||||||
| denyAllForTCF: async (fromLayer: TCFDecisionUILayer, consentType: UsercentricsConsentType, unsavedPurposeLIDecisions: TCFUserDecisionOnPurpose[] = [], unsavedVendorLIDecisions: TCFUserDecisionOnVendor[] = []): Promise<Array<UsercentricsServiceConsent>> => { | ||||||||||||||
| await RNUsercentricsModule.isReady(); | ||||||||||||||
| return RNUsercentricsModule.denyAllForTCF(fromLayer, consentType, unsavedPurposeLIDecisions); | ||||||||||||||
| return RNUsercentricsModule.denyAllForTCF(fromLayer, consentType, unsavedPurposeLIDecisions, unsavedVendorLIDecisions); | ||||||||||||||
|
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 new native call always sends four arguments, but the iOS bridge export ( Severity Level: Critical 🚨- ❌ TCF deny-all fails on iOS bridge path.
- ❌ Turbo deny-all call breaks from arity mismatch.
- ⚠️ Consent rejection action may not persist.
Suggested change
Steps of Reproduction ✅1. Trigger the TCF deny flow from real callers in `sample/src/screens/CustomUI.tsx:207` or
`example/src/screens/CustomUI.tsx:207`, which call
`Usercentrics.denyAllForTCF(TCFDecisionUILayer.firstLayer,
UsercentricsConsentType.explicit)`.
2. Execution enters `src/Usercentrics.tsx:110-112`; default params create both LI arrays
and JS always calls native with 4 arguments: `fromLayer, consentType,
unsavedPurposeLIDecisions, unsavedVendorLIDecisions`.
3. On iOS legacy bridge, exported native contract is still 3-arg (single LI array) in
`ios/RNUsercentricsModule.mm:72-75` and `ios/RNUsercentricsModuleSpec.h:75-78`; Turbo
codegen contract is also 3-arg in `src/fabric/NativeUsercentricsModule.ts:36`.
4. Because module resolution can use Turbo/bridge (`src/NativeUsercentrics.ts:66`, then
consumed in `src/Usercentrics.tsx:23`), invoking this path on non-Android-legacy runtimes
hits argument-count mismatch and `denyAllForTCF` fails at runtime.Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** src/Usercentrics.tsx
**Line:** 112:112
**Comment:**
*Possible Bug: The new native call always sends four arguments, but the iOS bridge export (`RNUsercentricsModule.mm`) and the TurboModule codegen spec (`src/fabric/NativeUsercentricsModule.ts`) still define `denyAllForTCF` with only one LI-decisions array. This creates a runtime argument-count mismatch (method not found/wrong arg count) on those paths. Add a compatibility dispatch so four arguments are only sent on the legacy Android path that supports them, and use the 3-argument call elsewhere until native contracts are fully aligned.
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. |
||||||||||||||
| }, | ||||||||||||||
qodo-code-review[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
| saveDecisions: async (decisions: UserDecision[], consentType: UsercentricsConsentType): Promise<Array<UsercentricsServiceConsent>> => { | ||||||||||||||
|
|
||||||||||||||
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.
Suggestion:
unsavedVendorLIDecisionsis deserialized with a helper that force-defaults missinglegitimateInterestConsentvalues tofalse. Since this field is optional in the JS model, missing values get converted into explicit denials, which can overwrite vendor LI state incorrectly. Build the vendor map by only including entries wherelegitimateInterestConsentis present. [logic error]Severity Level: Critical 🚨
Steps of Reproduction ✅
Prompt for AI Agent 🤖