Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions packages/sourcepoint_unified_cmp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions packages/sourcepoint_unified_cmp/example_spm/ios/Podfile
Original file line number Diff line number Diff line change
@@ -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
8 changes: 6 additions & 2 deletions packages/sourcepoint_unified_cmp/lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<SPConsent> loadMessage() async {
Future<SPConsent> 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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<String, String>.toTargetingParams(): List<TargetingParam> =
map { (key, value) -> TargetingParam(key, value) }
Original file line number Diff line number Diff line change
Expand Up @@ -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<HostAPISPConsent>) -> 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<String, String>, ccpaTargetingParams: Map<String, String>, usnatTargetingParams: Map<String, String>, authId: String?, callback: (Result<HostAPISPConsent>) -> Unit)
fun loadPrivacyManager(pmId: String, pmTab: HostAPIPMTab, campaignType: HostAPICampaignType, messageType: HostAPIMessageType, callback: (Result<Unit>) -> Unit)
fun customConsentGDPR(vendors: List<String>, categories: List<String>, legIntCategories: List<String>, callback: (Result<HostAPISPConsent>) -> Unit)
fun deleteCustomConsentGDPR(vendors: List<String>, categories: List<String>, legIntCategories: List<String>, callback: (Result<HostAPISPConsent>) -> Unit)
Expand Down Expand Up @@ -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<HostAPISPConsent> ->
val gdprTargetingParamsArg = args[10] as Map<String, String>
val ccpaTargetingParamsArg = args[11] as Map<String, String>
val usnatTargetingParamsArg = args[12] as Map<String, String>
val authIdArg = args[13] as String?
api.loadMessage(accountIdArg, propertyIdArg, propertyNameArg, pmIdArg, messageLanguageArg, campaignsEnvArg, messageTimeoutArg, runGDPRCampaignArg, runCCPACampaignArg, runUSNATCampaignArg, gdprTargetingParamsArg, ccpaTargetingParamsArg, usnatTargetingParamsArg, authIdArg) { result: Result<HostAPISPConsent> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(SourcepointUnifiedCmpPigeonUtils.wrapError(error))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ class SourcepointUnifiedCmpPlugin :
runGDPRCampaign: Boolean,
runCCPACampaign: Boolean,
runUSNATCampaign: Boolean,
gdprTargetingParams: Map<String, String>,
ccpaTargetingParams: Map<String, String>,
usnatTargetingParams: Map<String, String>,
authId: String?,
callback: (Result<HostAPISPConsent>) -> Unit
) {
Log.d("SourcepointUnifiedCmp", "loadMessage")
Expand All @@ -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()
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ extension on messages.HostAPIConsentAction {
actionType: actionType.toActionType(),
pubData: jsonDecode(pubData),
campaignType: campaignType.toCampaignType(),
customActionId: customActionId,
);
}
}
Expand Down Expand Up @@ -269,7 +270,7 @@ class SourcepointUnifiedCmpAndroid extends SourcepointUnifiedCmpPlatform {
}

@override
Future<SPConsent> loadMessage(SPConfig config) async {
Future<SPConsent> loadMessage(SPConfig config, {String? authId}) async {
assert(config.campaigns.isNotEmpty, 'campaigns cannot be empty');
final hostConsent = await _api.loadMessage(
accountId: config.accountId,
Expand All @@ -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;
Expand Down
Loading