diff --git a/README.md b/README.md index bd4dd30..c5b92af 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 12bb43e..952abb0 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 0000000..620e46e --- /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 bcd6390..e2ed9d3 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 100df06..1b4291c 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 e792441..b3e888b 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 887c0f9..c608b8a 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 afc8cbe..b2b08fd 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 f7762c9..30b38fb 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 70139cc..05318db 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, ); } } @@ -269,7 +270,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 +283,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 b78f96d..ead97bd 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 76e50d4..9568f89 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 7282cd8..54d9e07 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,59 @@ 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')); + }); + + 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_android/test/sourcepoint_unified_cmp_android_test.mocks.dart b/packages/sourcepoint_unified_cmp_android/test/sourcepoint_unified_cmp_android_test.mocks.dart index 0263ccb..5bbc75f 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 e6671d2..9a7ba06 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 5dcd1d9..72cb526 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 7d84007..deb9316 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, ); } } @@ -267,7 +268,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 +280,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 1208137..af084f7 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 64bef9c..20f0064 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 b48993e..2738bc4 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,59 @@ 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')); + }); + + 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/test/sourcepoint_unified_cmp_ios_test.mocks.dart b/packages/sourcepoint_unified_cmp_ios/test/sourcepoint_unified_cmp_ios_test.mocks.dart index a69d9ef..ec57ee3 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 91b9f64..6d0a2f5 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 e9d169a..a83e1a2 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 c2d4378..322a328 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 86d412d..556352a 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 3be9d48..924ed26 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 68dcd32..5fe574b 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 190a3fc..a170e62 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', () {