diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe6c0f3..e4042dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ [Release Notes](https://docs.usercentrics.com/cmp_in_app_sdk/latest/about/history/) +### 2.24.2 – Dec 5, 2025 +## Improvement +* Patch with security fixes + ### 2.24.1 – Oct 31, 2025 ## Improvement * TCF 2.3 Support: fixes about tcString diff --git a/android/build.gradle b/android/build.gradle index 5aedae62..297ee13c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,4 @@ -def usercentrics_version = "2.24.1" +def usercentrics_version = "2.24.2" group 'com.usercentrics.sdk.flutter' version usercentrics_version diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridge.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridge.kt index 5d6d782a..a4dfcefb 100644 --- a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridge.kt +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridge.kt @@ -15,12 +15,15 @@ internal class DenyAllForTCFBridge( override val name: String get() = "denyAllForTCF" + @Suppress("UNCHECKED_CAST") override fun invoke(call: FlutterMethodCall, result: FlutterResult) { assert(name == call.method) val argsMap = call.arguments as Map<*, *> + val unsavedPurposeLIDecisions = (argsMap["unsavedPurposeLIDecisions"] as? Map) val consents = usercentrics.instance.denyAllForTCF( fromLayer = TCFDecisionUILayer.valueOf(argsMap["fromLayer"] as String), - consentType = UsercentricsConsentType.valueOf(argsMap["consentType"] as String) + consentType = UsercentricsConsentType.valueOf(argsMap["consentType"] as String), + unsavedPurposeLIDecisions = unsavedPurposeLIDecisions ) result.success(consents.map { it.serialize() }) } diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridgeTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridgeTest.kt index 4edcb5ed..b986a877 100644 --- a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridgeTest.kt +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/DenyAllForTCFBridgeTest.kt @@ -32,7 +32,7 @@ class DenyAllForTCFBridgeTest { @Test fun testInvoke() { val usercentricsSDK = mockk() - every { usercentricsSDK.denyAllForTCF(any(), any()) }.returns(DenyAllForTCFMock.fake) + every { usercentricsSDK.denyAllForTCF(any(), any(), any()) }.returns(DenyAllForTCFMock.fake) val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) val instance = DenyAllForTCFBridge(usercentricsProxy) val result = FakeFlutterResult() @@ -42,7 +42,30 @@ class DenyAllForTCFBridgeTest { verify(exactly = 1) { usercentricsSDK.denyAllForTCF( fromLayer = DenyAllForTCFMock.callFromLayer, - consentType = DenyAllForTCFMock.callConsentType + consentType = DenyAllForTCFMock.callConsentType, + unsavedPurposeLIDecisions = null + ) + } + + Assert.assertEquals(1, result.successCount) + Assert.assertEquals(DenyAllForTCFMock.expected, result.successResultArgument) + } + + @Test + fun testInvokeWithUnsavedPurposeLIDecisions() { + val usercentricsSDK = mockk() + every { usercentricsSDK.denyAllForTCF(any(), any(), any()) }.returns(DenyAllForTCFMock.fake) + val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) + val instance = DenyAllForTCFBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(DenyAllForTCFMock.callWithUnsavedPurposeLIDecisions, result) + + verify(exactly = 1) { + usercentricsSDK.denyAllForTCF( + fromLayer = DenyAllForTCFMock.callFromLayer, + consentType = DenyAllForTCFMock.callConsentType, + unsavedPurposeLIDecisions = DenyAllForTCFMock.callUnsavedPurposeLIDecisions ) } diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/DenyAllForTCFMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/DenyAllForTCFMock.kt index 06dd6b48..2c5e2834 100644 --- a/android/src/test/java/com/usercentrics/sdk/flutter/mock/DenyAllForTCFMock.kt +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/DenyAllForTCFMock.kt @@ -34,8 +34,18 @@ internal object DenyAllForTCFMock { "consentType" to "EXPLICIT" ) ) + + val callWithUnsavedPurposeLIDecisions = FakeFlutterMethodCall( + method = "denyAllForTCF", arguments = mapOf( + "fromLayer" to "FIRST_LAYER", + "consentType" to "EXPLICIT", + "unsavedPurposeLIDecisions" to mapOf(1 to true, 2 to false) + ) + ) + val callFromLayer = TCFDecisionUILayer.FIRST_LAYER val callConsentType = UsercentricsConsentType.EXPLICIT + val callUnsavedPurposeLIDecisions = mapOf(1 to true, 2 to false) val expected = listOf( mapOf( "templateId" to "ocv9HNX_g", diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 818a33d0..0cb1ef85 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,11 +1,11 @@ PODS: - Flutter (1.0.0) - - Usercentrics (2.24.1) - - usercentrics_sdk (2.24.1): + - Usercentrics (2.24.2) + - usercentrics_sdk (2.24.2): - Flutter - - UsercentricsUI (= 2.24.1) - - UsercentricsUI (2.24.1): - - Usercentrics (= 2.24.1) + - UsercentricsUI (= 2.24.2) + - UsercentricsUI (2.24.2): + - Usercentrics (= 2.24.2) - webview_flutter_wkwebview (0.0.1): - Flutter diff --git a/example/test/fake_usercentrics.dart b/example/test/fake_usercentrics.dart index 7e246341..a6fdfcb2 100644 --- a/example/test/fake_usercentrics.dart +++ b/example/test/fake_usercentrics.dart @@ -80,6 +80,7 @@ class FakeUsercentrics extends UsercentricsPlatform { Future> denyAllForTCF({ required UsercentricsConsentType consentType, required TCFDecisionUILayer fromLayer, + Map? unsavedPurposeLIDecisions, }) { throw UnimplementedError(); } diff --git a/ios/Classes/API/Bool+KotlinBoolean.swift b/ios/Classes/API/Bool+KotlinBoolean.swift index 238495f4..3b485f9f 100644 --- a/ios/Classes/API/Bool+KotlinBoolean.swift +++ b/ios/Classes/API/Bool+KotlinBoolean.swift @@ -8,3 +8,13 @@ extension Bool { self.init(value.boolValue) } } + +extension Dictionary where Key == Int, Value == Bool { + func asKotlinIntBooleanDict() -> [KotlinInt: KotlinBoolean] { + var result: [KotlinInt: KotlinBoolean] = [:] + for (key, value) in self { + result[KotlinInt(int: Int32(key))] = KotlinBoolean(bool: value) + } + return result + } +} diff --git a/ios/Classes/Bridge/DenyAllForTCFBridge.swift b/ios/Classes/Bridge/DenyAllForTCFBridge.swift index 2c52c7f9..47f6162c 100644 --- a/ios/Classes/Bridge/DenyAllForTCFBridge.swift +++ b/ios/Classes/Bridge/DenyAllForTCFBridge.swift @@ -10,7 +10,8 @@ struct DenyAllForTCFBridge : MethodBridge { let argsDict = call.arguments as! Dictionary let fromLayer = TCFDecisionUILayer.initialize(from: argsDict["fromLayer"])! let consentType = UsercentricsConsentType.initialize(from: argsDict["consentType"])! - let consents = usercentrics.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType) + let unsavedPurposeLIDecisions = (argsDict["unsavedPurposeLIDecisions"] as? [Int: Bool])?.asKotlinIntBooleanDict() + let consents = usercentrics.shared.denyAllForTCF(fromLayer: fromLayer, consentType: consentType, unsavedPurposeLIDecisions: unsavedPurposeLIDecisions) result(consents.map { $0.serialize() }) } } diff --git a/ios/usercentrics_sdk.podspec b/ios/usercentrics_sdk.podspec index fff3502c..80374a3f 100644 --- a/ios/usercentrics_sdk.podspec +++ b/ios/usercentrics_sdk.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'usercentrics_sdk' - s.version = '2.24.1' + s.version = '2.24.2' s.summary = 'Usercentrics Flutter SDK.' s.description = <<-DESC Usercentrics Flutter SDK. diff --git a/lib/src/internal/bridge/deny_all_for_tcf_bridge.dart b/lib/src/internal/bridge/deny_all_for_tcf_bridge.dart index 2a060d47..b09554c4 100644 --- a/lib/src/internal/bridge/deny_all_for_tcf_bridge.dart +++ b/lib/src/internal/bridge/deny_all_for_tcf_bridge.dart @@ -11,6 +11,7 @@ abstract class DenyAllForTCFBridge { required MethodChannel channel, required TCFDecisionUILayer fromLayer, required UsercentricsConsentType consentType, + Map? unsavedPurposeLIDecisions, }); } @@ -24,12 +25,15 @@ class MethodChannelDenyAllForTCF extends DenyAllForTCFBridge { required MethodChannel channel, required TCFDecisionUILayer fromLayer, required UsercentricsConsentType consentType, + Map? unsavedPurposeLIDecisions, }) async { final result = await channel.invokeMethod( _name, { 'fromLayer': TCFDecisionUILayerSerializer.serialize(fromLayer), 'consentType': ConsentTypeSerializer.serialize(consentType), + if (unsavedPurposeLIDecisions != null) + 'unsavedPurposeLIDecisions': unsavedPurposeLIDecisions, }, ); return (result as List) diff --git a/lib/src/internal/platform/method_channel_usercentrics.dart b/lib/src/internal/platform/method_channel_usercentrics.dart index c404c63f..26badee1 100644 --- a/lib/src/internal/platform/method_channel_usercentrics.dart +++ b/lib/src/internal/platform/method_channel_usercentrics.dart @@ -216,12 +216,14 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { Future> denyAllForTCF({ required UsercentricsConsentType consentType, required TCFDecisionUILayer fromLayer, + Map? unsavedPurposeLIDecisions, }) async { await _ensureIsReady(); return await denyAllForTCFBridge.invoke( channel: _channel, fromLayer: fromLayer, consentType: consentType, + unsavedPurposeLIDecisions: unsavedPurposeLIDecisions, ); } diff --git a/lib/src/platform/usercentrics_platform.dart b/lib/src/platform/usercentrics_platform.dart index c6d2d248..07edc6bd 100644 --- a/lib/src/platform/usercentrics_platform.dart +++ b/lib/src/platform/usercentrics_platform.dart @@ -70,6 +70,7 @@ abstract class UsercentricsPlatform { Future> denyAllForTCF({ required UsercentricsConsentType consentType, required TCFDecisionUILayer fromLayer, + Map? unsavedPurposeLIDecisions, }); Future> saveDecisions({ diff --git a/lib/src/usercentrics.dart b/lib/src/usercentrics.dart index 5121b4ff..8e840d8d 100644 --- a/lib/src/usercentrics.dart +++ b/lib/src/usercentrics.dart @@ -163,11 +163,17 @@ class Usercentrics { _delegate.denyAll(consentType: consentType); /// Deny all services and TCF. + /// - The [unsavedPurposeLIDecisions] is an optional map of purpose IDs to their legitimate interest decisions that have not yet been saved. static Future> denyAllForTCF({ required TCFDecisionUILayer fromLayer, required UsercentricsConsentType consentType, + Map? unsavedPurposeLIDecisions, }) => - _delegate.denyAllForTCF(consentType: consentType, fromLayer: fromLayer); + _delegate.denyAllForTCF( + consentType: consentType, + fromLayer: fromLayer, + unsavedPurposeLIDecisions: unsavedPurposeLIDecisions, + ); /// Save service decisions. static Future> saveDecisions({ diff --git a/pubspec.yaml b/pubspec.yaml index fe472697..6f1b2763 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ repository: https://github.com/Usercentrics/flutter-sdk/ # [X] android/build.gradle # [X] ios/usercentrics_sdk.podspec + pod install/update # [X] CHANGELOG.md -version: 2.24.1 +version: 2.24.2 environment: sdk: ">=2.17.1 <4.0.0" diff --git a/test/internal/bridge/deny_all_for_tcf_bridge_test.dart b/test/internal/bridge/deny_all_for_tcf_bridge_test.dart index 69081765..8440002c 100644 --- a/test/internal/bridge/deny_all_for_tcf_bridge_test.dart +++ b/test/internal/bridge/deny_all_for_tcf_bridge_test.dart @@ -81,4 +81,35 @@ void main() { expect(receivedCall?.arguments, expectedArguments); expect(result, expectedResult); }); + + test('invoke with unsavedPurposeLIDecisions', () async { + int callCounter = 0; + MethodCall? receivedCall; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return mockResponse; + }); + + const instance = MethodChannelDenyAllForTCF(); + const mockUnsavedPurposeLIDecisions = {1: true, 2: false, 3: true}; + + final result = await instance.invoke( + channel: channel, + fromLayer: mockFromLayer, + consentType: mockConsentType, + unsavedPurposeLIDecisions: mockUnsavedPurposeLIDecisions, + ); + + expect(callCounter, 1); + expect(receivedCall?.method, 'denyAllForTCF'); + expect(receivedCall?.arguments, { + "fromLayer": "FIRST_LAYER", + "consentType": "EXPLICIT", + "unsavedPurposeLIDecisions": mockUnsavedPurposeLIDecisions, + }); + expect(result, expectedResult); + }); } diff --git a/test/platform/fake_usercentrics_platform.dart b/test/platform/fake_usercentrics_platform.dart index a3c966e6..b70a311f 100644 --- a/test/platform/fake_usercentrics_platform.dart +++ b/test/platform/fake_usercentrics_platform.dart @@ -194,14 +194,17 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { var denyAllForTCFCount = 0; UsercentricsConsentType? denyAllForTCFConsentTypeArgument; TCFDecisionUILayer? denyAllForTCFFromLayerArgument; + Map? denyAllForTCFUnsavedPurposeLIDecisionsArgument; @override Future> denyAllForTCF({ required UsercentricsConsentType consentType, required TCFDecisionUILayer fromLayer, + Map? unsavedPurposeLIDecisions, }) { denyAllForTCFFromLayerArgument = fromLayer; denyAllForTCFConsentTypeArgument = consentType; + denyAllForTCFUnsavedPurposeLIDecisionsArgument = unsavedPurposeLIDecisions; denyAllForTCFCount++; return Future.value(denyAllForTCFAnswer!); }