Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
andresilveirah committed Aug 7, 2024
2 parents 7071f59 + 986c3ec commit 46de39e
Show file tree
Hide file tree
Showing 15 changed files with 168 additions and 52 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,14 @@ Regarding `handleOnBackPress` from `spConsentLib`, there are 2 parameters:
- isMessageDismissible - flag that can customize the behaviour, when the user clicks back button on "Home" page of the message (if true - message is dismissible, if false - when the user is on "Home" page and clicks back, then the back event will be dispatched to the activity delegating navigation to the app)
- onHomePage - lambda, code in which should be invoked when the user clicks back on "Home" page of the message (in other words, the initial navigation position in message)

## Programmatically rejecting all for a user

It’s possible to programmatically issue a “reject all” action on behalf of the current end-user by calling the rejectAll(campaignType) function. The rejectAll function behaves in the exact same manner as if an end-user pressed the “reject all” button on the 1st layer message or privacy manager. Upon completion, the SDK will call either onConsentReady in case of success or onError in case of failure.

```kotlin
spConsentLib.rejectAll(CampaignType.GDPR)
```

## Adding or Removing custom consents

It's possible to programmatically consent the current user to a list of vendors, categories and legitimate interest categories by using the following method from the consent lib:
Expand Down Expand Up @@ -1160,7 +1168,7 @@ public class MainActivityJava extends AppCompatActivity {

When migrating a property from the U.S. Privacy (Legacy) campaign to U.S. Multi-State Privacy campaign, the SDK will automatically detect previously set end-user opt-in/opt-out preferences for U.S. Privacy (Legacy) and have that transferred over to U.S. Multi-State Privacy.

> If an end-user rejected a vendor or category for U.S. Privacy, Sourcepoint will set the _Sharing of Personal Information Targeted Advertisting_ and _Sale of Personal Information_ privacy choices or the _Sale or Share of Personal Information/Targeted Advertising_ privacy choice (depending on your configuration) to **opted-out** when the preferences are transferred.
> If an end-user rejected a vendor or category for U.S. Privacy, Sourcepoint will set the _Sharing of Personal Information Targeted Advertising_ and _Sale of Personal Information_ privacy choices or the _Sale or Share of Personal Information/Targeted Advertising_ privacy choice (depending on your configuration) to **opted-out** when the preferences are transferred.
If you ever used authenticated consent for CCPA, you'll have to specify the `ConfigOption.TRANSITION_CCPA_AUTH` option in your configuration to transfer an end-user's opt-in/opt-out preferences. The `ConfigOption.TRANSITION_CCPA_AUTH` option is crucial if you are using AuthId. This way, the SDK will look for authenticated consent within CCPA profiles and carry that over to USNat, even if the user current doesn't have CCPA local data (on a fresh install, for example).

Expand Down Expand Up @@ -1274,6 +1282,18 @@ import com.sourcepoint.cmplibrary.util.SpUtils;
SpUtils.clearAllData(context: Context)
```

## Dealing with device rotation

Make sure to add the following to your `AndroidManifest.xml` file:
```diff
<activity
android:name=".ActivityWhereTheConsentUIIsPresented"
+ android:configChanges="orientation|screenSize"
...
>
```
This way, Android won't re-instantiate your activity when users rotate the device, keeping the consent UI in the view hierarchy and maitaining its state.

## Frequently Asked Questions

### 1. How big is the SDK?
Expand Down
4 changes: 3 additions & 1 deletion cmplibrary/release_note.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*[DIA-4254](https://sourcepoint.atlassian.net/browse/DIA-4254) Fixed an preventing users from being sampled when calling the pv-data endpoint [#826](https://github.com/SourcePointUSA/android-cmp-app/pull/826)
*[DIA-4323](https://sourcepoint.atlassian.net/browse/DIA-4323) New feature enabling developers to programmatically reject all for a given user.[#828](https://github.com/SourcePointUSA/android-cmp-app/pull/828)
*[DIA-3891](https://sourcepoint.atlassian.net/browse/DIA-3891) Handle Accept/Reject all actions from USNat messages.[#829](https://github.com/SourcePointUSA/android-cmp-app/pull/829)
*[DIA-4258](https://sourcepoint.atlassian.net/browse/DIA-4258) Add an example on how to deal with device rotations without closing the consent UI.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ interface SpConsentLib {
successCallback: CustomConsentClient
)

fun rejectAll(campaignType: CampaignType)

fun loadPrivacyManager(pmId: String, campaignType: CampaignType)
fun loadPrivacyManager(pmId: String, pmTab: PMTab, campaignType: CampaignType)
fun loadPrivacyManager(pmId: String, pmTab: PMTab, campaignType: CampaignType, useGroupPmIfAvailable: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ internal class SpConsentLibImpl(
messageType = firstCampaign2Process.messageSubCategory.toMessageType(),
cmpViewId = cmpViewId,
)
.executeOnLeft { spClient.onError(it) }
.executeOnLeft { error -> spClient.onError(error) }
.getOrNull()

/** inject the message into the WebView */
Expand Down Expand Up @@ -304,7 +304,7 @@ internal class SpConsentLibImpl(
vendors = vendors.toList(),
categories = categories.toList(),
legIntCategories = legIntCategories.toList(),
success = {
success = { it ->
it?.let { spc ->
check { spc.toJsonObject().toString() }
.map { successCallback.transferCustomConsentToUnity(it) }
Expand All @@ -330,8 +330,8 @@ internal class SpConsentLibImpl(
vendors = vendors.toList(),
categories = categories.toList(),
legIntCategories = legIntCategories.toList(),
success = {
it?.let { spc ->
success = { response ->
response?.let { spc ->
check { spc.toJsonObject().toString() }
.map { successCallback.transferCustomConsentToUnity(it) }
} ?: run {
Expand Down Expand Up @@ -490,7 +490,11 @@ internal class SpConsentLibImpl(
consent = storedConsent,
)
}
.executeOnLeft { logMess("PmUrlConfig is null") }
.executeOnLeft { pLogger.d(this::class.java.simpleName, "PmUrlConfig is null") }
}

override fun rejectAll(campaignType: CampaignType) {
sendConsent(NativeMessageActionType.REJECT_ALL, campaignType)
}

override fun showView(view: View) {
Expand All @@ -508,8 +512,6 @@ internal class SpConsentLibImpl(
viewManager.removeAllViews()
}

private fun logMess(mess: String) = pLogger.d(this::class.java.simpleName, mess)

/**
* Method that verifies home page and delegates navigation between the message view and the
* activity that utilizes the message, using functional interface.
Expand Down Expand Up @@ -550,27 +552,11 @@ internal class SpConsentLibImpl(
}

override fun log(view: View, tag: String?, msg: String?) {
check { JSONObject(msg).toString() }
.getOrNull()
?.let {
// pLogger.clientEvent(
// event = "log",
// msg = "RenderingApp",
// content = it
// )
}
// TODO(not implemented)
}

override fun log(view: View, msg: String?) {
check { JSONObject(msg).toString() }
.getOrNull()
?.let {
// pLogger.clientEvent(
// event = "log",
// msg = "RenderingApp",
// content = it
// )
}
// TODO(not implemented)
}

override fun onError(view: View, errorMessage: String) {
Expand Down Expand Up @@ -610,20 +596,20 @@ internal class SpConsentLibImpl(
)
}

override fun onAction(iConsentWebView: IConsentWebView, actionData: String, nextCampaign: CampaignModel) {
override fun onAction(view: IConsentWebView, actionData: String, nextCampaign: CampaignModel) {
/** spClient is called from [onActionFromWebViewClient] */
(iConsentWebView as? View)?.let {
(view as? View)?.let {
pJsonConverter
.toConsentAction(actionData)
.map { ca ->
onActionFromWebViewClient(ca, iConsentWebView)
onActionFromWebViewClient(ca, view)
if (ca.actionType != SHOW_OPTIONS) {
val legislation = nextCampaign.type
val url = nextCampaign.url
when (nextCampaign.messageSubCategory) {
TCFv2, OTT, NATIVE_OTT -> {
executor.executeOnMain {
iConsentWebView.loadConsentUI(
view.loadConsentUI(
nextCampaign,
url,
legislation
Expand All @@ -632,7 +618,7 @@ internal class SpConsentLibImpl(
}
NATIVE_IN_APP -> {
executor.executeOnMain {
viewManager.removeView(iConsentWebView)
viewManager.removeView(view)
currentNativeMessageCampaign = nextCampaign
spClient.onNativeMessageReady(
nextCampaign.message.toNativeMessageDTO(
Expand Down Expand Up @@ -862,6 +848,7 @@ internal class SpConsentLibImpl(
}
.executeOnLeft { spClient.onError(it) }
}
CampaignType.USNAT -> { /* TODO(Not implemented) */ }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ internal class ServiceImpl(
.executeOnLeft { error ->
(error as? ConsentLibExceptionK)?.let { logger.error(error) }
val spConsents = ConsentManager.responseConsentHandler(
gdpr = campaignManager.gdprConsentStatus?.copy(applies = dataStorage.gdprApplies),
ccpa = campaignManager.ccpaConsentStatus?.copy(applies = dataStorage.ccpaApplies),
consentManagerUtils = consentManagerUtils,
)
onSpConsentsSuccess?.invoke(spConsents)
Expand Down Expand Up @@ -544,7 +544,7 @@ internal class ServiceImpl(
(error as? ConsentLibExceptionK)?.let { logger.error(error) }
}

// don't overwrite gdpr consents if the action is accept all or reject all
// don't overwrite ccpa consents if the action is accept all or reject all
// because the response from those endpoints does not contain a full consent
// object.
if (shouldWaitForPost) {
Expand All @@ -566,6 +566,45 @@ internal class ServiceImpl(
consentAction: ConsentActionImpl,
onSpConsentSuccess: ((SPConsents) -> Unit)?,
): Either<USNatConsentData> = check {
var getResp: ChoiceResp? = null
if (consentAction.actionType.isAcceptOrRejectAll()) {
getResp = networkClient.getChoice(
GetChoiceParamReq(
choiceType = consentAction.actionType.toChoiceTypeParam(),
accountId = spConfig.accountId.toLong(),
propertyId = spConfig.propertyId.toLong(),
env = env,
metadataArg = campaignManager.metaDataResp?.toMetaDataArg()?.copy(gdpr = null, ccpa = null),
includeData = buildIncludeData(gppDataValue = campaignManager.spConfig.getGppCustomOption())
)
)
.executeOnRight { response ->
response.usNat?.let { usnatResponse ->
campaignManager.usNatConsentData = usnatResponse.copy(uuid = campaignManager.usNatConsentData?.uuid)
onSpConsentSuccess?.invoke(
ConsentManager.responseConsentHandler(
usNat = usnatResponse.copy(
uuid = campaignManager.usNatConsentData?.uuid,
applies = dataStorage.usNatApplies,
),
consentManagerUtils = consentManagerUtils,
)
)
}
}
.executeOnLeft { error ->
(error as? ConsentLibExceptionK)?.let { logger.error(error) }
val spConsents = ConsentManager.responseConsentHandler(
usNat = campaignManager.usNatConsentData?.copy(applies = dataStorage.usNatApplies),
consentManagerUtils = consentManagerUtils,
)
onSpConsentSuccess?.invoke(spConsents)
}
.getOrNull()
}

val shouldWaitForPost = consentAction.actionType.isAcceptOrRejectAll().not() || getResp?.usNat == null

networkClient.storeUsNatChoice(
PostChoiceParamReq(
env = env,
Expand All @@ -592,12 +631,16 @@ internal class ServiceImpl(
(error as? ConsentLibExceptionK)?.let { logger.error(error) }
}

onSpConsentSuccess?.invoke(
ConsentManager.responseConsentHandler(
usNat = campaignManager.usNatConsentData?.copy(applies = dataStorage.usNatApplies),
consentManagerUtils = consentManagerUtils,
// don't overwrite usNat consents if the action is accept all or reject all
// because the response from those endpoints does not contain a full consent
// object.
if (shouldWaitForPost) {
val spConsents = ConsentManager.responseConsentHandler(
usNat = campaignManager.usNatConsentData?.copy(applies = dataStorage.usNatApplies),
consentManagerUtils = consentManagerUtils,
)
)
onSpConsentSuccess?.invoke(spConsents)
}

campaignManager.usNatConsentData ?: throw InvalidConsentResponse(
cause = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,7 @@ internal data class UsNatConsentInternal(
gpcStatus = consentStatus?.granularStatus?.gpcStatus,
)
}

internal fun SPConsents.toWebViewConsentsJsonObject(): JsonObject = buildJsonObject {
fun SPConsents.toWebViewConsentsJsonObject(): JsonObject = buildJsonObject {
ccpa?.consent?.let { ccpaConsent ->
if (ccpaConsent.isWebConsentEligible()) {
putJsonObject("ccpa") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.content.Context
import com.sourcepoint.cmplibrary.campaign.CampaignManager
import com.sourcepoint.cmplibrary.consent.ClientEventManager
import com.sourcepoint.cmplibrary.consent.ConsentManager
import com.sourcepoint.cmplibrary.consent.ConsentManagerUtils
import com.sourcepoint.cmplibrary.core.Either
import com.sourcepoint.cmplibrary.core.ExecutorManager
import com.sourcepoint.cmplibrary.data.Service
Expand All @@ -14,7 +13,6 @@ import com.sourcepoint.cmplibrary.data.network.converter.JsonConverter
import com.sourcepoint.cmplibrary.data.network.util.HttpUrlManager
import com.sourcepoint.cmplibrary.exception.CampaignType.GDPR
import com.sourcepoint.cmplibrary.exception.Logger
import com.sourcepoint.cmplibrary.model.Campaign
import com.sourcepoint.cmplibrary.model.PMTab
import com.sourcepoint.cmplibrary.model.exposed.MessageSubCategory.OTT
import com.sourcepoint.cmplibrary.model.exposed.MessageType
Expand All @@ -28,9 +26,6 @@ import org.junit.Before
import org.junit.Test

class SpConsentLibImplTest {

internal var campaign = Campaign(22, "tcfv2.mobile.webview", "122058")

@MockK
private lateinit var appCtx: Context

Expand Down Expand Up @@ -58,9 +53,6 @@ class SpConsentLibImplTest {
@MockK
private lateinit var consentManager: ConsentManager

@MockK
private lateinit var consentManagerUtils: ConsentManagerUtils

@MockK
private lateinit var execManager: ExecutorManager

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ class ClientEventManagerTest {
}

@Test
fun `GIVEN 2 successfully sendConsent (GDPR, CCPA) calls, TRIGGER 1 onSpFinish`() {
fun `GIVEN 3 successfully sendConsent (GDPR, CCPA, USNAT) calls, TRIGGER 1 onSpFinish`() {
clientEventManager.run {
setCampaignsToProcess(2) // 2 campaigns GDPR and CCPA
setAction(ConsentActionImpl(actionType = ACCEPT_ALL, requestFromPm = false, campaignType = CampaignType.GDPR)) // accept the GDPR
setAction(ConsentActionImpl(actionType = ACCEPT_ALL, requestFromPm = false, campaignType = CampaignType.CCPA)) // accept the CCPA
setAction(ConsentActionImpl(actionType = ACCEPT_ALL, requestFromPm = false, campaignType = CampaignType.USNAT)) // accept the USNAT
registerConsentResponse() // first consent saved
registerConsentResponse() // second consent saved
registerConsentResponse() // third consent saved
checkIfAllCampaignsWereProcessed() // check the status
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,35 @@ class NetworkClientImplTest {
method.assertEquals("POST")
}
}

@Test
fun `storeUsNatChoice - WHEN executed with pubData in request params THEN should have pubData in request for GDPR`() {
// GIVEN
val slot = slot<Request>()
val mockResponse = mockk<Response>()
val mockCall = mockk<Call>()
val mockBody = JsonObject(
mapOf(
"pb_key" to JsonPrimitive("pb_value")
)
)
val mockRequest = PostChoiceParamReq(
env = Env.PROD,
actionType = ActionType.ACCEPT_ALL,
body = mockBody
)

// WHEN
every { okHttp.newCall(any()) }.returns(mockCall)
every { mockCall.execute() }.returns(mockResponse)
sut.storeUsNatChoice(mockRequest)

// THEN
verify(exactly = 1) { responseManager.parsePostUsNatChoiceResp(mockResponse) }
verify(exactly = 1) { okHttp.newCall(capture(slot)) }
slot.captured.run {
readText().let { Json.parseToJsonElement(it).jsonObject }.assertEquals(mockBody)
method.assertEquals("POST")
}
}
}
Loading

0 comments on commit 46de39e

Please sign in to comment.