diff --git a/CHANGELOG.md b/CHANGELOG.md index db71e41..5cfe2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ [Release Notes](https://docs.usercentrics.com/cmp_in_app_sdk/latest/about/history/) +### 2.13.2 - March 13, 2024 + +## Features + +**Clear User Session** - Introducing a new API designed to simplify the process of clearing user sessions + +## Improvements + +**Google Consent Mode Granular Choices** - Enhances integration with Google SDKs by updating to the latest changes. + +**Adjust Granular Consent** - By Using Consent Mediation, we have fully integrated with Adjust SDK updates associated with the DMA + +## iOS Bug Fixes + +**[Fix]** Adjusts in landscape mode where labels were not fully aligned with other elements of the screen +**[tvOS Fix]** Numerous layout modifications have been made to address the arrangement of titles and the rendering of other elements in languages that result in larger text sizes + +## Other Fixes + +**[Fix]** Removes deprecated field TCFVendor::deviceStorage +**[Fix]** In certain scenarios, the 'Save Settings' button color was not customizable + + ### 2.13.1 - March 05, 2024 ## Hotfix diff --git a/android/build.gradle b/android/build.gradle index 79f4407..23a56a7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,4 @@ -def usercentrics_version = "2.13.0" +def usercentrics_version = "2.13.2" group 'com.usercentrics.sdk.flutter' version usercentrics_version diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt index 48abfaf..750af94 100644 --- a/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt @@ -64,6 +64,7 @@ class UsercentricsPlugin : FlutterPlugin, SetABTestingVariantBridge(), TrackBridge(), GetAdditionalConsentModeDataBridge(), + ClearUserSessionBridge() ).associateBy { it.name } } diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridge.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridge.kt new file mode 100644 index 0000000..28b58a5 --- /dev/null +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridge.kt @@ -0,0 +1,36 @@ +package com.usercentrics.sdk.flutter.bridge + +import com.usercentrics.sdk.flutter.api.FlutterMethodCall +import com.usercentrics.sdk.flutter.api.FlutterResult +import com.usercentrics.sdk.flutter.api.UsercentricsProxy +import com.usercentrics.sdk.flutter.api.UsercentricsProxySingleton +import com.usercentrics.sdk.flutter.serializer.serialize + +internal class ClearUserSessionBridge( + private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton +) : MethodBridge { + + companion object { + private const val clearUserSessionErrorCode = + "usercentrics_flutter_clearUserSession_error" + } + + override val name: String + get() = "clearUserSession" + + override fun invoke(call: FlutterMethodCall, result: FlutterResult) { + assert(name == call.method) + usercentrics.instance.clearUserSession( + onSuccess = { + result.success(it.serialize()) + }, + onError = { + result.error( + clearUserSessionErrorCode, + it.message, + it + ) + }, + ) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/CMPDataSerializer.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/CMPDataSerializer.kt index f66d895..9fa89bf 100644 --- a/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/CMPDataSerializer.kt +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/CMPDataSerializer.kt @@ -258,8 +258,6 @@ private fun CustomizationColor.serialize(): Any { "toggleDisabledBackground" to toggleDisabledBackground, "toggleDisabledIcon" to toggleDisabledIcon, "secondLayerTab" to secondLayerTab, - "moreBtnBackground" to moreBtnBackground, - "moreBtnText" to moreBtnText, ) } diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridgeTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridgeTest.kt new file mode 100644 index 0000000..e802801 --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/ClearUserSessionBridgeTest.kt @@ -0,0 +1,79 @@ +package com.usercentrics.sdk.flutter.bridge + +import com.usercentrics.sdk.UsercentricsReadyStatus +import com.usercentrics.sdk.UsercentricsSDK +import com.usercentrics.sdk.errors.UsercentricsError +import com.usercentrics.sdk.flutter.api.FakeFlutterMethodCall +import com.usercentrics.sdk.flutter.api.FakeFlutterResult +import com.usercentrics.sdk.flutter.api.FakeUsercentricsProxy +import com.usercentrics.sdk.flutter.mock.ClearUserSessionMock +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class ClearUserSessionBridgeTest { + + @Test + fun testName() { + val instance = ClearUserSessionBridge(FakeUsercentricsProxy()) + assertEquals("clearUserSession", instance.name) + } + + @Test + fun testInvokeWithOtherName() { + val instance = ClearUserSessionBridge(FakeUsercentricsProxy()) + val call = FakeFlutterMethodCall(method = "otherName", arguments = null) + + assertThrows(AssertionError::class.java) { + instance.invoke(call, FakeFlutterResult()) + } + } + + @Test + fun testInvokeWithSuccess() { + val usercentricsSDK = mockk() + every { usercentricsSDK.clearUserSession(any(), any()) } + .answers() { + (arg(0) as (UsercentricsReadyStatus) -> Unit)(ClearUserSessionMock.fake) + } + val usercentricsProxy = FakeUsercentricsProxy(instanceAnswer = usercentricsSDK) + val instance = ClearUserSessionBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(ClearUserSessionMock.call, result) + + verify(exactly = 1) { usercentricsSDK.clearUserSession(any(), any()) } + + assertEquals(1, result.successCount) + assertEquals(ClearUserSessionMock.expected, result.successResultArgument) + } + + @Test + fun testInvokeWithError() { + val error = mockk(relaxed = true) + val errorMessage = "The error message" + every { error.message }.returns(errorMessage) + + val usercentricsSDK = mockk() + every { usercentricsSDK.clearUserSession(any(), any()) } + .answers() { + (arg(1) as (UsercentricsError) -> Unit)(error) + } + + val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) + val instance = ClearUserSessionBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(ClearUserSessionMock.call, result) + + verify(exactly = 1) { usercentricsSDK.clearUserSession(any(), any()) } + + assertEquals(1, result.errorCount) + assertEquals("usercentrics_flutter_clearUserSession_error", result.errorCodeArgument) + assertEquals(errorMessage, result.errorMessageArgument) + assertEquals(error, result.errorDetailsArgument) + } +} \ No newline at end of file diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/ClearUserSessionMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/ClearUserSessionMock.kt new file mode 100644 index 0000000..aad2f36 --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/ClearUserSessionMock.kt @@ -0,0 +1,63 @@ +package com.usercentrics.sdk.flutter.mock + +import com.usercentrics.sdk.GeolocationRuleset +import com.usercentrics.sdk.UsercentricsConsentHistoryEntry +import com.usercentrics.sdk.UsercentricsReadyStatus +import com.usercentrics.sdk.UsercentricsServiceConsent +import com.usercentrics.sdk.flutter.api.FakeFlutterMethodCall +import com.usercentrics.sdk.models.settings.UsercentricsConsentType +import com.usercentrics.sdk.v2.location.data.UsercentricsLocation + +internal object ClearUserSessionMock { + val fake = UsercentricsReadyStatus( + shouldCollectConsent = true, + consents = listOf( + UsercentricsServiceConsent( + templateId = "ocv9HNX_g", + status = false, + dataProcessor = "Facebook SDK", + type = UsercentricsConsentType.EXPLICIT, + version = "1.0.1", + isEssential = true, + history = listOf( + UsercentricsConsentHistoryEntry( + status = true, + type = UsercentricsConsentType.EXPLICIT, + timestampInMillis = 123, + ) + ) + ) + ), + geolocationRuleset = GeolocationRuleset(activeSettingsId = "settingsId", bannerRequiredAtLocation = true), + location = UsercentricsLocation(countryCode = "PT", regionCode = "PT11") + ) + + // From the debugger + val call = + FakeFlutterMethodCall( + method = "clearUserSession", + arguments = "" + ) + val expected = mapOf( + "shouldCollectConsent" to true, + "consents" to listOf( + mapOf( + "templateId" to "ocv9HNX_g", + "status" to false, + "type" to "EXPLICIT", + "version" to "1.0.1", + "dataProcessor" to "Facebook SDK", + "isEssential" to true, + "history" to listOf( + mapOf( + "status" to true, "timestampInMillis" to 123L, "type" to "EXPLICIT", + ) + ) + ) + ), + "geolocationRuleset" to mapOf("activeSettingsId" to "settingsId", "bannerRequiredAtLocation" to true), + "location" to mapOf( + "countryCode" to "PT", "regionCode" to "PT11", "isInEU" to true, "isInUS" to false, "isInCalifornia" to false + ) + ) +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetCMPDataMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetCMPDataMock.kt index 23577b8..c2c3269 100644 --- a/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetCMPDataMock.kt +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetCMPDataMock.kt @@ -522,9 +522,7 @@ internal object GetCMPDataMock { "toggleActiveIcon" to null, "toggleDisabledBackground" to null, "toggleDisabledIcon" to null, - "secondLayerTab" to null, - "moreBtnBackground" to null, - "moreBtnText" to null, + "secondLayerTab" to null ), "font" to mapOf( "family" to "BlinkMacSystemFont,-apple-system,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,Helvetica,Arial,sans-serif", diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3172a07..1af4fbc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,11 +1,11 @@ PODS: - Flutter (1.0.0) - - Usercentrics (2.13.0) - - usercentrics_sdk (2.13.0): + - Usercentrics (2.13.2) + - usercentrics_sdk (2.13.2): - Flutter - - UsercentricsUI (= 2.13.0) - - UsercentricsUI (2.13.0): - - Usercentrics (= 2.13.0) + - UsercentricsUI (= 2.13.2) + - UsercentricsUI (2.13.2): + - Usercentrics (= 2.13.2) - webview_flutter_wkwebview (0.0.1): - Flutter @@ -29,9 +29,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - Usercentrics: d434a8e69dc40584a35d884895b8b918aaf34317 - usercentrics_sdk: 854a8448c3f31a21f71aa59ccb8fcbaa16fc47a9 - UsercentricsUI: 2340d7a8c0c2bf6aee9a3bc90f245aef01d62409 + Usercentrics: 03f848172a7bc4ddf9e94f4e0809e1ba6025f9fb + usercentrics_sdk: 98f41a4241300e820948cec831af9814fc62ba8d + UsercentricsUI: 4e0d44300ae27be020974f2e2d2a8691c3819ad7 webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f PODFILE CHECKSUM: 723de1cf6e2f18b51eb3426c945e31134a750097 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 35ccaea..76747f4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 17F28FC4AC3BF66FC4301149 /* Pods_Runner_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 760C9E4CB26E7DAC6C729A22 /* Pods_Runner_RunnerTests.framework */; }; 3453DCAF2B3D971200EFE874 /* GetAdditionalConsentModeBridgeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3453DCAE2B3D971200EFE874 /* GetAdditionalConsentModeBridgeTest.swift */; }; + 34E8AE5B2B9B7375001242FE /* ClearUserSessionBridgeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E8AE5A2B9B7375001242FE /* ClearUserSessionBridgeTest.swift */; }; 3735A4C22A582462001666E4 /* TrackBridgeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3735A4C12A582462001666E4 /* TrackBridgeTest.swift */; }; 3735A4C42A582C54001666E4 /* UsercentricsAnalyticsEventTypeSerializerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3735A4C32A582C54001666E4 /* UsercentricsAnalyticsEventTypeSerializerTest.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -64,6 +65,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3453DCAE2B3D971200EFE874 /* GetAdditionalConsentModeBridgeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAdditionalConsentModeBridgeTest.swift; sourceTree = ""; }; + 34E8AE5A2B9B7375001242FE /* ClearUserSessionBridgeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearUserSessionBridgeTest.swift; sourceTree = ""; }; 3735A4C12A582462001666E4 /* TrackBridgeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackBridgeTest.swift; sourceTree = ""; }; 3735A4C32A582C54001666E4 /* UsercentricsAnalyticsEventTypeSerializerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsercentricsAnalyticsEventTypeSerializerTest.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -221,6 +223,7 @@ AD9067DF29439C86008B4204 /* GetABTestingVariantBridgeTest.swift */, AD9067E129439D5F008B4204 /* SetABTestingVariantBridgeTest.swift */, 3735A4C32A582C54001666E4 /* UsercentricsAnalyticsEventTypeSerializerTest.swift */, + 34E8AE5A2B9B7375001242FE /* ClearUserSessionBridgeTest.swift */, ); path = Bridge; sourceTree = ""; @@ -507,6 +510,7 @@ 3453DCAF2B3D971200EFE874 /* GetAdditionalConsentModeBridgeTest.swift in Sources */, A297F3FE2717097A00F7AF8A /* GetControllerIdBridgeTest.swift in Sources */, AD7CFFD427BA8FD300567D62 /* ShowSecondLayerBridgeTest.swift in Sources */, + 34E8AE5B2B9B7375001242FE /* ClearUserSessionBridgeTest.swift in Sources */, 3735A4C22A582462001666E4 /* TrackBridgeTest.swift in Sources */, A28883ED271824C4007E0C44 /* RestoreUserSessionBridgeTest.swift in Sources */, A298576C2716DE6A0062F7F8 /* FakeUsercentricsSDK.swift in Sources */, diff --git a/example/ios/RunnerTests/Bridge/ClearUserSessionBridgeTest.swift b/example/ios/RunnerTests/Bridge/ClearUserSessionBridgeTest.swift new file mode 100644 index 0000000..64e5210 --- /dev/null +++ b/example/ios/RunnerTests/Bridge/ClearUserSessionBridgeTest.swift @@ -0,0 +1,97 @@ +import XCTest + +@testable import Usercentrics +@testable import usercentrics_sdk + +class ClearUserSessionBridgeTest: XCTestCase, BaseBridgeTestProtocol { + + var bridgeName: String! = "clearUserSession" + var bridge: MethodBridge! + + private var usercentrics: FakeUsercentricsSDK! + + private let consent = UsercentricsServiceConsent( + templateId: "ocv9HNX_g", + status: true, + history: [], + type: .explicit_, + dataProcessor: "Facebook SDK", + version: "1.0.1", + isEssential: true + ) + + override func setUp() { + usercentrics = FakeUsercentricsSDK() + bridge = ClearUserSessionBridge(usercentrics: FakeUsercentricsProxy(shared: usercentrics)) + super.setUp() + } + + override func tearDown() { + usercentrics = nil + bridge = nil + } + + func testName() { + testNameProtocol() + } + + func testInvoke() { + usercentrics.clearUSSuccess = .init(shouldCollectConsent: true, + consents: [consent], + geolocationRuleset: GeolocationRuleset(activeSettingsId: "settingsId", bannerRequiredAtLocation: true), + location: UsercentricsLocation(countryCode: "PT", regionCode: "PT11")) + + let expectation = XCTestExpectation(description: "resultCompletion") + let resultCompletion: FlutterResult = { result in + guard + let result = result as? [String: Any], + let shouldCollectConsent = result["shouldCollectConsent"] as? Bool, + let consentsMap = result["consents"] as? [[String: Any]], + let consent = consentsMap.first + else { + XCTFail() + return + } + + XCTAssertEqual(shouldCollectConsent, true) + XCTAssertEqual(consentsMap.count, 1) + XCTAssertEqual(consent["version"] as! String, "1.0.1") + XCTAssertEqual(consent["dataProcessor"] as! String, "Facebook SDK") + XCTAssertEqual(consent["templateId"] as! String, "ocv9HNX_g") + let historyMap = consent["history"] as? [[String: Any]] + XCTAssertEqual(historyMap?.isEmpty, true) + XCTAssertEqual(consent["type"] as! String, "EXPLICIT") + XCTAssertEqual(consent["status"] as! Bool, true) + XCTAssertEqual(consent["isEssential"] as! Bool, true) + + expectation.fulfill() + } + + let method = FakeFlutterMethodCall(methodName: bridgeName) + method.argumentsMap = "abc" + + bridge.invoke(method, resultCompletion) + wait(for: [expectation], timeout: 2.0) + } + + + func testInvokeWithErrorOnUsercentrics() { + usercentrics.clearUSError = UsercentricsFakeError() + + let expectation = XCTestExpectation(description: "resultCompletion") + let resultCompletion: FlutterResult = { result in + guard let result = result as? FlutterError else { + XCTFail() + return + } + + XCTAssertEqual(result.code, "usercentrics_flutter_clearUserSession_error") + expectation.fulfill() + } + + let method = FakeFlutterMethodCall(methodName: bridgeName) + + bridge.invoke(method, resultCompletion) + wait(for: [expectation], timeout: 2.0) + } +} diff --git a/example/ios/RunnerTests/Fake/FakeUsercentricsSDK.swift b/example/ios/RunnerTests/Fake/FakeUsercentricsSDK.swift index d0541ce..a391c1a 100644 --- a/example/ios/RunnerTests/Fake/FakeUsercentricsSDK.swift +++ b/example/ios/RunnerTests/Fake/FakeUsercentricsSDK.swift @@ -69,4 +69,20 @@ final class FakeUsercentricsSDK: UsercentricsSDK { override func track(event: UsercentricsAnalyticsEventType) { trackCalls.append(event) } + + var clearUSError: Error? + var clearUSSuccess: UsercentricsReadyStatus? + + override func clearUserSession(onSuccess: @escaping (UsercentricsReadyStatus) -> Void, onError: @escaping (Error) -> Void) { + + if let clearUSError = clearUSError { + onError(clearUSError) + return + } + + if let restoreUSSuccess = clearUSSuccess { + onSuccess(clearUSSuccess!) + return + } + } } diff --git a/example/test/fake_usercentrics.dart b/example/test/fake_usercentrics.dart index 0ff1d12..dbdd3ed 100644 --- a/example/test/fake_usercentrics.dart +++ b/example/test/fake_usercentrics.dart @@ -154,4 +154,9 @@ class FakeUsercentrics extends UsercentricsPlatform { Future get additionalConsentModeData { throw UnimplementedError(); } + + @override + Future clearUserSession() { + throw UnimplementedError(); + } } diff --git a/ios/Classes/Bridge/ClearUserSessionBridge.swift b/ios/Classes/Bridge/ClearUserSessionBridge.swift new file mode 100644 index 0000000..0e2232a --- /dev/null +++ b/ios/Classes/Bridge/ClearUserSessionBridge.swift @@ -0,0 +1,19 @@ +import Foundation + +struct ClearUserSessionBridge : MethodBridge { + + let name: String = "clearUserSession" + let usercentrics: UsercentricsProxyProtocol + + func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + assert(call.method == name) + + usercentrics.shared.clearUserSession { status in + result(status.serialize()) + } onError: { error in + result(FlutterError(code: "usercentrics_flutter_clearUserSession_error", + message: error.localizedDescription, + details: nil )) + } + } +} diff --git a/ios/Classes/Serializer/CMPDataSerializer.swift b/ios/Classes/Serializer/CMPDataSerializer.swift index 0b627ee..e4060b2 100644 --- a/ios/Classes/Serializer/CMPDataSerializer.swift +++ b/ios/Classes/Serializer/CMPDataSerializer.swift @@ -262,8 +262,6 @@ extension CustomizationColor { "toggleDisabledBackground" : self.toggleDisabledBackground, "toggleDisabledIcon" : self.toggleDisabledIcon, "secondLayerTab" : self.secondLayerTab, - "moreBtnBackground" : self.moreBtnBackground, - "moreBtnText" : self.moreBtnText, ] } } diff --git a/ios/Classes/SwiftUsercentricsPlugin.swift b/ios/Classes/SwiftUsercentricsPlugin.swift index 06baa00..0f7710a 100644 --- a/ios/Classes/SwiftUsercentricsPlugin.swift +++ b/ios/Classes/SwiftUsercentricsPlugin.swift @@ -44,6 +44,7 @@ public class SwiftUsercentricsPlugin: NSObject, FlutterPlugin { SetABTestingVariantBridge(usercentrics: usercentrics), GetABTestingVariantBridge(usercentrics: usercentrics), GetAdditionalConsentModeBridge(usercentrics: usercentrics), + ClearUserSessionBridge(usercentrics: usercentrics) ] return bridges.reduce([String : MethodBridge]()) { dict, value in var dict = dict diff --git a/ios/usercentrics_sdk.podspec b/ios/usercentrics_sdk.podspec index 208b3bc..cf9a800 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.13.0' + s.version = '2.13.2' s.summary = 'Usercentrics Flutter SDK.' s.description = <<-DESC Usercentrics Flutter SDK. diff --git a/lib/src/internal/bridge/bridge.dart b/lib/src/internal/bridge/bridge.dart index 944769e..795aa4c 100644 --- a/lib/src/internal/bridge/bridge.dart +++ b/lib/src/internal/bridge/bridge.dart @@ -23,3 +23,4 @@ export 'set_cmp_id_bridge.dart'; export 'show_first_layer_bridge.dart'; export 'show_second_layer_bridge.dart'; export 'track_bridge.dart'; +export 'clear_user_session_bridge.dart'; diff --git a/lib/src/internal/bridge/clear_user_session_bridge.dart b/lib/src/internal/bridge/clear_user_session_bridge.dart new file mode 100644 index 0000000..ffd9272 --- /dev/null +++ b/lib/src/internal/bridge/clear_user_session_bridge.dart @@ -0,0 +1,22 @@ +import 'package:flutter/services.dart'; +import 'package:usercentrics_sdk/src/internal/serializer/ready_status_serializer.dart'; +import 'package:usercentrics_sdk/src/model/ready_status.dart'; + +abstract class ClearUserSessionBridge { + const ClearUserSessionBridge(); + + Future invoke({required MethodChannel channel}); +} + +class MethodChannelClearUserSession extends ClearUserSessionBridge { + const MethodChannelClearUserSession(); + + static const String _name = 'clearUserSession'; + + @override + Future invoke( + {required MethodChannel channel}) async { + final result = await channel.invokeMethod(_name); + return ReadyStatusSerializer.deserialize(result); + } +} diff --git a/lib/src/internal/platform/method_channel_usercentrics.dart b/lib/src/internal/platform/method_channel_usercentrics.dart index 12f22b9..07f6253 100644 --- a/lib/src/internal/platform/method_channel_usercentrics.dart +++ b/lib/src/internal/platform/method_channel_usercentrics.dart @@ -33,7 +33,8 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { this.setABTestingVariantBridge = const MethodChannelSetABTestingVariant(), this.trackBridge = const MethodChannelTrack(), this.getAdditionalConsentModeData = - const MethodChannelGetAdditionalConsentModeData()}); + const MethodChannelGetAdditionalConsentModeData(), + this.clearUserSessionBridge = const MethodChannelClearUserSession()}); static const MethodChannel _channel = MethodChannel('usercentrics'); @@ -62,6 +63,7 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { final SetABTestingVariantBridge setABTestingVariantBridge; final TrackBridge trackBridge; final GetAdditionalConsentModeDataBridge getAdditionalConsentModeData; + final ClearUserSessionBridge clearUserSessionBridge; @visibleForTesting Completer? isReadyCompleter; @@ -327,4 +329,10 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { await _ensureIsReady(); return await getAdditionalConsentModeData.invoke(channel: _channel); } + + @override + Future clearUserSession() async { + await _ensureIsReady(); + return await clearUserSessionBridge.invoke(channel: _channel); + } } diff --git a/lib/src/internal/serializer/customization_serializer.dart b/lib/src/internal/serializer/customization_serializer.dart index 1a55372..e68c028 100644 --- a/lib/src/internal/serializer/customization_serializer.dart +++ b/lib/src/internal/serializer/customization_serializer.dart @@ -44,8 +44,6 @@ class CustomizationColorSerializer { toggleDisabledBackground: value['toggleDisabledBackground'] ?? "", toggleDisabledIcon: value['toggleDisabledIcon'] ?? "", secondLayerTab: value['secondLayerTab'] ?? "", - moreBtnBackground: value['moreBtnBackground'] ?? "", - moreBtnText: value['moreBtnText'] ?? "", ); } } diff --git a/lib/src/model/customization.dart b/lib/src/model/customization.dart index d2a186e..96dda15 100644 --- a/lib/src/model/customization.dart +++ b/lib/src/model/customization.dart @@ -85,8 +85,6 @@ class CustomizationColor { required this.toggleDisabledBackground, required this.toggleDisabledIcon, required this.secondLayerTab, - required this.moreBtnBackground, - required this.moreBtnText, }); final String primary; @@ -108,8 +106,6 @@ class CustomizationColor { final String toggleDisabledBackground; final String toggleDisabledIcon; final String secondLayerTab; - final String moreBtnBackground; - final String moreBtnText; @override bool operator ==(Object other) => @@ -134,9 +130,7 @@ class CustomizationColor { toggleActiveIcon == other.toggleActiveIcon && toggleDisabledBackground == other.toggleDisabledBackground && toggleDisabledIcon == other.toggleDisabledIcon && - secondLayerTab == other.secondLayerTab && - moreBtnBackground == other.moreBtnBackground && - moreBtnText == other.moreBtnText; + secondLayerTab == other.secondLayerTab; @override int get hashCode => @@ -158,9 +152,7 @@ class CustomizationColor { toggleActiveIcon.hashCode + toggleDisabledBackground.hashCode + toggleDisabledIcon.hashCode + - secondLayerTab.hashCode + - moreBtnBackground.hashCode + - moreBtnText.hashCode; + secondLayerTab.hashCode; @override String toString() => "$CustomizationColor($hashCode)"; diff --git a/lib/src/platform/usercentrics_platform.dart b/lib/src/platform/usercentrics_platform.dart index f457653..d4f03bd 100644 --- a/lib/src/platform/usercentrics_platform.dart +++ b/lib/src/platform/usercentrics_platform.dart @@ -99,4 +99,6 @@ abstract class UsercentricsPlatform { Future track({ required UsercentricsAnalyticsEventType event, }); + + Future clearUserSession(); } diff --git a/lib/src/usercentrics.dart b/lib/src/usercentrics.dart index f011b38..66c9bcf 100644 --- a/lib/src/usercentrics.dart +++ b/lib/src/usercentrics.dart @@ -207,4 +207,8 @@ class Usercentrics { required UsercentricsAnalyticsEventType event, }) => _delegate.track(event: event); + + /// Clears the user session avoiding the sdk initialization. + static Future clearUserSession() => + _delegate.clearUserSession(); } diff --git a/pubspec.yaml b/pubspec.yaml index 85b8d95..7e2dac8 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.13.1 +version: 2.13.2 environment: sdk: ">=2.17.1 <4.0.0" diff --git a/test/internal/bridge/clear_user_session_bridge_test.dart b/test/internal/bridge/clear_user_session_bridge_test.dart new file mode 100644 index 0000000..8e5f3d5 --- /dev/null +++ b/test/internal/bridge/clear_user_session_bridge_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:usercentrics_sdk/src/internal/internal.dart'; +import 'package:usercentrics_sdk/src/model/model.dart'; + +void main() { + // Data from the debugger + const mockResponse = { + "shouldCollectConsent": true, + "consents": [ + { + "templateId": "SJKM9Ns_ibQ", + "status": true, + "type": "EXPLICIT", + "version": "10.4.5", + "dataProcessor": "Facebook Connect", + "isEssential": true, + "history": [ + { + "status": true, + "timestampInMillis": 123, + "type": "EXPLICIT", + } + ] + }, + ], + "geolocationRuleset": { + "activeSettingsId": "settingsId", + "bannerRequiredAtLocation": true + }, + "location": { + "countryCode": "PT", + "regionCode": "PT11", + "isInEU": true, + "isInUS": false, + "isInCalifornia": false + } + }; + + const expectedResult = UsercentricsReadyStatus( + shouldCollectConsent: true, + consents: [ + UsercentricsServiceConsent( + templateId: "SJKM9Ns_ibQ", + status: true, + dataProcessor: "Facebook Connect", + version: "10.4.5", + type: UsercentricsConsentType.explicit, + isEssential: true, + history: [ + UsercentricsConsentHistoryEntry( + status: true, + timestampInMillis: 123, + type: UsercentricsConsentType.explicit, + ) + ], + ), + ], + geolocationRuleset: GeolocationRuleset( + activeSettingsId: "settingsId", bannerRequiredAtLocation: true), + location: UsercentricsLocation( + countryCode: "PT", + regionCode: "PT11", + isInEU: true, + isInUS: false, + isInCalifornia: false)); + + const MethodChannel channel = MethodChannel('usercentrics'); + TestWidgetsFlutterBinding.ensureInitialized(); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('invoke', () async { + int callCounter = 0; + MethodCall? receivedCall; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return mockResponse; + }); + const instance = MethodChannelClearUserSession(); + + final result = await instance.invoke(channel: channel); + + expect(callCounter, 1); + expect(receivedCall?.method, 'clearUserSession'); + expect(result, expectedResult); + }); +} diff --git a/test/internal/bridge/fake_clear_user_session_bridge.dart b/test/internal/bridge/fake_clear_user_session_bridge.dart new file mode 100644 index 0000000..83c6ea9 --- /dev/null +++ b/test/internal/bridge/fake_clear_user_session_bridge.dart @@ -0,0 +1,20 @@ +import 'package:flutter/src/services/platform_channel.dart'; +import 'package:usercentrics_sdk/src/internal/bridge/clear_user_session_bridge.dart'; +import 'package:usercentrics_sdk/src/model/ready_status.dart'; + +class FakeClearUserSessionBridge extends ClearUserSessionBridge { + FakeClearUserSessionBridge({ + this.invokeAnswer, + }); + + final UsercentricsReadyStatus? invokeAnswer; + var invokeCount = 0; + MethodChannel? invokeChannelArgument; + + @override + Future invoke({required MethodChannel channel}) { + invokeCount++; + invokeChannelArgument = channel; + return Future.value(invokeAnswer!); + } +} diff --git a/test/internal/bridge/get_cmp_data_bridge_test.mock.dart b/test/internal/bridge/get_cmp_data_bridge_test.mock.dart index 84f588b..24dee78 100644 --- a/test/internal/bridge/get_cmp_data_bridge_test.mock.dart +++ b/test/internal/bridge/get_cmp_data_bridge_test.mock.dart @@ -212,8 +212,6 @@ const _responseCustomization = { "toggleDisabledBackground": null, "toggleDisabledIcon": null, "secondLayerTab": null, - "moreBtnBackground": null, - "moreBtnText": null, }, "font": { "family": @@ -615,8 +613,6 @@ const _expectedCustomization = UsercentricsCustomization( toggleDisabledBackground: "", toggleDisabledIcon: "", secondLayerTab: "", - moreBtnBackground: "", - moreBtnText: "", ), font: CustomizationFont( family: diff --git a/test/internal/platform/method_channel_usercentrics_test.dart b/test/internal/platform/method_channel_usercentrics_test.dart index 526c015..ab89382 100644 --- a/test/internal/platform/method_channel_usercentrics_test.dart +++ b/test/internal/platform/method_channel_usercentrics_test.dart @@ -11,6 +11,7 @@ import '../bridge/fake_initialize_bridge.dart'; import '../bridge/fake_is_ready_bridge.dart'; import '../bridge/fake_reset_bridge.dart'; import '../bridge/fake_restore_user_session_bridge.dart'; +import '../bridge/fake_clear_user_session_bridge.dart'; import '../bridge/fake_show_first_layer_bridge.dart'; import '../bridge/fake_show_second_layer_bridge.dart'; import '../bridge/get_cmp_data_bridge_test.dart'; @@ -419,4 +420,53 @@ void main() { ); }); }); + + group('clearUserSession', () { + test('default', () { + final instance = MethodChannelUsercentrics(); + expect(instance.clearUserSessionBridge, + const TypeMatcher()); + }); + + test('success', () async { + const expectedStatus = UsercentricsReadyStatus( + shouldCollectConsent: true, + consents: [], + geolocationRuleset: null, + location: UsercentricsLocation( + countryCode: "PT", + regionCode: "PT11", + isInEU: true, + isInUS: false, + isInCalifornia: false)); + + final clearUserSessionBridge = FakeClearUserSessionBridge( + invokeAnswer: expectedStatus, + ); + + final instance = MethodChannelUsercentrics( + clearUserSessionBridge: clearUserSessionBridge, + ); + + instance.isReadyCompleter = Completer(); + instance.isReadyCompleter?.complete(); + + final response = await instance.clearUserSession(); + + expect(clearUserSessionBridge.invokeCount, 1); + expect( + clearUserSessionBridge.invokeChannelArgument?.name, "usercentrics"); + expect(response, expectedStatus); + }); + + test('when it is not ready', () { + final instance = MethodChannelUsercentrics(); + instance.isReadyCompleter = null; + + expect( + () => instance.clearUserSession(), + throwsA(const TypeMatcher()), + ); + }); + }); } diff --git a/test/platform/fake_usercentrics_platform.dart b/test/platform/fake_usercentrics_platform.dart index c99e064..6bc82ad 100644 --- a/test/platform/fake_usercentrics_platform.dart +++ b/test/platform/fake_usercentrics_platform.dart @@ -35,7 +35,8 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { this.tcfDataAnswer, this.userSessionDataAnswer, this.aBTestingVariantAnswer, - this.acmDataAnswer}); + this.acmDataAnswer, + this.clearUserSessionAnswer}); final List? consentsAnswer; var consentsCount = 0; @@ -348,4 +349,13 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { acmDataCount++; return Future.value(acmDataAnswer!); } + + final UsercentricsReadyStatus? clearUserSessionAnswer; + var clearUserSessionCount = 0; + + @override + Future clearUserSession() { + clearUserSessionCount++; + return Future.value(clearUserSessionAnswer!); + } } diff --git a/test/usercentrics_test.dart b/test/usercentrics_test.dart index dfb377f..1d7fa86 100644 --- a/test/usercentrics_test.dart +++ b/test/usercentrics_test.dart @@ -210,4 +210,28 @@ void main() { expect(delegate.trackCalls[0], UsercentricsAnalyticsEventType.acceptAllFirstLayer); }); + + test('clearUserSession', () async { + const expectedStatus = UsercentricsReadyStatus( + shouldCollectConsent: true, + consents: [], + geolocationRuleset: GeolocationRuleset( + activeSettingsId: "settingsId", bannerRequiredAtLocation: true), + location: UsercentricsLocation( + countryCode: "PT", + regionCode: "PT11", + isInEU: true, + isInUS: false, + isInCalifornia: false)); + + final delegate = + FakeUsercentricsPlatform(clearUserSessionAnswer: expectedStatus); + + Usercentrics.delegatePackingProperty = delegate; + + final result = await Usercentrics.clearUserSession(); + + expect(delegate.clearUserSessionCount, 1); + expect(result, expectedStatus); + }); }