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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
def usercentrics_version = "2.24.1"
def usercentrics_version = "2.24.2"

group 'com.usercentrics.sdk.flutter'
version usercentrics_version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int, Boolean>)
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() })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DenyAllForTCFBridgeTest {
@Test
fun testInvoke() {
val usercentricsSDK = mockk<UsercentricsSDK>()
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()
Expand All @@ -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<UsercentricsSDK>()
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
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions example/test/fake_usercentrics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class FakeUsercentrics extends UsercentricsPlatform {
Future<List<UsercentricsServiceConsent>> denyAllForTCF({
required UsercentricsConsentType consentType,
required TCFDecisionUILayer fromLayer,
Map<int, bool>? unsavedPurposeLIDecisions,
}) {
throw UnimplementedError();
}
Expand Down
10 changes: 10 additions & 0 deletions ios/Classes/API/Bool+KotlinBoolean.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
3 changes: 2 additions & 1 deletion ios/Classes/Bridge/DenyAllForTCFBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ struct DenyAllForTCFBridge : MethodBridge {
let argsDict = call.arguments as! Dictionary<String, Any>
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() })
}
}
2 changes: 1 addition & 1 deletion ios/usercentrics_sdk.podspec
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 4 additions & 0 deletions lib/src/internal/bridge/deny_all_for_tcf_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ abstract class DenyAllForTCFBridge {
required MethodChannel channel,
required TCFDecisionUILayer fromLayer,
required UsercentricsConsentType consentType,
Map<int, bool>? unsavedPurposeLIDecisions,
});
}

Expand All @@ -24,12 +25,15 @@ class MethodChannelDenyAllForTCF extends DenyAllForTCFBridge {
required MethodChannel channel,
required TCFDecisionUILayer fromLayer,
required UsercentricsConsentType consentType,
Map<int, bool>? 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)
Expand Down
2 changes: 2 additions & 0 deletions lib/src/internal/platform/method_channel_usercentrics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,14 @@ class MethodChannelUsercentrics extends UsercentricsPlatform {
Future<List<UsercentricsServiceConsent>> denyAllForTCF({
required UsercentricsConsentType consentType,
required TCFDecisionUILayer fromLayer,
Map<int, bool>? unsavedPurposeLIDecisions,
}) async {
await _ensureIsReady();
return await denyAllForTCFBridge.invoke(
channel: _channel,
fromLayer: fromLayer,
consentType: consentType,
unsavedPurposeLIDecisions: unsavedPurposeLIDecisions,
);
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/platform/usercentrics_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ abstract class UsercentricsPlatform {
Future<List<UsercentricsServiceConsent>> denyAllForTCF({
required UsercentricsConsentType consentType,
required TCFDecisionUILayer fromLayer,
Map<int, bool>? unsavedPurposeLIDecisions,
});

Future<List<UsercentricsServiceConsent>> saveDecisions({
Expand Down
8 changes: 7 additions & 1 deletion lib/src/usercentrics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<UsercentricsServiceConsent>> denyAllForTCF({
required TCFDecisionUILayer fromLayer,
required UsercentricsConsentType consentType,
Map<int, bool>? unsavedPurposeLIDecisions,
}) =>
_delegate.denyAllForTCF(consentType: consentType, fromLayer: fromLayer);
_delegate.denyAllForTCF(
consentType: consentType,
fromLayer: fromLayer,
unsavedPurposeLIDecisions: unsavedPurposeLIDecisions,
);

/// Save service decisions.
static Future<List<UsercentricsServiceConsent>> saveDecisions({
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions test/internal/bridge/deny_all_for_tcf_bridge_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
3 changes: 3 additions & 0 deletions test/platform/fake_usercentrics_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,17 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform {
var denyAllForTCFCount = 0;
UsercentricsConsentType? denyAllForTCFConsentTypeArgument;
TCFDecisionUILayer? denyAllForTCFFromLayerArgument;
Map<int, bool>? denyAllForTCFUnsavedPurposeLIDecisionsArgument;

@override
Future<List<UsercentricsServiceConsent>> denyAllForTCF({
required UsercentricsConsentType consentType,
required TCFDecisionUILayer fromLayer,
Map<int, bool>? unsavedPurposeLIDecisions,
}) {
denyAllForTCFFromLayerArgument = fromLayer;
denyAllForTCFConsentTypeArgument = consentType;
denyAllForTCFUnsavedPurposeLIDecisionsArgument = unsavedPurposeLIDecisions;
denyAllForTCFCount++;
return Future.value(denyAllForTCFAnswer!);
}
Expand Down
Loading