Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Google EU Consent #171

Merged
merged 4 commits into from
Sep 11, 2024
Merged
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
154 changes: 152 additions & 2 deletions src/main/kotlin/com/mparticle/kits/AppboyKit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.app.Application.ActivityLifecycleCallbacks
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.util.Log
import com.braze.Braze
import com.braze.BrazeActivityLifecycleCallbackListener
import com.braze.BrazeUser
Expand All @@ -23,11 +24,13 @@ import com.mparticle.commerce.CommerceEvent
import com.mparticle.commerce.Impression
import com.mparticle.commerce.Product
import com.mparticle.commerce.Promotion
import com.mparticle.consent.ConsentState
import com.mparticle.identity.MParticleUser
import com.mparticle.internal.Logger
import com.mparticle.kits.CommerceEventUtils.OnAttributeExtracted
import com.mparticle.kits.KitIntegration.*
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.math.BigDecimal
import java.text.SimpleDateFormat
Expand All @@ -38,7 +41,7 @@ import kotlin.collections.HashMap
* mParticle client-side Appboy integration
*/
open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
KitIntegration.EventListener, PushListener, IdentityListener {
KitIntegration.EventListener, PushListener, IdentityListener, KitIntegration.UserAttributeListener {

var enableTypeDetection = false
var bundleCommerceEvents = false
Expand Down Expand Up @@ -106,7 +109,10 @@ open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
if (user != null) {
updateUser(user)
}

val userConsentState = currentUser?.consentState
userConsentState?.let {
setConsent(currentUser.consentState)
}
return null
}

Expand Down Expand Up @@ -357,7 +363,146 @@ open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
})
}

override fun onIncrementUserAttribute(
Mansi-mParticle marked this conversation as resolved.
Show resolved Hide resolved
key: String?,
incrementedBy: Number?,
value: String?,
user: FilteredMParticleUser?
) {
}

override fun onRemoveUserAttribute(key: String?, user: FilteredMParticleUser?) {
}

override fun onSetUserAttribute(key: String?, value: Any?, user: FilteredMParticleUser?) {
}

override fun onSetUserTag(key: String?, user: FilteredMParticleUser?) {
}

override fun onSetUserAttributeList(
attributeKey: String?,
attributeValueList: MutableList<String>?,
user: FilteredMParticleUser?
) {
}

override fun onSetAllUserAttributes(
userAttributes: MutableMap<String, String>?,
userAttributeLists: MutableMap<String, MutableList<String>>?,
user: FilteredMParticleUser?
) {
}

override fun supportsAttributeLists(): Boolean = true
override fun onConsentStateUpdated(
oldState: ConsentState,
newState: ConsentState,
user: FilteredMParticleUser
) {
setConsent(newState)
}

private fun setConsent(consentState: ConsentState) {
val clientConsentSettings = parseToNestedMap(consentState.toString())

parseConsentMapping(settings[consentMappingSDK]).iterator().forEach { currentConsent ->
val isConsentAvailable =
searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key)

if (isConsentAvailable != null) {
val isConsentGranted: Boolean =
JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean

when (currentConsent.value) {
"google_ad_user_data" -> setConsentValueToBraze(
KEY_GOOGLE_AD_USER_DATA, isConsentGranted
)

"google_ad_personalization" -> setConsentValueToBraze(
KEY_GOOGLE_AD_PERSONALIZATION, isConsentGranted
)

}
}
}
}

private fun setConsentValueToBraze(key: String, value: Boolean) {
Braze.getInstance(context).getCurrentUser(object : IValueCallback<BrazeUser> {
override fun onSuccess(brazeUser: BrazeUser) {
brazeUser.setCustomUserAttribute(key, value)
}

override fun onError() {
super.onError()
}
})
}

private fun parseConsentMapping(json: String?): Map<String, String> {
if (json.isNullOrEmpty()) {
return emptyMap()
}
val jsonWithFormat = json.replace("\\", "")

return try {
JSONArray(jsonWithFormat)
.let { jsonArray ->
(0 until jsonArray.length())
.associate {
val jsonObject = jsonArray.getJSONObject(it)
val map = jsonObject.getString("map")
val value = jsonObject.getString("value")
map to value
}
}
} catch (jse: JSONException) {
Logger.warning(jse, "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
emptyMap()
}
}

private fun parseToNestedMap(jsonString: String): Map<String, Any> {
val topLevelMap = mutableMapOf<String, Any>()
try {
val jsonObject = JSONObject(jsonString)

for (key in jsonObject.keys()) {
val value = jsonObject.get(key)
if (value is JSONObject) {
topLevelMap[key] = parseToNestedMap(value.toString())
} else {
topLevelMap[key] = value
}
}
} catch (e: Exception) {
Logger.error(e, "The Braze kit was unable to parse the user's ConsentState, consent may not be set correctly on the Braze SDK")
}
return topLevelMap
}

private fun searchKeyInNestedMap(map: Map<*, *>, key: Any): Any? {
if (map.isNullOrEmpty()) {
return null
}
try {
for ((mapKey, mapValue) in map) {
if (mapKey.toString().equals(key.toString(), ignoreCase = true)) {
return mapValue
}
if (mapValue is Map<*, *>) {
val foundValue = searchKeyInNestedMap(mapValue, key)
if (foundValue != null) {
return foundValue
}
}
}
} catch (e: Exception) {
Logger.error(e, "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
}
return null
}

protected open fun queueDataFlush() {
dataFlushRunnable?.let { dataFlushHandler.removeCallbacks(it) }
Expand Down Expand Up @@ -944,6 +1089,11 @@ open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
private const val UNSUBSCRIBED = "unsubscribed"
private const val SUBSCRIBED = "subscribed"

//Constants for Read Consent
private const val consentMappingSDK = "consentMappingSDK"
private const val KEY_GOOGLE_AD_USER_DATA = "\$google_ad_user_data"
private const val KEY_GOOGLE_AD_PERSONALIZATION = "\$google_ad_personalization"

const val CUSTOM_ATTRIBUTES_KEY = "Attributes"
const val PRODUCT_KEY = "products"
const val PROMOTION_KEY = "promotions"
Expand Down
Loading
Loading