Skip to content

Commit 7812b65

Browse files
feat: Implement Google EU Consent (#171)
1 parent 8a2aee0 commit 7812b65

2 files changed

Lines changed: 400 additions & 3 deletions

File tree

src/main/kotlin/com/mparticle/kits/AppboyKit.kt

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.app.Application.ActivityLifecycleCallbacks
55
import android.content.Context
66
import android.content.Intent
77
import android.os.Handler
8+
import android.util.Log
89
import com.braze.Braze
910
import com.braze.BrazeActivityLifecycleCallbackListener
1011
import com.braze.BrazeUser
@@ -23,11 +24,13 @@ import com.mparticle.commerce.CommerceEvent
2324
import com.mparticle.commerce.Impression
2425
import com.mparticle.commerce.Product
2526
import com.mparticle.commerce.Promotion
27+
import com.mparticle.consent.ConsentState
2628
import com.mparticle.identity.MParticleUser
2729
import com.mparticle.internal.Logger
2830
import com.mparticle.kits.CommerceEventUtils.OnAttributeExtracted
2931
import com.mparticle.kits.KitIntegration.*
3032
import org.json.JSONArray
33+
import org.json.JSONException
3134
import org.json.JSONObject
3235
import java.math.BigDecimal
3336
import java.text.SimpleDateFormat
@@ -38,7 +41,7 @@ import kotlin.collections.HashMap
3841
* mParticle client-side Appboy integration
3942
*/
4043
open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
41-
KitIntegration.EventListener, PushListener, IdentityListener {
44+
KitIntegration.EventListener, PushListener, IdentityListener, KitIntegration.UserAttributeListener {
4245

4346
var enableTypeDetection = false
4447
var bundleCommerceEvents = false
@@ -106,7 +109,10 @@ open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
106109
if (user != null) {
107110
updateUser(user)
108111
}
109-
112+
val userConsentState = currentUser?.consentState
113+
userConsentState?.let {
114+
setConsent(currentUser.consentState)
115+
}
110116
return null
111117
}
112118

@@ -357,7 +363,146 @@ open class AppboyKit : KitIntegration(), AttributeListener, CommerceListener,
357363
})
358364
}
359365

366+
override fun onIncrementUserAttribute(
367+
key: String?,
368+
incrementedBy: Number?,
369+
value: String?,
370+
user: FilteredMParticleUser?
371+
) {
372+
}
373+
374+
override fun onRemoveUserAttribute(key: String?, user: FilteredMParticleUser?) {
375+
}
376+
377+
override fun onSetUserAttribute(key: String?, value: Any?, user: FilteredMParticleUser?) {
378+
}
379+
380+
override fun onSetUserTag(key: String?, user: FilteredMParticleUser?) {
381+
}
382+
383+
override fun onSetUserAttributeList(
384+
attributeKey: String?,
385+
attributeValueList: MutableList<String>?,
386+
user: FilteredMParticleUser?
387+
) {
388+
}
389+
390+
override fun onSetAllUserAttributes(
391+
userAttributes: MutableMap<String, String>?,
392+
userAttributeLists: MutableMap<String, MutableList<String>>?,
393+
user: FilteredMParticleUser?
394+
) {
395+
}
396+
360397
override fun supportsAttributeLists(): Boolean = true
398+
override fun onConsentStateUpdated(
399+
oldState: ConsentState,
400+
newState: ConsentState,
401+
user: FilteredMParticleUser
402+
) {
403+
setConsent(newState)
404+
}
405+
406+
private fun setConsent(consentState: ConsentState) {
407+
val clientConsentSettings = parseToNestedMap(consentState.toString())
408+
409+
parseConsentMapping(settings[consentMappingSDK]).iterator().forEach { currentConsent ->
410+
val isConsentAvailable =
411+
searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key)
412+
413+
if (isConsentAvailable != null) {
414+
val isConsentGranted: Boolean =
415+
JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean
416+
417+
when (currentConsent.value) {
418+
"google_ad_user_data" -> setConsentValueToBraze(
419+
KEY_GOOGLE_AD_USER_DATA, isConsentGranted
420+
)
421+
422+
"google_ad_personalization" -> setConsentValueToBraze(
423+
KEY_GOOGLE_AD_PERSONALIZATION, isConsentGranted
424+
)
425+
426+
}
427+
}
428+
}
429+
}
430+
431+
private fun setConsentValueToBraze(key: String, value: Boolean) {
432+
Braze.getInstance(context).getCurrentUser(object : IValueCallback<BrazeUser> {
433+
override fun onSuccess(brazeUser: BrazeUser) {
434+
brazeUser.setCustomUserAttribute(key, value)
435+
}
436+
437+
override fun onError() {
438+
super.onError()
439+
}
440+
})
441+
}
442+
443+
private fun parseConsentMapping(json: String?): Map<String, String> {
444+
if (json.isNullOrEmpty()) {
445+
return emptyMap()
446+
}
447+
val jsonWithFormat = json.replace("\\", "")
448+
449+
return try {
450+
JSONArray(jsonWithFormat)
451+
.let { jsonArray ->
452+
(0 until jsonArray.length())
453+
.associate {
454+
val jsonObject = jsonArray.getJSONObject(it)
455+
val map = jsonObject.getString("map")
456+
val value = jsonObject.getString("value")
457+
map to value
458+
}
459+
}
460+
} catch (jse: JSONException) {
461+
Logger.warning(jse, "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
462+
emptyMap()
463+
}
464+
}
465+
466+
private fun parseToNestedMap(jsonString: String): Map<String, Any> {
467+
val topLevelMap = mutableMapOf<String, Any>()
468+
try {
469+
val jsonObject = JSONObject(jsonString)
470+
471+
for (key in jsonObject.keys()) {
472+
val value = jsonObject.get(key)
473+
if (value is JSONObject) {
474+
topLevelMap[key] = parseToNestedMap(value.toString())
475+
} else {
476+
topLevelMap[key] = value
477+
}
478+
}
479+
} catch (e: Exception) {
480+
Logger.error(e, "The Braze kit was unable to parse the user's ConsentState, consent may not be set correctly on the Braze SDK")
481+
}
482+
return topLevelMap
483+
}
484+
485+
private fun searchKeyInNestedMap(map: Map<*, *>, key: Any): Any? {
486+
if (map.isNullOrEmpty()) {
487+
return null
488+
}
489+
try {
490+
for ((mapKey, mapValue) in map) {
491+
if (mapKey.toString().equals(key.toString(), ignoreCase = true)) {
492+
return mapValue
493+
}
494+
if (mapValue is Map<*, *>) {
495+
val foundValue = searchKeyInNestedMap(mapValue, key)
496+
if (foundValue != null) {
497+
return foundValue
498+
}
499+
}
500+
}
501+
} catch (e: Exception) {
502+
Logger.error(e, "The Braze kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.")
503+
}
504+
return null
505+
}
361506

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

1092+
//Constants for Read Consent
1093+
private const val consentMappingSDK = "consentMappingSDK"
1094+
private const val KEY_GOOGLE_AD_USER_DATA = "\$google_ad_user_data"
1095+
private const val KEY_GOOGLE_AD_PERSONALIZATION = "\$google_ad_personalization"
1096+
9471097
const val CUSTOM_ATTRIBUTES_KEY = "Attributes"
9481098
const val PRODUCT_KEY = "products"
9491099
const val PROMOTION_KEY = "promotions"

0 commit comments

Comments
 (0)