From f5275b9a1aeccfcccb62bf273cdec254c89f8c96 Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Tue, 25 Aug 2026 11:23:58 +0100 Subject: [PATCH 1/2] feat(MSDK-4525): add controllerId to Flutter bridge options Adds an optional controllerId String parameter to Usercentrics.initialize(), mirroring the native SDK's UsercentricsOptions.controllerId (MSDK-4160/4525). Lets Advanced-tier customers inject a previously-issued controllerID at SDK init to preserve identity across login/logout flows that clear local consent storage. Threaded through the full Dart call chain: - lib/src/usercentrics.dart (public API + doc comment) - lib/src/platform/usercentrics_platform.dart (abstract platform interface) - lib/src/internal/platform/method_channel_usercentrics.dart - lib/src/internal/bridge/initialize_bridge.dart - lib/src/internal/serializer/initialize_options_serializer.dart (Map key) And into the native bridges: - android/src/main/kotlin/.../serializer/OptionsSerializer.kt - forwards "controllerId" into UsercentricsOptions.controllerId - ios/usercentrics_sdk/.../Serializer/InitializeOptionsSerializer.swift - forwards "controllerId" into UsercentricsOptions.controllerId Extended existing tests (fakes + assertions) for the Dart layer and the Android bridge unit test to cover the new field. No Dart model class or build_runner codegen exists for options - they are passed as named parameters and serialized to a plain Map, so no .g.dart regeneration was needed. Co-Authored-By: Claude Sonnet 5 --- .../sdk/flutter/serializer/OptionsSerializer.kt | 3 +++ .../sdk/flutter/bridge/InitializeBridgeUnitTest.kt | 8 ++++++++ .../usercentrics/sdk/flutter/mock/InitializeMock.kt | 2 ++ .../Serializer/InitializeOptionsSerializer.swift | 4 ++++ lib/src/internal/bridge/initialize_bridge.dart | 3 +++ .../internal/platform/method_channel_usercentrics.dart | 2 ++ .../serializer/initialize_options_serializer.dart | 2 ++ lib/src/platform/usercentrics_platform.dart | 1 + lib/src/usercentrics.dart | 10 ++++++++++ test/internal/bridge/fake_initialize_bridge.dart | 3 +++ test/internal/bridge/initialize_bridge_test.dart | 5 +++++ .../platform/method_channel_usercentrics_test.dart | 6 +++++- test/platform/fake_usercentrics_platform.dart | 3 +++ 13 files changed, 51 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/OptionsSerializer.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/OptionsSerializer.kt index 28876ce1..c7a1f7bd 100644 --- a/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/OptionsSerializer.kt +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/OptionsSerializer.kt @@ -35,6 +35,9 @@ internal fun Any?.deserializeOptions(): UsercentricsOptions { this["initTimeoutMillis"]?.let { options.initTimeoutMillis = (it as Int).toLong() } + this["controllerId"]?.let { + options.controllerId = it as String + } this["bannerCustomization"]?.let { options.bannerCustomization = it.deserializeBannerInitCustomization() } diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/InitializeBridgeUnitTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/InitializeBridgeUnitTest.kt index d17dee93..70a64f64 100644 --- a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/InitializeBridgeUnitTest.kt +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/InitializeBridgeUnitTest.kt @@ -81,6 +81,10 @@ class InitializeBridgeUnitTest { InitializeMock.callWithDataInitTimeoutMillis, usercentricsProxy.initializeOptionsArgument?.initTimeoutMillis ) + assertEquals( + InitializeMock.callWithDataControllerId, + usercentricsProxy.initializeOptionsArgument?.controllerId + ) assertEquals(1, result.successCount) assertEquals(InitializeMock.expected, result.successResultArgument) } @@ -118,6 +122,10 @@ class InitializeBridgeUnitTest { defaultOptions.networkMode, usercentricsProxy.initializeOptionsArgument?.networkMode ) + assertEquals( + defaultOptions.controllerId, + usercentricsProxy.initializeOptionsArgument?.controllerId + ) assertEquals(1, result.successCount) assertEquals(InitializeMock.expected, result.successResultArgument) } diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/InitializeMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/InitializeMock.kt index 0341c0d8..31c05ea3 100644 --- a/android/src/test/java/com/usercentrics/sdk/flutter/mock/InitializeMock.kt +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/InitializeMock.kt @@ -18,6 +18,7 @@ internal object InitializeMock { "version" to "1.2.3", "networkMode" to "EU", "initTimeoutMillis" to 1500, + "controllerId" to "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) ) val callWithDataSettingsId = "AAAAA" @@ -28,6 +29,7 @@ internal object InitializeMock { val callWithDataVersion = "1.2.3" val callWithDataNetworkMode = NetworkMode.EU val callWithDataInitTimeoutMillis = 1500L + val callWithDataControllerId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" val callWithoutData = FakeFlutterMethodCall( method = "initialize", arguments = mapOf( diff --git a/ios/usercentrics_sdk/Sources/usercentrics_sdk/Serializer/InitializeOptionsSerializer.swift b/ios/usercentrics_sdk/Sources/usercentrics_sdk/Serializer/InitializeOptionsSerializer.swift index cd73d307..acd11594 100644 --- a/ios/usercentrics_sdk/Sources/usercentrics_sdk/Serializer/InitializeOptionsSerializer.swift +++ b/ios/usercentrics_sdk/Sources/usercentrics_sdk/Serializer/InitializeOptionsSerializer.swift @@ -44,6 +44,10 @@ extension UsercentricsOptions { options.initTimeoutMillis = Int64(initTimeoutMillis) } + if let controllerId = dict["controllerId"] as? String { + options.controllerId = controllerId + } + if let consentMediation = dict["consentMediation"] as? Bool { options.consentMediation = consentMediation } diff --git a/lib/src/internal/bridge/initialize_bridge.dart b/lib/src/internal/bridge/initialize_bridge.dart index d713eb2c..f5f22b97 100644 --- a/lib/src/internal/bridge/initialize_bridge.dart +++ b/lib/src/internal/bridge/initialize_bridge.dart @@ -18,6 +18,7 @@ abstract class InitializeBridge { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }); @@ -40,6 +41,7 @@ class MethodChannelInitialize extends InitializeBridge { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }) { @@ -53,6 +55,7 @@ class MethodChannelInitialize extends InitializeBridge { networkMode: networkMode, consentMediation: consentMediation, initTimeoutMillis: initTimeoutMillis, + controllerId: controllerId, bannerCustomization: bannerCustomization, ); channel.invokeMethod(_name, arguments); diff --git a/lib/src/internal/platform/method_channel_usercentrics.dart b/lib/src/internal/platform/method_channel_usercentrics.dart index 944dfd18..360ea609 100644 --- a/lib/src/internal/platform/method_channel_usercentrics.dart +++ b/lib/src/internal/platform/method_channel_usercentrics.dart @@ -88,6 +88,7 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }) async { @@ -116,6 +117,7 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { networkMode: networkMode, consentMediation: consentMediation, initTimeoutMillis: initTimeoutMillis, + controllerId: controllerId, bannerCustomization: bannerCustomization); await status.then((value) => isReadyCompleter?.complete(null)); diff --git a/lib/src/internal/serializer/initialize_options_serializer.dart b/lib/src/internal/serializer/initialize_options_serializer.dart index dc84260a..66284225 100644 --- a/lib/src/internal/serializer/initialize_options_serializer.dart +++ b/lib/src/internal/serializer/initialize_options_serializer.dart @@ -17,6 +17,7 @@ class InitializeOptionsSerializer { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }) => @@ -30,6 +31,7 @@ class InitializeOptionsSerializer { 'networkMode': NetworkModeSerializer.serialize(networkMode), 'consentMediation': consentMediation, 'initTimeoutMillis': initTimeoutMillis, + 'controllerId': controllerId, 'bannerCustomization': BannerInitCustomizationSerializer.serialize( bannerCustomization, ), diff --git a/lib/src/platform/usercentrics_platform.dart b/lib/src/platform/usercentrics_platform.dart index ef558369..60d12fa1 100644 --- a/lib/src/platform/usercentrics_platform.dart +++ b/lib/src/platform/usercentrics_platform.dart @@ -16,6 +16,7 @@ abstract class UsercentricsPlatform { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }); diff --git a/lib/src/usercentrics.dart b/lib/src/usercentrics.dart index 05668819..2e378847 100644 --- a/lib/src/usercentrics.dart +++ b/lib/src/usercentrics.dart @@ -27,6 +27,14 @@ class Usercentrics { /// - The [networkMode] sets the network operation mode. Be careful, use this option only if we have confirmed that it is ready to use because it has a significant impact on the whole system's performance. The default value is "world". /// - The [consentMediation] enables the feature of consent mediation, this feature works with a set of selected SDKs that you can find in the documentation. /// - The [initTimeoutMillis] sets a timeout for the SDK to initialize. Minimum value is 5000ms. The default value is 10000ms. + /// - The [controllerId] can be used to inject a previously-issued controllerID at SDK + /// initialisation. Use this to preserve user identity across login/logout flows that clear + /// local consent storage. Store the controllerId server-side after the first successful init, + /// then pass it here on every subsequent login. The SDK will skip generating a new ID and use + /// this value instead. It is persisted to local storage, so subsequent re-initialisations + /// without this option continue to use it. The value must be a 64-character lowercase + /// hexadecimal string (the format produced internally by the SDK). Invalid values are ignored + /// with a warning log and the SDK falls back to its normal ID resolution (stored → generated). /// - The [bannerCustomization] enables further programmatic customization of the predefined Banner UI, applied at init-time. /// **Deprecated:** [bannerCustomization] is deprecated and will be removed in a future release. Configure banner appearance via the Usercentrics dashboard instead. static void initialize({ @@ -39,6 +47,7 @@ class Usercentrics { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, @Deprecated( 'BannerInitCustomization is deprecated and will be removed in a future release. Configure banner appearance via the Usercentrics dashboard instead.') BannerInitCustomization? bannerCustomization, @@ -53,6 +62,7 @@ class Usercentrics { networkMode: networkMode, consentMediation: consentMediation, initTimeoutMillis: initTimeoutMillis, + controllerId: controllerId, // ignore: deprecated_member_use_from_same_package bannerCustomization: bannerCustomization); diff --git a/test/internal/bridge/fake_initialize_bridge.dart b/test/internal/bridge/fake_initialize_bridge.dart index 5767ed09..3f77ec9b 100644 --- a/test/internal/bridge/fake_initialize_bridge.dart +++ b/test/internal/bridge/fake_initialize_bridge.dart @@ -16,6 +16,7 @@ class FakeInitializeBridge extends InitializeBridge { NetworkMode? invokeNetworkModeArgument; bool? invokeConsentMediationArgument; int? invokeInitTimeoutMillisArgument; + String? invokeControllerIdArgument; // ignore: deprecated_member_use_from_same_package BannerInitCustomization? invokeBannerCustomizationArgument; @@ -31,6 +32,7 @@ class FakeInitializeBridge extends InitializeBridge { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }) { @@ -45,6 +47,7 @@ class FakeInitializeBridge extends InitializeBridge { invokeNetworkModeArgument = networkMode; invokeConsentMediationArgument = consentMediation; invokeInitTimeoutMillisArgument = initTimeoutMillis; + invokeControllerIdArgument = controllerId; invokeBannerCustomizationArgument = bannerCustomization; } } diff --git a/test/internal/bridge/initialize_bridge_test.dart b/test/internal/bridge/initialize_bridge_test.dart index 321d33ef..3cdb55e3 100644 --- a/test/internal/bridge/initialize_bridge_test.dart +++ b/test/internal/bridge/initialize_bridge_test.dart @@ -18,6 +18,8 @@ void main() { "networkMode": "EU", 'consentMediation': true, "initTimeoutMillis": 25000, + "controllerId": + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bannerCustomization": { 'paddingTop': 16, 'paddingBottom': null, @@ -55,6 +57,8 @@ void main() { const mockDefaultLanguage = "en"; const mockTimeoutMillis = 20000; const mockInitTimeoutMillis = 25000; + const mockControllerId = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const mockNetworkMode = NetworkMode.eu; const mockConsentMediation = true; // ignore: deprecated_member_use_from_same_package @@ -94,6 +98,7 @@ void main() { networkMode: mockNetworkMode, consentMediation: mockConsentMediation, initTimeoutMillis: mockInitTimeoutMillis, + controllerId: mockControllerId, // ignore: deprecated_member_use bannerCustomization: mockBannerCustomization); diff --git a/test/internal/platform/method_channel_usercentrics_test.dart b/test/internal/platform/method_channel_usercentrics_test.dart index 4c1177a2..3ade104d 100644 --- a/test/internal/platform/method_channel_usercentrics_test.dart +++ b/test/internal/platform/method_channel_usercentrics_test.dart @@ -51,6 +51,8 @@ void main() { const networkMode = NetworkMode.eu; const consentMediation = true; const initTimeoutMillis = 7000; + const controllerId = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; instance.initialize( settingsId: settingsId, @@ -61,7 +63,8 @@ void main() { version: version, networkMode: networkMode, consentMediation: consentMediation, - initTimeoutMillis: initTimeoutMillis); + initTimeoutMillis: initTimeoutMillis, + controllerId: controllerId); await instance.isReadyCompleter?.future; expect(initializeBridge.invokeCount, 1); @@ -76,6 +79,7 @@ void main() { expect(initializeBridge.invokeConsentMediationArgument, consentMediation); expect( initializeBridge.invokeInitTimeoutMillisArgument, initTimeoutMillis); + expect(initializeBridge.invokeControllerIdArgument, controllerId); expect(isReadyBridge.invokeCount, 1); expect(isReadyBridge.invokeChannelArgument?.name, "usercentrics"); diff --git a/test/platform/fake_usercentrics_platform.dart b/test/platform/fake_usercentrics_platform.dart index 7bdcc3ba..3568cffb 100644 --- a/test/platform/fake_usercentrics_platform.dart +++ b/test/platform/fake_usercentrics_platform.dart @@ -71,6 +71,7 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { NetworkMode? networkModeArgument; bool? consentMediationArgument; int? initializeInitTimeoutMillisArgument; + String? initializeControllerIdArgument; // ignore: deprecated_member_use_from_same_package BannerInitCustomization? initializeBannerCustomizationArgument; @@ -85,6 +86,7 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use_from_same_package BannerInitCustomization? bannerCustomization, }) { @@ -98,6 +100,7 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { networkModeArgument = networkMode; consentMediationArgument = consentMediation; initializeInitTimeoutMillisArgument = initTimeoutMillis; + initializeControllerIdArgument = controllerId; initializeBannerCustomizationArgument = bannerCustomization; } From 7efe967365a0b537507ff72bbb45aa070efda65b Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Tue, 25 Aug 2026 11:45:47 +0100 Subject: [PATCH 2/2] fix(MSDK-4525): add missing controllerId param to example FakeUsercentrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI Lint job was failing: FakeUsercentrics (example/test/fake_usercentrics.dart) also implements UsercentricsPlatform and was missed when controllerId was added to the interface — flutter analyze flagged it as invalid_override. Co-Authored-By: Claude Sonnet 5 --- example/test/fake_usercentrics.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/test/fake_usercentrics.dart b/example/test/fake_usercentrics.dart index e63bd3a3..1f9bab27 100644 --- a/example/test/fake_usercentrics.dart +++ b/example/test/fake_usercentrics.dart @@ -18,6 +18,7 @@ class FakeUsercentrics extends UsercentricsPlatform { NetworkMode? networkMode, bool? consentMediation, int? initTimeoutMillis, + String? controllerId, // ignore: deprecated_member_use BannerInitCustomization? bannerCustomization, }) {