From 91dbbf7f9da2ad18dba329ab5501f2e95fabaa36 Mon Sep 17 00:00:00 2001 From: bschmalb-ksta Date: Wed, 16 Sep 2026 09:43:34 +0200 Subject: [PATCH 1/2] feat: pass targeting params and an authId to loadMessage The scenario in the sourcepoint portal picks the message a property serves from the targeting params the SDK sends along, and neither platform could send any, so a property with more than one message always fell through to the same one. SPConfig now takes targeting params per campaign. They reach addCampaign(campaignType, params, groupPmId) on android and SPCampaign(targetingParams:) on ios. loadMessage takes an optional authId, which both native SDKs already accept, so a consent decision a signed in user has taken elsewhere is found again instead of the message being shown a second time. Both are optional and default to nothing, so an existing caller is unaffected. Closes #265 --- README.md | 29 +++++++++++ packages/sourcepoint_unified_cmp/README.md | 29 +++++++++++ .../example_spm/ios/Podfile | 43 +++++++++++++++++ .../lib/src/controller.dart | 8 +++- .../test/sourcepoint_unified_cmp_test.dart | 29 +++++++++-- .../sourcepoint_unified_cmp_test.mocks.dart | 18 +++++-- .../unified/cmp/SourcePointUnifiedCmpData.kt | 8 ++++ .../unified/cmp/SourcepointUnifiedCmp.g.kt | 8 +++- .../cmp/SourcepointUnifiedCmpPlugin.kt | 37 ++++++++++++-- .../lib/sourcepoint_unified_cmp_android.dart | 9 +++- .../lib/src/messages.g.dart | 35 ++++++++------ .../pigeons/messages.dart | 4 ++ .../sourcepoint_unified_cmp_android_test.dart | 48 ++++++++++++++++++- ...epoint_unified_cmp_android_test.mocks.dart | 17 +++++++ .../SourcepointUnifiedCmpPlugin.swift | 8 ++-- .../pigeons/SourcepointUnifiedCmp.g.swift | 19 ++++++-- .../lib/sourcepoint_unified_cmp_ios.dart | 7 ++- .../lib/src/messages.g.dart | 6 +++ .../pigeons/messages.dart | 3 ++ .../sourcepoint_unified_cmp_ios_test.dart | 45 ++++++++++++++++- ...ourcepoint_unified_cmp_ios_test.mocks.dart | 13 +++++ .../lib/src/controller.dart | 2 +- .../lib/src/interface.dart | 6 ++- .../lib/src/method_channel.dart | 3 +- .../lib/src/types.dart | 14 ++++++ .../test/controller_test.dart | 2 +- ...ied_cmp_platform_interface_test.mocks.dart | 18 +++++-- .../test/types_test.dart | 21 ++++++++ 28 files changed, 441 insertions(+), 48 deletions(-) create mode 100644 packages/sourcepoint_unified_cmp/example_spm/ios/Podfile diff --git a/README.md b/README.md index bd4dd30f..c5b92af0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,35 @@ final config = SPConfig( _controller = SourcepointController(config: config); ``` +### targeting params + +`SPConfig.targetingParams` are handed to the scenario in the sourcepoint +portal, which is what decides the message a property serves. They are keyed by +the campaign they belong to: + +```dart +final config = SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'tcfv2.mobile.webview', + pmId: '122058', + campaigns: [CampaignType.gdpr], + targetingParams: { + CampaignType.gdpr: {'message': 'pur'}, + }, +); +``` + +### authenticated consent + +Pass an `authId` to load the message for a signed in user, so a consent +decision that user has already taken elsewhere is found again instead of the +message being shown a second time: + +```dart +final consent = await _controller.loadMessage(authId: 'a-user-id'); +``` + in the widget tree, the consent can initially be loaded using the `SourcepointUnifiedCMPBuilder`: ```dart diff --git a/packages/sourcepoint_unified_cmp/README.md b/packages/sourcepoint_unified_cmp/README.md index 12bb43e2..952abb0a 100644 --- a/packages/sourcepoint_unified_cmp/README.md +++ b/packages/sourcepoint_unified_cmp/README.md @@ -37,6 +37,35 @@ final config = SPConfig( _controller = SourcepointController(config: config); ``` +### targeting params + +`SPConfig.targetingParams` are handed to the scenario in the sourcepoint +portal, which is what decides the message a property serves. They are keyed by +the campaign they belong to: + +```dart +final config = SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'tcfv2.mobile.webview', + pmId: '122058', + campaigns: [CampaignType.gdpr], + targetingParams: { + CampaignType.gdpr: {'message': 'pur'}, + }, +); +``` + +### authenticated consent + +Pass an `authId` to load the message for a signed in user, so a consent +decision that user has already taken elsewhere is found again instead of the +message being shown a second time: + +```dart +final consent = await _controller.loadMessage(authId: 'a-user-id'); +``` + in the widget tree, the consent can initially be loaded using the `SourcepointUnifiedCMPBuilder`: ```dart diff --git a/packages/sourcepoint_unified_cmp/example_spm/ios/Podfile b/packages/sourcepoint_unified_cmp/example_spm/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/packages/sourcepoint_unified_cmp/example_spm/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/sourcepoint_unified_cmp/lib/src/controller.dart b/packages/sourcepoint_unified_cmp/lib/src/controller.dart index bcd63905..e2ed9d38 100644 --- a/packages/sourcepoint_unified_cmp/lib/src/controller.dart +++ b/packages/sourcepoint_unified_cmp/lib/src/controller.dart @@ -41,10 +41,14 @@ class SourcepointController extends ConsentChangeNotifier /// Loading the First Layer Message /// and returns the initial consent status + /// + /// Pass [authId] to load the message for a signed-in user, so a consent + /// decision that user already took elsewhere is reused instead of the + /// message being shown again. @override - Future loadMessage() async { + Future loadMessage({String? authId}) async { debugPrint('loadMessage'); - return _platform.loadMessage(config); + return _platform.loadMessage(config, authId: authId); } /// Programmatically grant custom GDPR consent to the supplied [vendors], diff --git a/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.dart b/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.dart index 100df060..1b4291c6 100644 --- a/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.dart +++ b/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.dart @@ -31,12 +31,31 @@ void main() { pmId: '122058', campaigns: [CampaignType.gdpr], ); - when(methodChannel.loadMessage(any)).thenAnswer((_) async => SPConsent()); + when( + methodChannel.loadMessage(any, authId: anyNamed('authId')), + ).thenAnswer((_) async => SPConsent()); final controller = SourcepointController(config: config); final r = await controller.loadMessage(); expect(r, isNotNull); }); + test('loadMessage passes the authId through to the platform', () async { + final config = SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'tcfv2.mobile.webview', + pmId: '122058', + campaigns: [CampaignType.gdpr], + ); + when( + methodChannel.loadMessage(any, authId: anyNamed('authId')), + ).thenAnswer((_) async => SPConsent()); + await SourcepointController( + config: config, + ).loadMessage(authId: 'user-42'); + verify(methodChannel.loadMessage(config, authId: 'user-42')).called(1); + }); + group('SourcepointController', () { test('setEventDelegate registers the delegate with the platform', () { final config = SPConfig( @@ -186,7 +205,9 @@ void main() { pmId: '122058', campaigns: [CampaignType.gdpr], ); - when(methodChannel.loadMessage(any)).thenAnswer((_) async => consent); + when( + methodChannel.loadMessage(any, authId: anyNamed('authId')), + ).thenAnswer((_) async => consent); final controller = SourcepointController(config: config); @@ -221,7 +242,9 @@ void main() { pmId: '122058', campaigns: [CampaignType.gdpr], ); - when(methodChannel.loadMessage(any)).thenAnswer((_) async => consent); + when( + methodChannel.loadMessage(any, authId: anyNamed('authId')), + ).thenAnswer((_) async => consent); SPConsent? receivedConsent; final controller = SourcepointController(config: config); diff --git a/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.mocks.dart b/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.mocks.dart index e7924413..b3e888b6 100644 --- a/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.mocks.dart +++ b/packages/sourcepoint_unified_cmp/test/sourcepoint_unified_cmp_test.mocks.dart @@ -3,6 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes + import 'dart:async' as _i5; import 'package:flutter/services.dart' as _i2; @@ -62,14 +63,23 @@ class MockMethodChannelSourcepointUnifiedCmp extends _i1.Mock as _i2.MethodChannel); @override - _i5.Future<_i3.SPConsent> loadMessage(_i3.SPConfig? config) => + _i5.Future<_i3.SPConsent> loadMessage( + _i3.SPConfig? config, { + String? authId, + }) => (super.noSuchMethod( - Invocation.method(#loadMessage, [config]), + Invocation.method(#loadMessage, [config], {#authId: authId}), returnValue: _i5.Future<_i3.SPConsent>.value( - _FakeSPConsent_1(this, Invocation.method(#loadMessage, [config])), + _FakeSPConsent_1( + this, + Invocation.method(#loadMessage, [config], {#authId: authId}), + ), ), returnValueForMissingStub: _i5.Future<_i3.SPConsent>.value( - _FakeSPConsent_1(this, Invocation.method(#loadMessage, [config])), + _FakeSPConsent_1( + this, + Invocation.method(#loadMessage, [config], {#authId: authId}), + ), ), ) as _i5.Future<_i3.SPConsent>); diff --git a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcePointUnifiedCmpData.kt b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcePointUnifiedCmpData.kt index 887c0f98..c608b8a2 100644 --- a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcePointUnifiedCmpData.kt +++ b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcePointUnifiedCmpData.kt @@ -25,6 +25,7 @@ import com.sourcepoint.cmplibrary.model.exposed.GDPRPurposeGrants import com.sourcepoint.cmplibrary.model.exposed.MessageType import com.sourcepoint.cmplibrary.model.exposed.SPConsents import com.sourcepoint.cmplibrary.model.exposed.SPGDPRConsent +import com.sourcepoint.cmplibrary.model.exposed.TargetingParam import com.sourcepoint.cmplibrary.model.exposed.toWebViewConsentsJsonObject fun GDPRPurposeGrants.toHostAPIPurposeGrants() = HostAPIGDPRPurposeGrants( @@ -136,3 +137,10 @@ fun HostAPIMessageType.toMessageType() = when (this) { HostAPIMessageType.OTT -> MessageType.OTT HostAPIMessageType.LEGACY_OTT -> MessageType.LEGACY_OTT } + +/** + * Sourcepoint takes targeting params as a list of key/value pairs, the + * flutter side sends them as a map. + */ +fun Map.toTargetingParams(): List = + map { (key, value) -> TargetingParam(key, value) } diff --git a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmp.g.kt b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmp.g.kt index afc8cbe1..b2b08fde 100644 --- a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmp.g.kt +++ b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmp.g.kt @@ -889,7 +889,7 @@ private open class SourcepointUnifiedCmpPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface SourcepointUnifiedCmpHostApi { - fun loadMessage(accountId: Long, propertyId: Long, propertyName: String, pmId: String, messageLanguage: HostAPIMessageLanguage, campaignsEnv: HostAPICampaignsEnv, messageTimeout: Long, runGDPRCampaign: Boolean, runCCPACampaign: Boolean, runUSNATCampaign: Boolean, callback: (Result) -> Unit) + fun loadMessage(accountId: Long, propertyId: Long, propertyName: String, pmId: String, messageLanguage: HostAPIMessageLanguage, campaignsEnv: HostAPICampaignsEnv, messageTimeout: Long, runGDPRCampaign: Boolean, runCCPACampaign: Boolean, runUSNATCampaign: Boolean, gdprTargetingParams: Map, ccpaTargetingParams: Map, usnatTargetingParams: Map, authId: String?, callback: (Result) -> Unit) fun loadPrivacyManager(pmId: String, pmTab: HostAPIPMTab, campaignType: HostAPICampaignType, messageType: HostAPIMessageType, callback: (Result) -> Unit) fun customConsentGDPR(vendors: List, categories: List, legIntCategories: List, callback: (Result) -> Unit) fun deleteCustomConsentGDPR(vendors: List, categories: List, legIntCategories: List, callback: (Result) -> Unit) @@ -918,7 +918,11 @@ interface SourcepointUnifiedCmpHostApi { val runGDPRCampaignArg = args[7] as Boolean val runCCPACampaignArg = args[8] as Boolean val runUSNATCampaignArg = args[9] as Boolean - api.loadMessage(accountIdArg, propertyIdArg, propertyNameArg, pmIdArg, messageLanguageArg, campaignsEnvArg, messageTimeoutArg, runGDPRCampaignArg, runCCPACampaignArg, runUSNATCampaignArg) { result: Result -> + val gdprTargetingParamsArg = args[10] as Map + val ccpaTargetingParamsArg = args[11] as Map + val usnatTargetingParamsArg = args[12] as Map + val authIdArg = args[13] as String? + api.loadMessage(accountIdArg, propertyIdArg, propertyNameArg, pmIdArg, messageLanguageArg, campaignsEnvArg, messageTimeoutArg, runGDPRCampaignArg, runCCPACampaignArg, runUSNATCampaignArg, gdprTargetingParamsArg, ccpaTargetingParamsArg, usnatTargetingParamsArg, authIdArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(SourcepointUnifiedCmpPigeonUtils.wrapError(error)) diff --git a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmpPlugin.kt b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmpPlugin.kt index f7762c91..30b38fb1 100644 --- a/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmpPlugin.kt +++ b/packages/sourcepoint_unified_cmp_android/android/src/main/kotlin/de/thekorn/sourcepoint/unified/cmp/SourcepointUnifiedCmpPlugin.kt @@ -221,6 +221,10 @@ class SourcepointUnifiedCmpPlugin : runGDPRCampaign: Boolean, runCCPACampaign: Boolean, runUSNATCampaign: Boolean, + gdprTargetingParams: Map, + ccpaTargetingParams: Map, + usnatTargetingParams: Map, + authId: String?, callback: (Result) -> Unit ) { Log.d("SourcepointUnifiedCmp", "loadMessage") @@ -231,9 +235,27 @@ class SourcepointUnifiedCmpPlugin : .addMessageLanguage(messageLanguage.toMessageLanguage()) .addCampaignsEnv(campaignsEnv.toCampaignsEnv()) .addMessageTimeout(messageTimeout) - if (runGDPRCampaign) cmpConfig.addCampaign(CampaignType.GDPR) - if (runCCPACampaign) cmpConfig.addCampaign(CampaignType.CCPA) - if (runUSNATCampaign) cmpConfig.addCampaign(CampaignType.USNAT) + if (runGDPRCampaign) { + cmpConfig.addCampaign( + CampaignType.GDPR, + gdprTargetingParams.toTargetingParams(), + null + ) + } + if (runCCPACampaign) { + cmpConfig.addCampaign( + CampaignType.CCPA, + ccpaTargetingParams.toTargetingParams(), + null + ) + } + if (runUSNATCampaign) { + cmpConfig.addCampaign( + CampaignType.USNAT, + usnatTargetingParams.toTargetingParams(), + null + ) + } Log.d("SourcepointUnifiedCmp", "loadMessage") val spClient = LocalClient() @@ -244,7 +266,14 @@ class SourcepointUnifiedCmpPlugin : activity = this.activity, spClient = spClient ) - spConsentLib!!.loadMessage() + // `loadMessage(authId)` and `loadMessage(authId, pubData, cmpViewId)` + // are both applicable to a single nullable String, so the no-arg + // overload keeps the call unambiguous when there is no authId. + if (authId == null) { + spConsentLib!!.loadMessage() + } else { + spConsentLib!!.loadMessage(authId = authId, pubData = null, cmpViewId = null) + } spClient.isInitialized.invokeOnCompletion { if (it != null) { Log.d("SourcepointUnifiedCmp", "initial loadMessage error thrown: $it") diff --git a/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart b/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart index 70139ccd..401f8a5e 100644 --- a/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart +++ b/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart @@ -269,7 +269,7 @@ class SourcepointUnifiedCmpAndroid extends SourcepointUnifiedCmpPlatform { } @override - Future loadMessage(SPConfig config) async { + Future loadMessage(SPConfig config, {String? authId}) async { assert(config.campaigns.isNotEmpty, 'campaigns cannot be empty'); final hostConsent = await _api.loadMessage( accountId: config.accountId, @@ -282,6 +282,13 @@ class SourcepointUnifiedCmpAndroid extends SourcepointUnifiedCmpPlatform { runCCPACampaign: config.campaigns.contains(CampaignType.ccpa), runGDPRCampaign: config.campaigns.contains(CampaignType.gdpr), runUSNATCampaign: config.campaigns.contains(CampaignType.usnat), + gdprTargetingParams: + config.targetingParams[CampaignType.gdpr] ?? const {}, + ccpaTargetingParams: + config.targetingParams[CampaignType.ccpa] ?? const {}, + usnatTargetingParams: + config.targetingParams[CampaignType.usnat] ?? const {}, + authId: authId, ); final consent = hostConsent.toSPConsent(); return consent; diff --git a/packages/sourcepoint_unified_cmp_android/lib/src/messages.g.dart b/packages/sourcepoint_unified_cmp_android/lib/src/messages.g.dart index b78f96da..ead97bdf 100644 --- a/packages/sourcepoint_unified_cmp_android/lib/src/messages.g.dart +++ b/packages/sourcepoint_unified_cmp_android/lib/src/messages.g.dart @@ -873,6 +873,10 @@ class SourcepointUnifiedCmpHostApi { required bool runGDPRCampaign, required bool runCCPACampaign, required bool runUSNATCampaign, + required Map gdprTargetingParams, + required Map ccpaTargetingParams, + required Map usnatTargetingParams, + String? authId, }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.sourcepoint_unified_cmp_android.SourcepointUnifiedCmpHostApi.loadMessage$pigeonVar_messageChannelSuffix'; @@ -881,20 +885,23 @@ class SourcepointUnifiedCmpHostApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [ - accountId, - propertyId, - propertyName, - pmId, - messageLanguage, - campaignsEnv, - messageTimeout, - runGDPRCampaign, - runCCPACampaign, - runUSNATCampaign, - ], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel + .send([ + accountId, + propertyId, + propertyName, + pmId, + messageLanguage, + campaignsEnv, + messageTimeout, + runGDPRCampaign, + runCCPACampaign, + runUSNATCampaign, + gdprTargetingParams, + ccpaTargetingParams, + usnatTargetingParams, + authId, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( diff --git a/packages/sourcepoint_unified_cmp_android/pigeons/messages.dart b/packages/sourcepoint_unified_cmp_android/pigeons/messages.dart index 76e50d43..9568f89e 100644 --- a/packages/sourcepoint_unified_cmp_android/pigeons/messages.dart +++ b/packages/sourcepoint_unified_cmp_android/pigeons/messages.dart @@ -169,6 +169,10 @@ abstract class SourcepointUnifiedCmpHostApi { required bool runGDPRCampaign, required bool runCCPACampaign, required bool runUSNATCampaign, + required Map gdprTargetingParams, + required Map ccpaTargetingParams, + required Map usnatTargetingParams, + String? authId, }); @async diff --git a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart index 7282cd8e..f5013d35 100644 --- a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart +++ b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart @@ -1,7 +1,11 @@ +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:sourcepoint_unified_cmp_android/src/messages.g.dart'; +import 'package:sourcepoint_unified_cmp_android/sourcepoint_unified_cmp_android.dart'; +import 'package:sourcepoint_unified_cmp_android/src/messages.g.dart' + hide SPConfig; +import 'package:sourcepoint_unified_cmp_platform_interface/sourcepoint_unified_cmp_platform_interface.dart'; @GenerateNiceMocks([MockSpec()]) import 'sourcepoint_unified_cmp_android_test.mocks.dart'; @@ -27,6 +31,9 @@ void main() { runGDPRCampaign: true, runCCPACampaign: false, runUSNATCampaign: false, + gdprTargetingParams: {}, + ccpaTargetingParams: {}, + usnatTargetingParams: {}, ), ).thenAnswer((_) async => HostAPISPConsent()); final consent = await api.loadMessage( @@ -40,6 +47,9 @@ void main() { runGDPRCampaign: true, runCCPACampaign: false, runUSNATCampaign: false, + gdprTargetingParams: {}, + ccpaTargetingParams: {}, + usnatTargetingParams: {}, ); expect(consent, isNotNull); }); @@ -75,4 +85,40 @@ void main() { ); expect(consent, isNotNull); }); + + test('loadMessage sends each campaign its own targeting params', () async { + List? sent; + const channel = BasicMessageChannel( + 'dev.flutter.pigeon.sourcepoint_unified_cmp_android' + '.SourcepointUnifiedCmpHostApi.loadMessage', + SourcepointUnifiedCmpHostApi.pigeonChannelCodec, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, (message) async { + sent = message! as List; + return [HostAPISPConsent()]; + }); + addTearDown( + () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null), + ); + + await SourcepointUnifiedCmpAndroid().loadMessage( + SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'tcfv2.mobile.webview', + pmId: '122058', + campaigns: [CampaignType.gdpr], + targetingParams: { + CampaignType.gdpr: {'message': 'pur', 'legal': 'no'}, + }, + ), + authId: 'user-42', + ); + + expect(sent, contains(equals({'message': 'pur', 'legal': 'no'}))); + expect(sent, contains('user-42')); + }); + } diff --git a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.mocks.dart b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.mocks.dart index 0263ccb7..5bbc75fc 100644 --- a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.mocks.dart +++ b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.mocks.dart @@ -3,6 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes + import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; @@ -62,6 +63,10 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock required bool? runGDPRCampaign, required bool? runCCPACampaign, required bool? runUSNATCampaign, + required Map? gdprTargetingParams, + required Map? ccpaTargetingParams, + required Map? usnatTargetingParams, + String? authId, }) => (super.noSuchMethod( Invocation.method(#loadMessage, [], { @@ -75,6 +80,10 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, #runUSNATCampaign: runUSNATCampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #usnatTargetingParams: usnatTargetingParams, + #authId: authId, }), returnValue: _i4.Future<_i2.HostAPISPConsent>.value( _FakeHostAPISPConsent_0( @@ -90,6 +99,10 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, #runUSNATCampaign: runUSNATCampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #usnatTargetingParams: usnatTargetingParams, + #authId: authId, }), ), ), @@ -107,6 +120,10 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, #runUSNATCampaign: runUSNATCampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #usnatTargetingParams: usnatTargetingParams, + #authId: authId, }), ), ), diff --git a/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/SourcepointUnifiedCmpPlugin.swift b/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/SourcepointUnifiedCmpPlugin.swift index e6671d28..9a7ba06c 100644 --- a/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/SourcepointUnifiedCmpPlugin.swift +++ b/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/SourcepointUnifiedCmpPlugin.swift @@ -15,21 +15,23 @@ public class SourcepointUnifiedCmpPlugin: UIViewController, FlutterPlugin, func loadMessage(accountId: Int64, propertyId: Int64, propertyName: String, pmId _: String, messageLanguage: HostAPIMessageLanguage, campaignsEnv: HostAPICampaignsEnv, messageTimeout: Int64, runGDPRCampaign: Bool, runCCPACampaign: Bool, + gdprTargetingParams: [String: String], + ccpaTargetingParams: [String: String], authId: String?, completion: @escaping (Result) -> Void) { consentManager = SPConsentManager( accountId: Int(accountId), propertyId: Int(propertyId), propertyName: try! SPPropertyName(propertyName), campaigns: SPCampaigns( - gdpr: runGDPRCampaign ? SPCampaign() : nil, - ccpa: runCCPACampaign ? SPCampaign() : nil, + gdpr: runGDPRCampaign ? SPCampaign(targetingParams: gdprTargetingParams) : nil, + ccpa: runCCPACampaign ? SPCampaign(targetingParams: ccpaTargetingParams) : nil, environment: campaignsEnv.toSPCampaignEnv() ), delegate: self ) consentManager.messageLanguage = messageLanguage.toSPMessageLanguage() consentManager.messageTimeoutInSeconds = Double(messageTimeout) / 1000 - consentManager.loadMessage() + consentManager.loadMessage(forAuthId: authId) isInitialized.setCompletionHandler { [completion] result in completion(.success(result.toHostAPISPConsent())) } diff --git a/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/pigeons/SourcepointUnifiedCmp.g.swift b/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/pigeons/SourcepointUnifiedCmp.g.swift index 5dcd1d90..72cb5266 100644 --- a/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/pigeons/SourcepointUnifiedCmp.g.swift +++ b/packages/sourcepoint_unified_cmp_ios/ios/sourcepoint_unified_cmp_ios/Sources/sourcepoint_unified_cmp_ios/pigeons/SourcepointUnifiedCmp.g.swift @@ -67,7 +67,9 @@ private func isNullish(_ value: Any?) -> Bool { } private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } + if value is NSNull { + return nil + } return value as! T? } @@ -132,7 +134,9 @@ func deepEqualsSourcepointUnifiedCmp(_ lhs: Any?, _ rhs: Any?) -> Bool { } } } - if !found { return false } + if !found { + return false + } } return true @@ -910,7 +914,8 @@ protocol SourcepointUnifiedCmpHostApi { func loadMessage(accountId: Int64, propertyId: Int64, propertyName: String, pmId: String, messageLanguage: HostAPIMessageLanguage, campaignsEnv: HostAPICampaignsEnv, messageTimeout: Int64, runGDPRCampaign: Bool, runCCPACampaign: Bool, - completion: @escaping (Result) -> Void) + gdprTargetingParams: [String: String], ccpaTargetingParams: [String: String], + authId: String?, completion: @escaping (Result) -> Void) func loadPrivacyManager(pmId: String, pmTab: HostAPIPMTab, campaignType: HostAPICampaignType, messageType: HostAPIMessageType, completion: @escaping (Result) -> Void) @@ -948,6 +953,9 @@ class SourcepointUnifiedCmpHostApiSetup { let messageTimeoutArg = args[6] as! Int64 let runGDPRCampaignArg = args[7] as! Bool let runCCPACampaignArg = args[8] as! Bool + let gdprTargetingParamsArg = args[9] as! [String: String] + let ccpaTargetingParamsArg = args[10] as! [String: String] + let authIdArg: String? = nilOrValue(args[11]) api.loadMessage( accountId: accountIdArg, propertyId: propertyIdArg, @@ -957,7 +965,10 @@ class SourcepointUnifiedCmpHostApiSetup { campaignsEnv: campaignsEnvArg, messageTimeout: messageTimeoutArg, runGDPRCampaign: runGDPRCampaignArg, - runCCPACampaign: runCCPACampaignArg + runCCPACampaign: runCCPACampaignArg, + gdprTargetingParams: gdprTargetingParamsArg, + ccpaTargetingParams: ccpaTargetingParamsArg, + authId: authIdArg ) { result in switch result { case let .success(res): diff --git a/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart b/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart index 7d840070..08a7103e 100644 --- a/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart +++ b/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart @@ -267,7 +267,7 @@ class SourcepointUnifiedCmpIOS extends SourcepointUnifiedCmpPlatform { } @override - Future loadMessage(SPConfig config) async { + Future loadMessage(SPConfig config, {String? authId}) async { assert(config.campaigns.isNotEmpty, 'campaigns cannot be empty'); final hostConsent = await _api.loadMessage( accountId: config.accountId, @@ -279,6 +279,11 @@ class SourcepointUnifiedCmpIOS extends SourcepointUnifiedCmpPlatform { messageTimeout: config.messageTimeout, runCCPACampaign: config.campaigns.contains(CampaignType.ccpa), runGDPRCampaign: config.campaigns.contains(CampaignType.gdpr), + gdprTargetingParams: + config.targetingParams[CampaignType.gdpr] ?? const {}, + ccpaTargetingParams: + config.targetingParams[CampaignType.ccpa] ?? const {}, + authId: authId, ); final consent = hostConsent.toSPConsent(); return consent; diff --git a/packages/sourcepoint_unified_cmp_ios/lib/src/messages.g.dart b/packages/sourcepoint_unified_cmp_ios/lib/src/messages.g.dart index 1208137e..af084f71 100644 --- a/packages/sourcepoint_unified_cmp_ios/lib/src/messages.g.dart +++ b/packages/sourcepoint_unified_cmp_ios/lib/src/messages.g.dart @@ -819,6 +819,9 @@ class SourcepointUnifiedCmpHostApi { required int messageTimeout, required bool runGDPRCampaign, required bool runCCPACampaign, + required Map gdprTargetingParams, + required Map ccpaTargetingParams, + String? authId, }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.sourcepoint_unified_cmp_ios.SourcepointUnifiedCmpHostApi.loadMessage$pigeonVar_messageChannelSuffix'; @@ -838,6 +841,9 @@ class SourcepointUnifiedCmpHostApi { messageTimeout, runGDPRCampaign, runCCPACampaign, + gdprTargetingParams, + ccpaTargetingParams, + authId, ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; diff --git a/packages/sourcepoint_unified_cmp_ios/pigeons/messages.dart b/packages/sourcepoint_unified_cmp_ios/pigeons/messages.dart index 64bef9c2..20f0064a 100644 --- a/packages/sourcepoint_unified_cmp_ios/pigeons/messages.dart +++ b/packages/sourcepoint_unified_cmp_ios/pigeons/messages.dart @@ -181,6 +181,9 @@ abstract class SourcepointUnifiedCmpHostApi { required int messageTimeout, required bool runGDPRCampaign, required bool runCCPACampaign, + required Map gdprTargetingParams, + required Map ccpaTargetingParams, + String? authId, }); @async diff --git a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart index b48993e5..75f9a22a 100644 --- a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart +++ b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart @@ -1,7 +1,10 @@ +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:sourcepoint_unified_cmp_ios/src/messages.g.dart'; +import 'package:sourcepoint_unified_cmp_ios/sourcepoint_unified_cmp_ios.dart'; +import 'package:sourcepoint_unified_cmp_ios/src/messages.g.dart' hide SPConfig; +import 'package:sourcepoint_unified_cmp_platform_interface/sourcepoint_unified_cmp_platform_interface.dart'; @GenerateNiceMocks([MockSpec()]) import 'sourcepoint_unified_cmp_ios_test.mocks.dart'; @@ -26,6 +29,8 @@ void main() { messageTimeout: 10000, runGDPRCampaign: true, runCCPACampaign: false, + gdprTargetingParams: {}, + ccpaTargetingParams: {}, ), ).thenAnswer((_) async => HostAPISPConsent()); final consent = await api.loadMessage( @@ -38,6 +43,8 @@ void main() { messageTimeout: 10000, runGDPRCampaign: true, runCCPACampaign: false, + gdprTargetingParams: {}, + ccpaTargetingParams: {}, ); expect(consent, isNotNull); }); @@ -73,4 +80,40 @@ void main() { ); expect(consent, isNotNull); }); + + test('loadMessage sends each campaign its own targeting params', () async { + List? sent; + const channel = BasicMessageChannel( + 'dev.flutter.pigeon.sourcepoint_unified_cmp_ios' + '.SourcepointUnifiedCmpHostApi.loadMessage', + SourcepointUnifiedCmpHostApi.pigeonChannelCodec, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, (message) async { + sent = message! as List; + return [HostAPISPConsent()]; + }); + addTearDown( + () => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null), + ); + + await SourcepointUnifiedCmpIOS().loadMessage( + SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'tcfv2.mobile.webview', + pmId: '122058', + campaigns: [CampaignType.gdpr], + targetingParams: { + CampaignType.gdpr: {'message': 'pur', 'legal': 'no'}, + }, + ), + authId: 'user-42', + ); + + expect(sent, contains(equals({'message': 'pur', 'legal': 'no'}))); + expect(sent, contains('user-42')); + }); + } diff --git a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.mocks.dart b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.mocks.dart index a69d9ef5..ec57ee33 100644 --- a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.mocks.dart +++ b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.mocks.dart @@ -3,6 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes + import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; @@ -61,6 +62,9 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock required int? messageTimeout, required bool? runGDPRCampaign, required bool? runCCPACampaign, + required Map? gdprTargetingParams, + required Map? ccpaTargetingParams, + String? authId, }) => (super.noSuchMethod( Invocation.method(#loadMessage, [], { @@ -73,6 +77,9 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #messageTimeout: messageTimeout, #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #authId: authId, }), returnValue: _i4.Future<_i2.HostAPISPConsent>.value( _FakeHostAPISPConsent_0( @@ -87,6 +94,9 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #messageTimeout: messageTimeout, #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #authId: authId, }), ), ), @@ -103,6 +113,9 @@ class MockSourcepointUnifiedCmpHostApi extends _i1.Mock #messageTimeout: messageTimeout, #runGDPRCampaign: runGDPRCampaign, #runCCPACampaign: runCCPACampaign, + #gdprTargetingParams: gdprTargetingParams, + #ccpaTargetingParams: ccpaTargetingParams, + #authId: authId, }), ), ), diff --git a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/controller.dart b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/controller.dart index 91b9f649..6d0a2f58 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/controller.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/controller.dart @@ -28,7 +28,7 @@ abstract class AbstractSourcepointConsentController /// Loading the First Layer Message /// and returns the initial consent status - Future loadMessage(); + Future loadMessage({String? authId}); /// Programmatically grant custom GDPR consent to the supplied [vendors], /// [categories] and [legIntCategories]. diff --git a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/interface.dart b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/interface.dart index e9d169a0..a83e1a25 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/interface.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/interface.dart @@ -74,7 +74,11 @@ abstract class SourcepointUnifiedCmpPlatform extends PlatformInterface { } /// show privacy manager - Future loadMessage(SPConfig config) { + /// + /// [authId] identifies the signed-in user to Sourcepoint, so a consent + /// decision taken on one device or on the web is found again instead of + /// the message being shown a second time. + Future loadMessage(SPConfig config, {String? authId}) { throw UnimplementedError('loadMessage() has not been implemented.'); } diff --git a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/method_channel.dart b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/method_channel.dart index c2d43781..322a328e 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/method_channel.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/method_channel.dart @@ -14,9 +14,10 @@ class MethodChannelSourcepointUnifiedCmp extends SourcepointUnifiedCmpPlatform { ConsentChangeNotifier? _notifier; @override - Future loadMessage(SPConfig config) async { + Future loadMessage(SPConfig config, {String? authId}) async { return (await methodChannel.invokeMethod('loadMessage', [ config, + authId, ]))!; } diff --git a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/types.dart b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/types.dart index 86d412d7..556352a7 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/lib/src/types.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/lib/src/types.dart @@ -46,6 +46,7 @@ class SPConfig { this.messageLanguage = MessageLanguage.english, this.campaignsEnv = CampaignsEnv.public, this.messageTimeout = 30000, + this.targetingParams = const {}, }); /// The account ID associated with the source point. @@ -73,6 +74,19 @@ class SPConfig { /// /// The [campaigns] property holds a list of [CampaignType] objects. final List campaigns; + + /// Targeting params sent with each campaign, keyed by the campaign they + /// belong to. + /// + /// Sourcepoint's scenario in the portal matches on these to decide which + /// message a property serves, so they are what lets an app ask for a + /// different first layer than the one the scenario falls through to. + /// + /// An entry is sent only for a campaign that [campaigns] also lists and + /// that `loadMessage` itself runs, which is [CampaignType.gdpr] and + /// [CampaignType.ccpa] on both platforms and [CampaignType.usnat] on + /// Android. Every other entry is ignored. + final Map> targetingParams; } /// Selected tab within the privacy manager. diff --git a/packages/sourcepoint_unified_cmp_platform_interface/test/controller_test.dart b/packages/sourcepoint_unified_cmp_platform_interface/test/controller_test.dart index 3be9d485..924ed264 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/test/controller_test.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/test/controller_test.dart @@ -30,7 +30,7 @@ class _TestController extends AbstractSourcepointConsentController }) async {} @override - Future loadMessage() async => SPConsent(); + Future loadMessage({String? authId}) async => SPConsent(); @override Future customConsentGdpr({ diff --git a/packages/sourcepoint_unified_cmp_platform_interface/test/sourcepoint_unified_cmp_platform_interface_test.mocks.dart b/packages/sourcepoint_unified_cmp_platform_interface/test/sourcepoint_unified_cmp_platform_interface_test.mocks.dart index 68dcd32c..5fe574b6 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/test/sourcepoint_unified_cmp_platform_interface_test.mocks.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/test/sourcepoint_unified_cmp_platform_interface_test.mocks.dart @@ -3,6 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes + import 'dart:async' as _i5; import 'package:flutter/services.dart' as _i2; @@ -62,14 +63,23 @@ class MockMethodChannelSourcepointUnifiedCmp extends _i1.Mock as _i2.MethodChannel); @override - _i5.Future<_i3.SPConsent> loadMessage(_i3.SPConfig? config) => + _i5.Future<_i3.SPConsent> loadMessage( + _i3.SPConfig? config, { + String? authId, + }) => (super.noSuchMethod( - Invocation.method(#loadMessage, [config]), + Invocation.method(#loadMessage, [config], {#authId: authId}), returnValue: _i5.Future<_i3.SPConsent>.value( - _FakeSPConsent_1(this, Invocation.method(#loadMessage, [config])), + _FakeSPConsent_1( + this, + Invocation.method(#loadMessage, [config], {#authId: authId}), + ), ), returnValueForMissingStub: _i5.Future<_i3.SPConsent>.value( - _FakeSPConsent_1(this, Invocation.method(#loadMessage, [config])), + _FakeSPConsent_1( + this, + Invocation.method(#loadMessage, [config], {#authId: authId}), + ), ), ) as _i5.Future<_i3.SPConsent>); diff --git a/packages/sourcepoint_unified_cmp_platform_interface/test/types_test.dart b/packages/sourcepoint_unified_cmp_platform_interface/test/types_test.dart index 190a3fc4..a170e62c 100644 --- a/packages/sourcepoint_unified_cmp_platform_interface/test/types_test.dart +++ b/packages/sourcepoint_unified_cmp_platform_interface/test/types_test.dart @@ -19,6 +19,7 @@ void main() { expect(config.messageLanguage, MessageLanguage.english); expect(config.campaignsEnv, CampaignsEnv.public); expect(config.messageTimeout, 30000); + expect(config.targetingParams, isEmpty); }); test('creates with optional parameters overriding defaults', () { @@ -37,6 +38,26 @@ void main() { expect(config.messageTimeout, 10000); expect(config.campaigns, contains(CampaignType.ccpa)); }); + + test('keeps targeting params separate per campaign', () { + final config = SPConfig( + accountId: 22, + propertyId: 7639, + propertyName: 'test.property', + pmId: '123', + campaigns: [CampaignType.gdpr, CampaignType.ccpa], + targetingParams: { + CampaignType.gdpr: {'message': 'pur', 'legal': 'no'}, + CampaignType.ccpa: {'message': 'ccpa'}, + }, + ); + expect(config.targetingParams[CampaignType.gdpr], { + 'message': 'pur', + 'legal': 'no', + }); + expect(config.targetingParams[CampaignType.ccpa], {'message': 'ccpa'}); + expect(config.targetingParams[CampaignType.usnat], isNull); + }); }); group('GDPRPurposeGrants', () { From b8e4ee593ca66d8b7e7d6e9a588fae7b4b12f7e9 Mon Sep 17 00:00:00 2001 From: bschmalb-ksta Date: Wed, 16 Sep 2026 09:43:55 +0200 Subject: [PATCH 2/2] fix: forward the custom action id to dart Both native sides already put the id of the tapped custom button on the pigeon message, but the dart mappers built a ConsentAction without it, so ConsentAction.customActionId was always null and a message with more than one custom button could not be told apart in onAction. --- .../lib/sourcepoint_unified_cmp_android.dart | 1 + .../sourcepoint_unified_cmp_android_test.dart | 19 +++++++++++++++++++ .../lib/sourcepoint_unified_cmp_ios.dart | 1 + .../sourcepoint_unified_cmp_ios_test.dart | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart b/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart index 401f8a5e..05318db9 100644 --- a/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart +++ b/packages/sourcepoint_unified_cmp_android/lib/sourcepoint_unified_cmp_android.dart @@ -102,6 +102,7 @@ extension on messages.HostAPIConsentAction { actionType: actionType.toActionType(), pubData: jsonDecode(pubData), campaignType: campaignType.toCampaignType(), + customActionId: customActionId, ); } } diff --git a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart index f5013d35..54d9e077 100644 --- a/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart +++ b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.dart @@ -121,4 +121,23 @@ void main() { expect(sent, contains('user-42')); }); + test('onAction reports the id of the custom button that was tapped', () { + ConsentAction? received; + SourcepointEventHandler( + delegate: _RecordingDelegate((action) => received = action), + ).onAction( + HostAPIConsentAction( + actionType: HostAPIActionType.custom, + pubData: '{}', + campaignType: HostAPICampaignType.gdpr, + customActionId: 'pur-subscribe', + ), + ); + expect(received?.customActionId, 'pur-subscribe'); + }); +} + +class _RecordingDelegate extends SourcepointEventDelegatePlatform { + _RecordingDelegate(void Function(ConsentAction) onAction) + : super(onAction: onAction); } diff --git a/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart b/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart index 08a7103e..deb9316b 100644 --- a/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart +++ b/packages/sourcepoint_unified_cmp_ios/lib/sourcepoint_unified_cmp_ios.dart @@ -102,6 +102,7 @@ extension on messages.HostAPIConsentAction { actionType: actionType.toActionType(), pubData: jsonDecode(pubData), campaignType: campaignType.toCampaignType(), + customActionId: customActionId, ); } } diff --git a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart index 75f9a22a..2738bc4b 100644 --- a/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart +++ b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.dart @@ -116,4 +116,23 @@ void main() { expect(sent, contains('user-42')); }); + test('onAction reports the id of the custom button that was tapped', () { + ConsentAction? received; + SourcepointEventHandler( + delegate: _RecordingDelegate((action) => received = action), + ).onAction( + HostAPIConsentAction( + actionType: HostAPIActionType.custom, + pubData: '{}', + campaignType: HostAPICampaignType.gdpr, + customActionId: 'pur-subscribe', + ), + ); + expect(received?.customActionId, 'pur-subscribe'); + }); +} + +class _RecordingDelegate extends SourcepointEventDelegatePlatform { + _RecordingDelegate(void Function(ConsentAction) onAction) + : super(onAction: onAction); }