From 4112cca9d0914b3c4d93a93a84d3c22c58778f57 Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Tue, 10 Mar 2026 18:15:10 +0000 Subject: [PATCH 1/2] [MSDK-3287] GPP implementation Made-with: Cursor --- .../sdk/flutter/UsercentricsPlugin.kt | 29 +++- .../sdk/flutter/bridge/GetGPPDataBridge.kt | 20 +++ .../sdk/flutter/bridge/GetGPPStringBridge.kt | 19 +++ .../sdk/flutter/bridge/SetGPPConsentBridge.kt | 30 ++++ .../flutter/serializer/CMPDataSerializer.kt | 1 - .../flutter/serializer/GppDataSerializer.kt | 30 ++++ .../flutter/bridge/GetGPPDataBridgeTest.kt | 48 ++++++ .../flutter/bridge/GetGPPStringBridgeTest.kt | 48 ++++++ .../flutter/bridge/SetGPPConsentBridgeTest.kt | 55 ++++++ .../sdk/flutter/mock/GetCMPDataMock.kt | 2 - .../sdk/flutter/mock/GetGPPDataMock.kt | 32 ++++ .../sdk/flutter/mock/GetGPPStringMock.kt | 11 ++ .../sdk/flutter/mock/SetGPPConsentMock.kt | 19 +++ example/lib/gpp_testing.dart | 159 ++++++++++++++++++ example/lib/main.dart | 9 + example/pubspec.lock | 20 +-- example/test/fake_usercentrics.dart | 19 +++ ios/Classes/Bridge/GetGPPDataBridge.swift | 12 ++ ios/Classes/Bridge/GetGPPStringBridge.swift | 12 ++ .../GppSectionChangeStreamHandler.swift | 20 +++ ios/Classes/Bridge/SetGPPConsentBridge.swift | 17 ++ .../Serializer/GppDataSerializer.swift | 19 +++ ios/Classes/SwiftUsercentricsPlugin.swift | 12 +- lib/src/internal/bridge/bridge.dart | 3 + .../internal/bridge/get_gpp_data_bridge.dart | 25 +++ .../bridge/get_gpp_string_bridge.dart | 23 +++ .../bridge/set_gpp_consent_bridge.dart | 35 ++++ .../platform/method_channel_usercentrics.dart | 45 ++++- .../serializer/gpp_data_serializer.dart | 27 +++ lib/src/model/gpp_data.dart | 64 +++++++ lib/src/model/gpp_section_change_payload.dart | 22 +++ lib/src/model/model.dart | 2 + lib/src/platform/usercentrics_platform.dart | 12 ++ lib/src/usercentrics.dart | 22 +++ pubspec.lock | 62 +++---- .../bridge/get_gpp_data_bridge_test.dart | 66 ++++++++ .../bridge/get_gpp_string_bridge_test.dart | 53 ++++++ .../bridge/set_gpp_consent_bridge_test.dart | 71 ++++++++ test/platform/fake_usercentrics_platform.dart | 41 +++++ 39 files changed, 1165 insertions(+), 51 deletions(-) create mode 100644 android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridge.kt create mode 100644 android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridge.kt create mode 100644 android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridge.kt create mode 100644 android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/GppDataSerializer.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridgeTest.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridgeTest.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridgeTest.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPDataMock.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPStringMock.kt create mode 100644 android/src/test/java/com/usercentrics/sdk/flutter/mock/SetGPPConsentMock.kt create mode 100644 example/lib/gpp_testing.dart create mode 100644 ios/Classes/Bridge/GetGPPDataBridge.swift create mode 100644 ios/Classes/Bridge/GetGPPStringBridge.swift create mode 100644 ios/Classes/Bridge/GppSectionChangeStreamHandler.swift create mode 100644 ios/Classes/Bridge/SetGPPConsentBridge.swift create mode 100644 ios/Classes/Serializer/GppDataSerializer.swift create mode 100644 lib/src/internal/bridge/get_gpp_data_bridge.dart create mode 100644 lib/src/internal/bridge/get_gpp_string_bridge.dart create mode 100644 lib/src/internal/bridge/set_gpp_consent_bridge.dart create mode 100644 lib/src/internal/serializer/gpp_data_serializer.dart create mode 100644 lib/src/model/gpp_data.dart create mode 100644 lib/src/model/gpp_section_change_payload.dart create mode 100644 test/internal/bridge/get_gpp_data_bridge_test.dart create mode 100644 test/internal/bridge/get_gpp_string_bridge_test.dart create mode 100644 test/internal/bridge/set_gpp_consent_bridge_test.dart 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 d867caff..8d4b1860 100644 --- a/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/UsercentricsPlugin.kt @@ -11,6 +11,10 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterAssets import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import com.usercentrics.sdk.UsercentricsEvent +import com.usercentrics.sdk.services.gpp.GppSectionChangePayload +import com.usercentrics.sdk.flutter.serializer.serialize +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -24,6 +28,8 @@ class UsercentricsPlugin : FlutterPlugin, FlutterAssetsProvider { private lateinit var channel: MethodChannel + private var gppSectionChangeEventChannel: EventChannel? = null + private var gppSectionChangeSubscription: com.usercentrics.sdk.UsercentricsDisposableEvent? = null private var activityBinding: ActivityPluginBinding? = null private var flutterAssets: FlutterAssets? = null @@ -63,7 +69,10 @@ class UsercentricsPlugin : FlutterPlugin, SetABTestingVariantBridge(), TrackBridge(), GetAdditionalConsentModeDataBridge(), - ClearUserSessionBridge() + ClearUserSessionBridge(), + GetGPPDataBridge(), + GetGPPStringBridge(), + SetGPPConsentBridge() ).associateBy { it.name } } @@ -94,11 +103,29 @@ class UsercentricsPlugin : FlutterPlugin, flutterAssets = binding.flutterAssets channel = MethodChannel(binding.binaryMessenger, "usercentrics") channel.setMethodCallHandler(this) + + gppSectionChangeEventChannel = EventChannel(binding.binaryMessenger, "usercentrics/onGppSectionChange") + gppSectionChangeEventChannel?.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + gppSectionChangeSubscription = UsercentricsEvent.onGppSectionChange { payload -> + events?.success(payload.serialize()) + } + } + + override fun onCancel(arguments: Any?) { + gppSectionChangeSubscription?.dispose() + gppSectionChangeSubscription = null + } + }) } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { flutterAssets = null channel.setMethodCallHandler(null) + gppSectionChangeSubscription?.dispose() + gppSectionChangeSubscription = null + gppSectionChangeEventChannel?.setStreamHandler(null) + gppSectionChangeEventChannel = null } override fun onAttachedToActivity(activityBinding: ActivityPluginBinding) { diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridge.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridge.kt new file mode 100644 index 00000000..f28e3191 --- /dev/null +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridge.kt @@ -0,0 +1,20 @@ +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 GetGPPDataBridge( + private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton +) : MethodBridge { + + override val name: String + get() = "getGPPData" + + override fun invoke(call: FlutterMethodCall, result: FlutterResult) { + assert(name == call.method) + result.success(usercentrics.instance.getGPPData().serialize()) + } +} diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridge.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridge.kt new file mode 100644 index 00000000..ac80cd82 --- /dev/null +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridge.kt @@ -0,0 +1,19 @@ +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 + +internal class GetGPPStringBridge( + private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton +) : MethodBridge { + + override val name: String + get() = "getGPPString" + + override fun invoke(call: FlutterMethodCall, result: FlutterResult) { + assert(name == call.method) + result.success(usercentrics.instance.getGPPString()) + } +} diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridge.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridge.kt new file mode 100644 index 00000000..1c2c5414 --- /dev/null +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridge.kt @@ -0,0 +1,30 @@ +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 + +internal class SetGPPConsentBridge( + private val usercentrics: UsercentricsProxy = UsercentricsProxySingleton +) : MethodBridge { + + override val name: String + get() = "setGPPConsent" + + override fun invoke(call: FlutterMethodCall, result: FlutterResult) { + assert(name == call.method) + val argsMap = call.arguments as Map<*, *> + val sectionName = argsMap["sectionName"] as String + val fieldName = argsMap["fieldName"] as String + val value = normalizeValue(argsMap["value"]) + usercentrics.instance.setGPPConsent(sectionName, fieldName, value) + result.success(null) + } + + private fun normalizeValue(value: Any?): Any { + if (value == null) return Unit + if (value is Double && value % 1.0 == 0.0) return value.toInt() + return value + } +} 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 764cd74e..5e858d4a 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 @@ -204,7 +204,6 @@ private fun TCF2Settings.serialize(): Any { "disabledSpecialFeatures" to disabledSpecialFeatures, "firstLayerShowDescriptions" to firstLayerShowDescriptions, "hideNonIabOnFirstLayer" to hideNonIabOnFirstLayer, - "resurfacePeriodEnded" to resurfacePeriodEnded, "resurfacePurposeChanged" to resurfacePurposeChanged, "resurfaceVendorAdded" to resurfaceVendorAdded, "firstLayerDescription" to firstLayerDescription, diff --git a/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/GppDataSerializer.kt b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/GppDataSerializer.kt new file mode 100644 index 00000000..06f52dcf --- /dev/null +++ b/android/src/main/kotlin/com/usercentrics/sdk/flutter/serializer/GppDataSerializer.kt @@ -0,0 +1,30 @@ +package com.usercentrics.sdk.flutter.serializer + +import com.usercentrics.sdk.services.gpp.GppData +import com.usercentrics.sdk.services.gpp.GppSectionChangePayload + +internal fun GppData.serialize(): Any { + val serializedSections = sections.mapValues { (_, fields) -> + fields.mapValues { (_, value) -> + when (value) { + null -> null + is Boolean -> value + is Int -> value + is Double -> value + is String -> value + else -> value.toString() + } + } + } + return mapOf( + "gppString" to gppString, + "applicableSections" to applicableSections, + "sections" to serializedSections + ) +} + +internal fun GppSectionChangePayload.serialize(): Any { + return mapOf( + "data" to data + ) +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridgeTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridgeTest.kt new file mode 100644 index 00000000..07bf6adb --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPDataBridgeTest.kt @@ -0,0 +1,48 @@ +package com.usercentrics.sdk.flutter.bridge + +import com.usercentrics.sdk.UsercentricsSDK +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.GetGPPDataMock +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 GetGPPDataBridgeTest { + + @Test + fun testName() { + val instance = GetGPPDataBridge(FakeUsercentricsProxy()) + assertEquals("getGPPData", instance.name) + } + + @Test + fun testInvokeWithOtherName() { + val instance = GetGPPDataBridge(FakeUsercentricsProxy()) + val call = FakeFlutterMethodCall(method = "otherName", arguments = null) + + assertThrows(AssertionError::class.java) { + instance.invoke(call, FakeFlutterResult()) + } + } + + @Test + fun testInvoke() { + val usercentricsSDK = mockk() + every { usercentricsSDK.getGPPData() }.returns(GetGPPDataMock.fake) + val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) + val instance = GetGPPDataBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(GetGPPDataMock.call, result) + + verify(exactly = 1) { usercentricsSDK.getGPPData() } + + assertEquals(1, result.successCount) + assertEquals(GetGPPDataMock.expected, result.successResultArgument) + } +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridgeTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridgeTest.kt new file mode 100644 index 00000000..e4a91aa3 --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/GetGPPStringBridgeTest.kt @@ -0,0 +1,48 @@ +package com.usercentrics.sdk.flutter.bridge + +import com.usercentrics.sdk.UsercentricsSDK +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.GetGPPStringMock +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 GetGPPStringBridgeTest { + + @Test + fun testName() { + val instance = GetGPPStringBridge(FakeUsercentricsProxy()) + assertEquals("getGPPString", instance.name) + } + + @Test + fun testInvokeWithOtherName() { + val instance = GetGPPStringBridge(FakeUsercentricsProxy()) + val call = FakeFlutterMethodCall(method = "otherName", arguments = null) + + assertThrows(AssertionError::class.java) { + instance.invoke(call, FakeFlutterResult()) + } + } + + @Test + fun testInvoke() { + val usercentricsSDK = mockk() + every { usercentricsSDK.getGPPString() }.returns(GetGPPStringMock.fake) + val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) + val instance = GetGPPStringBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(GetGPPStringMock.call, result) + + verify(exactly = 1) { usercentricsSDK.getGPPString() } + + assertEquals(1, result.successCount) + assertEquals(GetGPPStringMock.expected, result.successResultArgument) + } +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridgeTest.kt b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridgeTest.kt new file mode 100644 index 00000000..dca4a6ec --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/bridge/SetGPPConsentBridgeTest.kt @@ -0,0 +1,55 @@ +package com.usercentrics.sdk.flutter.bridge + +import com.usercentrics.sdk.UsercentricsSDK +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.SetGPPConsentMock +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 SetGPPConsentBridgeTest { + + @Test + fun testName() { + val instance = SetGPPConsentBridge(FakeUsercentricsProxy()) + assertEquals("setGPPConsent", instance.name) + } + + @Test + fun testInvokeWithOtherName() { + val instance = SetGPPConsentBridge(FakeUsercentricsProxy()) + val call = FakeFlutterMethodCall(method = "otherName", arguments = null) + + assertThrows(AssertionError::class.java) { + instance.invoke(call, FakeFlutterResult()) + } + } + + @Test + fun testInvoke() { + val usercentricsSDK = mockk() + every { + usercentricsSDK.setGPPConsent(any(), any(), any()) + }.returns(Unit) + val usercentricsProxy = FakeUsercentricsProxy(usercentricsSDK) + val instance = SetGPPConsentBridge(usercentricsProxy) + val result = FakeFlutterResult() + + instance.invoke(SetGPPConsentMock.call, result) + + verify(exactly = 1) { + usercentricsSDK.setGPPConsent( + SetGPPConsentMock.callSectionName, + SetGPPConsentMock.callFieldName, + SetGPPConsentMock.callValue + ) + } + + assertEquals(1, result.successCount) + } +} 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 5cc38da5..ac89e098 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 @@ -214,7 +214,6 @@ internal object GetCMPDataMock { tabsVendorsLabel = "Vendors", labelsIabVendors = "Vendors who are part of the IAB TCF", buttonsDenyAllLabel = "Deny all", - resurfacePeriodEnded = true, vendorSpecialPurposes = "Special Purposes", firstLayerAdditionalInfo = "", resurfaceVendorAdded = true, @@ -502,7 +501,6 @@ internal object GetCMPDataMock { "disabledSpecialFeatures" to listOf(), "firstLayerShowDescriptions" to false, "hideNonIabOnFirstLayer" to false, - "resurfacePeriodEnded" to true, "resurfacePurposeChanged" to true, "resurfaceVendorAdded" to true, "firstLayerDescription" to "We and our third-party vendors use technologies (e.g. cookies) to store and/or access information on user's devices in order to process personal data such as IP addresses or browsing data. You may consent to the processing of your personal data for the listed purposes below. Alternatively you can set your preferences before consenting or refuse to consent. Please note that some vendors may process your personal data based on their legitimate business interest and do not ask for your consent. To exercise your right to object to processing based on legitimate interest please view our vendorlist.", diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPDataMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPDataMock.kt new file mode 100644 index 00000000..b39809cb --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPDataMock.kt @@ -0,0 +1,32 @@ +package com.usercentrics.sdk.flutter.mock + +import com.usercentrics.sdk.flutter.api.FakeFlutterMethodCall +import com.usercentrics.sdk.services.gpp.GppData + +internal object GetGPPDataMock { + + val fake = GppData( + gppString = "DBABLA~BVQqAAAACgA.QA", + applicableSections = listOf(7, 8), + sections = mapOf( + "usnat" to mapOf( + "Version" to 1, + "SaleOptOut" to 2, + "GpcSegmentIncluded" to true, + ), + ), + ) + + val call = FakeFlutterMethodCall(method = "getGPPData", arguments = null) + val expected = mapOf( + "gppString" to "DBABLA~BVQqAAAACgA.QA", + "applicableSections" to listOf(7, 8), + "sections" to mapOf( + "usnat" to mapOf( + "Version" to 1, + "SaleOptOut" to 2, + "GpcSegmentIncluded" to true, + ), + ), + ) +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPStringMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPStringMock.kt new file mode 100644 index 00000000..392a82cc --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/GetGPPStringMock.kt @@ -0,0 +1,11 @@ +package com.usercentrics.sdk.flutter.mock + +import com.usercentrics.sdk.flutter.api.FakeFlutterMethodCall + +internal object GetGPPStringMock { + + val fake = "DBABLA~BVQqAAAACgA.QA" + + val call = FakeFlutterMethodCall(method = "getGPPString", arguments = null) + val expected = "DBABLA~BVQqAAAACgA.QA" +} diff --git a/android/src/test/java/com/usercentrics/sdk/flutter/mock/SetGPPConsentMock.kt b/android/src/test/java/com/usercentrics/sdk/flutter/mock/SetGPPConsentMock.kt new file mode 100644 index 00000000..12dd98f4 --- /dev/null +++ b/android/src/test/java/com/usercentrics/sdk/flutter/mock/SetGPPConsentMock.kt @@ -0,0 +1,19 @@ +package com.usercentrics.sdk.flutter.mock + +import com.usercentrics.sdk.flutter.api.FakeFlutterMethodCall + +internal object SetGPPConsentMock { + + val callSectionName = "usnat" + val callFieldName = "SaleOptOut" + val callValue = 2 + + val call = FakeFlutterMethodCall( + method = "setGPPConsent", + arguments = mapOf( + "sectionName" to callSectionName, + "fieldName" to callFieldName, + "value" to callValue, + ) + ) +} diff --git a/example/lib/gpp_testing.dart b/example/lib/gpp_testing.dart new file mode 100644 index 00000000..74112ba9 --- /dev/null +++ b/example/lib/gpp_testing.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:usercentrics_sdk/usercentrics_sdk.dart'; + +class GppTestingPage extends StatefulWidget { + const GppTestingPage({super.key}); + + @override + State createState() => _GppTestingPageState(); +} + +class _GppTestingPageState extends State { + String? _gppString; + String _gppDataJson = ''; + String _lastEvent = 'No events yet'; + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + _subscription = Usercentrics.onGppSectionChange.listen((payload) { + setState(() { + _lastEvent = payload.data; + }); + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + Future _fetchGppString() async { + try { + final value = await Usercentrics.gppString; + setState(() => _gppString = value); + } catch (e) { + setState(() => _gppString = 'Error: $e'); + } + } + + Future _fetchGppData() async { + try { + final value = await Usercentrics.gppData; + final encoder = const JsonEncoder.withIndent(' '); + final json = encoder.convert({ + 'gppString': value.gppString, + 'applicableSections': value.applicableSections, + 'sections': value.sections, + }); + setState(() => _gppDataJson = json); + } catch (e) { + setState(() => _gppDataJson = 'Error: $e'); + } + } + + Future _setUsNatSaleOptOut() async { + try { + await Usercentrics.setGPPConsent( + sectionName: 'usnat', + fieldName: 'SaleOptOut', + value: 2, + ); + } catch (e) { + _showError(e); + } + } + + Future _setUsFlSaleOptOut() async { + try { + await Usercentrics.setGPPConsent( + sectionName: 'usfl', + fieldName: 'SaleOptOut', + value: 2, + ); + } catch (e) { + _showError(e); + } + } + + void _showError(Object e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error: $e')), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('GPP Testing')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ElevatedButton( + onPressed: _fetchGppString, + child: const Text('Get GPP String'), + ), + const SizedBox(height: 8), + Text('GPP String: ${_gppString ?? "null"}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _fetchGppData, + child: const Text('Get GPP Data'), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _gppDataJson.isEmpty ? 'Tap "Get GPP Data"' : _gppDataJson, + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _setUsNatSaleOptOut, + child: const Text('Set usnat SaleOptOut = 2'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _setUsFlSaleOptOut, + child: const Text('Set usfl SaleOptOut = 2'), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Last onGppSectionChange event', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + _lastEvent, + style: const TextStyle(fontFamily: 'monospace', fontSize: 12), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 17e932f9..e5a4c96d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:usercentrics_example/gpp_testing.dart'; import 'package:usercentrics_example/webview_integration.dart'; import 'package:usercentrics_sdk/usercentrics_sdk.dart'; @@ -186,6 +187,14 @@ class HomePageState extends State { ), child: const Text("Webview Integration"), ), + ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const GppTestingPage()), + ), + child: const Text("GPP Testing"), + ), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 57129b91..b652d29e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -111,18 +111,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -196,17 +196,17 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" usercentrics_sdk: dependency: "direct main" description: path: ".." relative: true source: path - version: "2.24.4" + version: "2.25.1" vector_math: dependency: transitive description: @@ -256,5 +256,5 @@ packages: source: hosted version: "3.23.0" sdks: - dart: ">=3.8.0-0 <4.0.0" + dart: ">=3.9.0-0 <4.0.0" flutter: ">=3.29.0" diff --git a/example/test/fake_usercentrics.dart b/example/test/fake_usercentrics.dart index a6fdfcb2..a358246e 100644 --- a/example/test/fake_usercentrics.dart +++ b/example/test/fake_usercentrics.dart @@ -158,4 +158,23 @@ class FakeUsercentrics extends UsercentricsPlatform { Future clearUserSession() { throw UnimplementedError(); } + + @override + Future get gppData => throw UnimplementedError(); + + @override + Future get gppString => throw UnimplementedError(); + + @override + Future setGPPConsent({ + required String sectionName, + required String fieldName, + required dynamic value, + }) { + throw UnimplementedError(); + } + + @override + Stream get onGppSectionChange => + throw UnimplementedError(); } diff --git a/ios/Classes/Bridge/GetGPPDataBridge.swift b/ios/Classes/Bridge/GetGPPDataBridge.swift new file mode 100644 index 00000000..08c8536c --- /dev/null +++ b/ios/Classes/Bridge/GetGPPDataBridge.swift @@ -0,0 +1,12 @@ +import Usercentrics + +struct GetGPPDataBridge : MethodBridge { + + let name: String = "getGPPData" + let usercentrics: UsercentricsProxyProtocol + + func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + assert(call.method == name) + result(usercentrics.shared.getGPPData().serialize()) + } +} diff --git a/ios/Classes/Bridge/GetGPPStringBridge.swift b/ios/Classes/Bridge/GetGPPStringBridge.swift new file mode 100644 index 00000000..89653b01 --- /dev/null +++ b/ios/Classes/Bridge/GetGPPStringBridge.swift @@ -0,0 +1,12 @@ +import Usercentrics + +struct GetGPPStringBridge : MethodBridge { + + let name: String = "getGPPString" + let usercentrics: UsercentricsProxyProtocol + + func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + assert(call.method == name) + result(usercentrics.shared.getGPPString()) + } +} diff --git a/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift b/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift new file mode 100644 index 00000000..c5289577 --- /dev/null +++ b/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift @@ -0,0 +1,20 @@ +import Flutter +import Usercentrics + +class GppSectionChangeStreamHandler: NSObject, FlutterStreamHandler { + + private var subscription: UsercentricsDisposableEvent? + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + subscription = UsercentricsEvent.shared.onGppSectionChange { payload in + events(payload.serialize()) + } + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + subscription?.dispose() + subscription = nil + return nil + } +} diff --git a/ios/Classes/Bridge/SetGPPConsentBridge.swift b/ios/Classes/Bridge/SetGPPConsentBridge.swift new file mode 100644 index 00000000..9d273810 --- /dev/null +++ b/ios/Classes/Bridge/SetGPPConsentBridge.swift @@ -0,0 +1,17 @@ +import Usercentrics + +struct SetGPPConsentBridge : MethodBridge { + + let name: String = "setGPPConsent" + let usercentrics: UsercentricsProxyProtocol + + func invoke(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + assert(call.method == name) + let argsDict = call.arguments as! Dictionary + let sectionName = argsDict["sectionName"] as! String + let fieldName = argsDict["fieldName"] as! String + let value = argsDict["value"] ?? NSNull() + usercentrics.shared.setGPPConsent(sectionName: sectionName, fieldName: fieldName, value: value) + result(nil) + } +} diff --git a/ios/Classes/Serializer/GppDataSerializer.swift b/ios/Classes/Serializer/GppDataSerializer.swift new file mode 100644 index 00000000..c56dbb36 --- /dev/null +++ b/ios/Classes/Serializer/GppDataSerializer.swift @@ -0,0 +1,19 @@ +import Usercentrics + +extension GppData { + func serialize() -> Any { + return [ + "gppString": gppString, + "applicableSections": applicableSections, + "sections": sections, + ] + } +} + +extension GppSectionChangePayload { + func serialize() -> Any { + return [ + "data": data, + ] + } +} diff --git a/ios/Classes/SwiftUsercentricsPlugin.swift b/ios/Classes/SwiftUsercentricsPlugin.swift index a553f9d6..5501b95b 100644 --- a/ios/Classes/SwiftUsercentricsPlugin.swift +++ b/ios/Classes/SwiftUsercentricsPlugin.swift @@ -4,10 +4,17 @@ import Usercentrics public class SwiftUsercentricsPlugin: NSObject, FlutterPlugin { + private static var gppStreamHandler: GppSectionChangeStreamHandler? + public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "usercentrics", binaryMessenger: registrar.messenger()) let instance = SwiftUsercentricsPlugin(assetProvider: FlutterAssetProviderImpl(registrar: registrar)) registrar.addMethodCallDelegate(instance, channel: channel) + + let gppEventChannel = FlutterEventChannel(name: "usercentrics/onGppSectionChange", binaryMessenger: registrar.messenger()) + let streamHandler = GppSectionChangeStreamHandler() + gppEventChannel.setStreamHandler(streamHandler) + gppStreamHandler = streamHandler } let assetProvider: FlutterAssetProvider @@ -44,7 +51,10 @@ public class SwiftUsercentricsPlugin: NSObject, FlutterPlugin { GetABTestingVariantBridge(usercentrics: usercentrics), TrackBridge(usercentrics: usercentrics), GetAdditionalConsentModeBridge(usercentrics: usercentrics), - ClearUserSessionBridge(usercentrics: usercentrics) + ClearUserSessionBridge(usercentrics: usercentrics), + GetGPPDataBridge(usercentrics: usercentrics), + GetGPPStringBridge(usercentrics: usercentrics), + SetGPPConsentBridge(usercentrics: usercentrics) ] return bridges.reduce([String : MethodBridge]()) { dict, value in var dict = dict diff --git a/lib/src/internal/bridge/bridge.dart b/lib/src/internal/bridge/bridge.dart index 7cd5d356..58affc0a 100644 --- a/lib/src/internal/bridge/bridge.dart +++ b/lib/src/internal/bridge/bridge.dart @@ -23,3 +23,6 @@ export 'show_first_layer_bridge.dart'; export 'show_second_layer_bridge.dart'; export 'track_bridge.dart'; export 'clear_user_session_bridge.dart'; +export 'get_gpp_data_bridge.dart'; +export 'get_gpp_string_bridge.dart'; +export 'set_gpp_consent_bridge.dart'; diff --git a/lib/src/internal/bridge/get_gpp_data_bridge.dart b/lib/src/internal/bridge/get_gpp_data_bridge.dart new file mode 100644 index 00000000..4d978b4a --- /dev/null +++ b/lib/src/internal/bridge/get_gpp_data_bridge.dart @@ -0,0 +1,25 @@ +import 'package:flutter/services.dart'; +import 'package:usercentrics_sdk/src/internal/serializer/gpp_data_serializer.dart'; +import 'package:usercentrics_sdk/src/model/gpp_data.dart'; + +abstract class GetGPPDataBridge { + const GetGPPDataBridge(); + + Future invoke({ + required MethodChannel channel, + }); +} + +class MethodChannelGetGPPData extends GetGPPDataBridge { + const MethodChannelGetGPPData(); + + static const String _name = 'getGPPData'; + + @override + Future invoke({ + required MethodChannel channel, + }) async { + final result = await channel.invokeMethod(_name); + return GppDataSerializer.deserialize(result); + } +} diff --git a/lib/src/internal/bridge/get_gpp_string_bridge.dart b/lib/src/internal/bridge/get_gpp_string_bridge.dart new file mode 100644 index 00000000..afadf92a --- /dev/null +++ b/lib/src/internal/bridge/get_gpp_string_bridge.dart @@ -0,0 +1,23 @@ +import 'package:flutter/services.dart'; + +abstract class GetGPPStringBridge { + const GetGPPStringBridge(); + + Future invoke({ + required MethodChannel channel, + }); +} + +class MethodChannelGetGPPString extends GetGPPStringBridge { + const MethodChannelGetGPPString(); + + static const String _name = 'getGPPString'; + + @override + Future invoke({ + required MethodChannel channel, + }) async { + final result = await channel.invokeMethod(_name); + return result; + } +} diff --git a/lib/src/internal/bridge/set_gpp_consent_bridge.dart b/lib/src/internal/bridge/set_gpp_consent_bridge.dart new file mode 100644 index 00000000..263287ab --- /dev/null +++ b/lib/src/internal/bridge/set_gpp_consent_bridge.dart @@ -0,0 +1,35 @@ +import 'package:flutter/services.dart'; + +abstract class SetGPPConsentBridge { + const SetGPPConsentBridge(); + + Future invoke({ + required MethodChannel channel, + required String sectionName, + required String fieldName, + required dynamic value, + }); +} + +class MethodChannelSetGPPConsent extends SetGPPConsentBridge { + const MethodChannelSetGPPConsent(); + + static const String _name = 'setGPPConsent'; + + @override + Future invoke({ + required MethodChannel channel, + required String sectionName, + required String fieldName, + required dynamic value, + }) async { + await channel.invokeMethod( + _name, + { + 'sectionName': sectionName, + 'fieldName': fieldName, + 'value': value, + }, + ); + } +} diff --git a/lib/src/internal/platform/method_channel_usercentrics.dart b/lib/src/internal/platform/method_channel_usercentrics.dart index 26badee1..5812a2fd 100644 --- a/lib/src/internal/platform/method_channel_usercentrics.dart +++ b/lib/src/internal/platform/method_channel_usercentrics.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:usercentrics_sdk/src/internal/bridge/bridge.dart'; +import 'package:usercentrics_sdk/src/internal/serializer/gpp_data_serializer.dart'; import 'package:usercentrics_sdk/src/model/model.dart'; import 'package:usercentrics_sdk/src/platform/usercentrics_platform.dart'; @@ -33,9 +34,14 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { this.trackBridge = const MethodChannelTrack(), this.getAdditionalConsentModeData = const MethodChannelGetAdditionalConsentModeData(), - this.clearUserSessionBridge = const MethodChannelClearUserSession()}); + this.clearUserSessionBridge = const MethodChannelClearUserSession(), + this.getGPPDataBridge = const MethodChannelGetGPPData(), + this.getGPPStringBridge = const MethodChannelGetGPPString(), + this.setGPPConsentBridge = const MethodChannelSetGPPConsent()}); static const MethodChannel _channel = MethodChannel('usercentrics'); + static const EventChannel _gppSectionChangeEventChannel = + EventChannel('usercentrics/onGppSectionChange'); final InitializeBridge initializeBridge; final IsReadyBridge isReadyBridge; @@ -62,6 +68,9 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { final TrackBridge trackBridge; final GetAdditionalConsentModeDataBridge getAdditionalConsentModeData; final ClearUserSessionBridge clearUserSessionBridge; + final GetGPPDataBridge getGPPDataBridge; + final GetGPPStringBridge getGPPStringBridge; + final SetGPPConsentBridge setGPPConsentBridge; @visibleForTesting Completer? isReadyCompleter; @@ -335,4 +344,38 @@ class MethodChannelUsercentrics extends UsercentricsPlatform { await _ensureIsReady(); return await clearUserSessionBridge.invoke(channel: _channel); } + + @override + Future get gppData async { + await _ensureIsReady(); + return await getGPPDataBridge.invoke(channel: _channel); + } + + @override + Future get gppString async { + await _ensureIsReady(); + return await getGPPStringBridge.invoke(channel: _channel); + } + + @override + Future setGPPConsent({ + required String sectionName, + required String fieldName, + required dynamic value, + }) async { + await _ensureIsReady(); + await setGPPConsentBridge.invoke( + channel: _channel, + sectionName: sectionName, + fieldName: fieldName, + value: value, + ); + } + + @override + Stream get onGppSectionChange { + return _gppSectionChangeEventChannel + .receiveBroadcastStream() + .map((event) => GppDataSerializer.deserializePayload(event)); + } } diff --git a/lib/src/internal/serializer/gpp_data_serializer.dart b/lib/src/internal/serializer/gpp_data_serializer.dart new file mode 100644 index 00000000..acb2cd34 --- /dev/null +++ b/lib/src/internal/serializer/gpp_data_serializer.dart @@ -0,0 +1,27 @@ +import 'package:usercentrics_sdk/src/model/gpp_data.dart'; +import 'package:usercentrics_sdk/src/model/gpp_section_change_payload.dart'; + +class GppDataSerializer { + static GppData deserialize(dynamic value) { + final rawSections = value['sections'] as Map? ?? {}; + final sections = rawSections.map>( + (key, val) => MapEntry( + key as String, + (val as Map).cast(), + ), + ); + + return GppData( + gppString: value['gppString'] as String, + applicableSections: + (value['applicableSections'] as List).cast(), + sections: sections, + ); + } + + static GppSectionChangePayload deserializePayload(dynamic value) { + return GppSectionChangePayload( + data: value['data'] as String, + ); + } +} diff --git a/lib/src/model/gpp_data.dart b/lib/src/model/gpp_data.dart new file mode 100644 index 00000000..619e79fe --- /dev/null +++ b/lib/src/model/gpp_data.dart @@ -0,0 +1,64 @@ +/// GPP (Global Privacy Platform) data. +class GppData { + const GppData({ + required this.gppString, + required this.applicableSections, + required this.sections, + }); + + /// The GPP string encoding the user's consent preferences. + final String gppString; + + /// List of applicable section IDs. + final List applicableSections; + + /// Map of section name to a map of field name to field value. + final Map> sections; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GppData && + runtimeType == other.runtimeType && + gppString == other.gppString && + _listEquals(applicableSections, other.applicableSections) && + _sectionsEquals(sections, other.sections); + + @override + int get hashCode => + gppString.hashCode + + applicableSections.hashCode + + sections.hashCode; + + @override + String toString() => + "$GppData(gppString: $gppString, applicableSections: $applicableSections, sections: $sections)"; +} + +bool _listEquals(List a, List b) { + if (identical(a, b)) return true; + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +} + +bool _sectionsEquals( + Map> a, + Map> b) { + if (identical(a, b)) return true; + if (a.length != b.length) return false; + for (final key in a.keys) { + if (!b.containsKey(key)) return false; + final aInner = a[key]!; + final bInner = b[key]!; + if (aInner.length != bInner.length) return false; + for (final innerKey in aInner.keys) { + if (!bInner.containsKey(innerKey) || aInner[innerKey] != bInner[innerKey]) { + return false; + } + } + } + return true; +} diff --git a/lib/src/model/gpp_section_change_payload.dart b/lib/src/model/gpp_section_change_payload.dart new file mode 100644 index 00000000..4745eb7f --- /dev/null +++ b/lib/src/model/gpp_section_change_payload.dart @@ -0,0 +1,22 @@ +/// Payload emitted when a GPP section changes. +class GppSectionChangePayload { + const GppSectionChangePayload({ + required this.data, + }); + + /// The serialized section change data. + final String data; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GppSectionChangePayload && + runtimeType == other.runtimeType && + data == other.data; + + @override + int get hashCode => data.hashCode; + + @override + String toString() => "$GppSectionChangePayload(data: $data)"; +} diff --git a/lib/src/model/model.dart b/lib/src/model/model.dart index 668a366c..9d21ba60 100644 --- a/lib/src/model/model.dart +++ b/lib/src/model/model.dart @@ -1,4 +1,6 @@ export 'additional_consent_mode_data.dart'; +export 'gpp_data.dart'; +export 'gpp_section_change_payload.dart'; export 'analytics_event_type.dart'; export 'banner_settings.dart'; export 'button_settings.dart'; diff --git a/lib/src/platform/usercentrics_platform.dart b/lib/src/platform/usercentrics_platform.dart index 07edc6bd..88c0be05 100644 --- a/lib/src/platform/usercentrics_platform.dart +++ b/lib/src/platform/usercentrics_platform.dart @@ -101,4 +101,16 @@ abstract class UsercentricsPlatform { }); Future clearUserSession(); + + Future get gppData; + + Future get gppString; + + Future setGPPConsent({ + required String sectionName, + required String fieldName, + required dynamic value, + }); + + Stream get onGppSectionChange; } diff --git a/lib/src/usercentrics.dart b/lib/src/usercentrics.dart index 8e840d8d..625017a0 100644 --- a/lib/src/usercentrics.dart +++ b/lib/src/usercentrics.dart @@ -215,4 +215,26 @@ class Usercentrics { /// Clears the user session avoiding the sdk initialization. static Future clearUserSession() => _delegate.clearUserSession(); + + /// Get the GPP data including the GPP string, applicable sections, and section field values. + static Future get gppData => _delegate.gppData; + + /// Get the GPP string encoding the user's consent preferences. + static Future get gppString => _delegate.gppString; + + /// Set a GPP consent value for a specific section and field. + static Future setGPPConsent({ + required String sectionName, + required String fieldName, + required dynamic value, + }) => + _delegate.setGPPConsent( + sectionName: sectionName, + fieldName: fieldName, + value: value, + ); + + /// Stream of GPP section change events. + static Stream get onGppSectionChange => + _delegate.onGppSectionChange; } diff --git a/pubspec.lock b/pubspec.lock index 152db0a2..fa4433ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "85.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "7.7.1" + version: "10.0.1" args: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" cli_config: dependency: transitive description: @@ -171,38 +171,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -223,26 +215,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -388,26 +380,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.15" typed_data: dependency: transitive description: @@ -420,10 +412,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -481,5 +473,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.9.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/test/internal/bridge/get_gpp_data_bridge_test.dart b/test/internal/bridge/get_gpp_data_bridge_test.dart new file mode 100644 index 00000000..f0801cf7 --- /dev/null +++ b/test/internal/bridge/get_gpp_data_bridge_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:usercentrics_sdk/src/internal/bridge/bridge.dart'; +import 'package:usercentrics_sdk/src/model/gpp_data.dart'; + +void main() { + const mockResponse = { + 'gppString': 'DBABLA~BVQqAAAACgA.QA', + 'applicableSections': [7, 8], + 'sections': { + 'usnat': { + 'Version': 1, + 'SaleOptOut': 2, + 'GpcSegmentIncluded': true, + }, + 'usfl': { + 'Version': 1, + 'SaleOptOut': 0, + }, + }, + }; + + const expectedResult = GppData( + gppString: 'DBABLA~BVQqAAAACgA.QA', + applicableSections: [7, 8], + sections: { + 'usnat': { + 'Version': 1, + 'SaleOptOut': 2, + 'GpcSegmentIncluded': true, + }, + 'usfl': { + 'Version': 1, + 'SaleOptOut': 0, + }, + }, + ); + + 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 = MethodChannelGetGPPData(); + + final result = await instance.invoke(channel: channel); + + expect(callCounter, 1); + expect(receivedCall?.method, 'getGPPData'); + expect(result, expectedResult); + }); +} diff --git a/test/internal/bridge/get_gpp_string_bridge_test.dart b/test/internal/bridge/get_gpp_string_bridge_test.dart new file mode 100644 index 00000000..721cd368 --- /dev/null +++ b/test/internal/bridge/get_gpp_string_bridge_test.dart @@ -0,0 +1,53 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:usercentrics_sdk/src/internal/bridge/bridge.dart'; + +void main() { + const MethodChannel channel = MethodChannel('usercentrics'); + TestWidgetsFlutterBinding.ensureInitialized(); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('invoke returns string', () async { + int callCounter = 0; + MethodCall? receivedCall; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return 'DBABLA~BVQqAAAACgA.QA'; + }); + + const instance = MethodChannelGetGPPString(); + + final result = await instance.invoke(channel: channel); + + expect(callCounter, 1); + expect(receivedCall?.method, 'getGPPString'); + expect(result, 'DBABLA~BVQqAAAACgA.QA'); + }); + + test('invoke returns null', () async { + int callCounter = 0; + MethodCall? receivedCall; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return null; + }); + + const instance = MethodChannelGetGPPString(); + + final result = await instance.invoke(channel: channel); + + expect(callCounter, 1); + expect(receivedCall?.method, 'getGPPString'); + expect(result, isNull); + }); +} diff --git a/test/internal/bridge/set_gpp_consent_bridge_test.dart b/test/internal/bridge/set_gpp_consent_bridge_test.dart new file mode 100644 index 00000000..f5333dbc --- /dev/null +++ b/test/internal/bridge/set_gpp_consent_bridge_test.dart @@ -0,0 +1,71 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:usercentrics_sdk/src/internal/bridge/bridge.dart'; + +void main() { + const MethodChannel channel = MethodChannel('usercentrics'); + TestWidgetsFlutterBinding.ensureInitialized(); + + tearDown(() async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('invoke with int value', () async { + int callCounter = 0; + MethodCall? receivedCall; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return null; + }); + + const instance = MethodChannelSetGPPConsent(); + + await instance.invoke( + channel: channel, + sectionName: 'usnat', + fieldName: 'SaleOptOut', + value: 2, + ); + + expect(callCounter, 1); + expect(receivedCall?.method, 'setGPPConsent'); + expect(receivedCall?.arguments, { + 'sectionName': 'usnat', + 'fieldName': 'SaleOptOut', + 'value': 2, + }); + }); + + test('invoke with bool value', () async { + int callCounter = 0; + MethodCall? receivedCall; + + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + callCounter++; + receivedCall = methodCall; + return null; + }); + + const instance = MethodChannelSetGPPConsent(); + + await instance.invoke( + channel: channel, + sectionName: 'usnat', + fieldName: 'GpcSegmentIncluded', + value: true, + ); + + expect(callCounter, 1); + expect(receivedCall?.method, 'setGPPConsent'); + expect(receivedCall?.arguments, { + 'sectionName': 'usnat', + 'fieldName': 'GpcSegmentIncluded', + 'value': true, + }); + }); +} diff --git a/test/platform/fake_usercentrics_platform.dart b/test/platform/fake_usercentrics_platform.dart index b70a311f..30e2fa97 100644 --- a/test/platform/fake_usercentrics_platform.dart +++ b/test/platform/fake_usercentrics_platform.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + import 'package:usercentrics_sdk/src/model/additional_consent_mode_data.dart'; +import 'package:usercentrics_sdk/src/model/gpp_data.dart'; +import 'package:usercentrics_sdk/src/model/gpp_section_change_payload.dart'; import 'package:usercentrics_sdk/src/model/analytics_event_type.dart'; import 'package:usercentrics_sdk/src/model/banner_settings.dart'; import 'package:usercentrics_sdk/src/model/ccpa_data.dart'; @@ -357,4 +361,41 @@ class FakeUsercentricsPlatform extends UsercentricsPlatform { clearUserSessionCount++; return Future.value(clearUserSessionAnswer!); } + + var gppDataCount = 0; + + @override + Future get gppData { + gppDataCount++; + return Future.value(const GppData( + gppString: '', + applicableSections: [], + sections: {}, + )); + } + + var gppStringCount = 0; + + @override + Future get gppString { + gppStringCount++; + return Future.value(null); + } + + var setGPPConsentCount = 0; + + @override + Future setGPPConsent({ + required String sectionName, + required String fieldName, + required dynamic value, + }) { + setGPPConsentCount++; + return Future.value(null); + } + + @override + Stream get onGppSectionChange { + return const Stream.empty(); + } } From 0b290aa294f133c8801fcb9974598af9ec228ce4 Mon Sep 17 00:00:00 2001 From: "bruno.silva" Date: Tue, 10 Mar 2026 18:58:24 +0000 Subject: [PATCH 2/2] [MSDK-3287] Missing files to build iOS --- example/ios/Flutter/AppFrameworkInfo.plist | 2 -- example/ios/Podfile.lock | 17 ++++++----- example/ios/Runner.xcodeproj/project.pbxproj | 20 ++++++------- .../xcshareddata/xcschemes/Runner.xcscheme | 5 +++- example/ios/Runner/AppDelegate.swift | 11 ++++--- example/ios/Runner/Info.plist | 29 ++++++++++++++++--- .../GppSectionChangeStreamHandler.swift | 2 +- .../Serializer/CMPDataSerializer.swift | 1 - 8 files changed, 56 insertions(+), 31 deletions(-) diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 9625e105..391a902b 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 11.0 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 9079fd09..7c4feb4e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,11 +8,12 @@ PODS: - Usercentrics (= 2.25.1) - webview_flutter_wkwebview (0.0.1): - Flutter + - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) - usercentrics_sdk (from `.symlinks/plugins/usercentrics_sdk/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -25,15 +26,15 @@ EXTERNAL SOURCES: usercentrics_sdk: :path: ".symlinks/plugins/usercentrics_sdk/ios" webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - Usercentrics: d8b539d3dcaf5798513a203073e296fc9d55164c - usercentrics_sdk: 0379d3d77e6819959c602eb64048bf7302fb0a24 - UsercentricsUI: 0929c61f1b1cd8347c46568bd0d80258c96354fe - webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Usercentrics: 093b435b1e1c8deae1564d3d30a4d565e260f488 + usercentrics_sdk: 2dae37b3334d6e00a38d8a751922436a38051bea + UsercentricsUI: 70423bc65f51ed6f4cac3a6d20ebbee6e4e832aa + webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 PODFILE CHECKSUM: 723de1cf6e2f18b51eb3426c945e31134a750097 -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index ecf3b130..e2a4cdc7 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -291,7 +291,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1300; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -598,7 +598,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -619,7 +619,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -680,7 +680,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -729,7 +729,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -752,7 +752,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -779,7 +779,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -809,7 +809,7 @@ DEVELOPMENT_TEAM = 5V6UZKA8F4; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -845,7 +845,7 @@ DEVELOPMENT_TEAM = 5V6UZKA8F4; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -878,7 +878,7 @@ DEVELOPMENT_TEAM = 5V6UZKA8F4; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 7259750a..bac3a4b1 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -59,11 +60,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4a..c30b367e 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,16 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 170e1f49..189e88d2 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -22,6 +24,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +66,5 @@ UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift b/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift index c5289577..16ea024a 100644 --- a/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift +++ b/ios/Classes/Bridge/GppSectionChangeStreamHandler.swift @@ -3,7 +3,7 @@ import Usercentrics class GppSectionChangeStreamHandler: NSObject, FlutterStreamHandler { - private var subscription: UsercentricsDisposableEvent? + private var subscription: UsercentricsDisposableEvent? func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { subscription = UsercentricsEvent.shared.onGppSectionChange { payload in diff --git a/ios/Classes/Serializer/CMPDataSerializer.swift b/ios/Classes/Serializer/CMPDataSerializer.swift index 0b894365..408a65fc 100644 --- a/ios/Classes/Serializer/CMPDataSerializer.swift +++ b/ios/Classes/Serializer/CMPDataSerializer.swift @@ -195,7 +195,6 @@ extension TCF2Settings { "disabledSpecialFeatures" : self.disabledSpecialFeatures, "firstLayerShowDescriptions" : self.firstLayerShowDescriptions, "hideNonIabOnFirstLayer" : self.hideNonIabOnFirstLayer, - "resurfacePeriodEnded" : self.resurfacePeriodEnded, "resurfacePurposeChanged" : self.resurfacePurposeChanged, "resurfaceVendorAdded" : self.resurfaceVendorAdded, "firstLayerDescription" : self.firstLayerDescription as Any,