@@ -14,7 +14,11 @@ import com.mparticle.identity.MParticleUser
14
14
import com.mparticle.internal.Logger
15
15
import com.mparticle.kits.KitIntegration.CommerceListener
16
16
import com.mparticle.kits.KitIntegration.IdentityListener
17
+ import org.json.JSONArray
18
+ import org.json.JSONException
19
+ import org.json.JSONObject
17
20
import java.math.BigDecimal
21
+ import java.util.EnumMap
18
22
19
23
class GoogleAnalyticsFirebaseKit : KitIntegration (), KitIntegration.EventListener, IdentityListener,
20
24
CommerceListener , KitIntegration .UserAttributeListener {
@@ -26,6 +30,10 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
26
30
context : Context
27
31
): List <ReportingMessage > {
28
32
Logger .info(" $name Kit relies on a functioning instance of Firebase Analytics. If your Firebase Analytics instance is not configured properly, this Kit will not work" )
33
+ val userConsentState = currentUser?.consentState
34
+ userConsentState?.let {
35
+ setConsent(currentUser.consentState)
36
+ }
29
37
return emptyList()
30
38
}
31
39
@@ -88,7 +96,35 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
88
96
Product .REFUND -> FirebaseAnalytics .Event .REFUND
89
97
Product .REMOVE_FROM_CART -> FirebaseAnalytics .Event .REMOVE_FROM_CART
90
98
Product .CLICK -> FirebaseAnalytics .Event .SELECT_CONTENT
91
- Product .CHECKOUT_OPTION -> FirebaseAnalytics .Event .SET_CHECKOUT_OPTION
99
+ Product .CHECKOUT_OPTION -> {
100
+ val warningMessage = WARNING_MESSAGE
101
+ val customFlags = commerceEvent.customFlags
102
+ if ((customFlags != null ) && customFlags.containsKey(CF_COMMERCE_EVENT_TYPE )
103
+ ) {
104
+ val commerceEventTypes =
105
+ customFlags[CF_COMMERCE_EVENT_TYPE ]
106
+ if (! commerceEventTypes.isNullOrEmpty()) {
107
+ when (commerceEventTypes[0 ]) {
108
+ FirebaseAnalytics .Event .ADD_SHIPPING_INFO -> {
109
+ FirebaseAnalytics .Event .ADD_SHIPPING_INFO
110
+ }
111
+ FirebaseAnalytics .Event .ADD_PAYMENT_INFO -> {
112
+ FirebaseAnalytics .Event .ADD_PAYMENT_INFO
113
+ }
114
+ else -> {
115
+ Logger .warning(warningMessage)
116
+ return emptyList()
117
+ }
118
+ }
119
+ } else {
120
+ Logger .warning(warningMessage)
121
+ return emptyList()
122
+ }
123
+ } else {
124
+ Logger .warning(warningMessage)
125
+ return emptyList()
126
+ }
127
+ }
92
128
Product .DETAIL -> FirebaseAnalytics .Event .VIEW_ITEM
93
129
else -> return emptyList()
94
130
}
@@ -198,12 +234,31 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
198
234
pickyBundle.putString(attributes.key, attributes.value.toString())
199
235
}
200
236
}
237
+ val customFlags = commerceEvent.customFlags
238
+ if (customFlags != null && customFlags.containsKey(CF_COMMERCE_EVENT_TYPE )) {
239
+ val commerceEventTypeList = customFlags[CF_COMMERCE_EVENT_TYPE ]
240
+ if (! commerceEventTypeList.isNullOrEmpty()) {
241
+ val commerceEventType = commerceEventTypeList[0 ]
242
+ if (commerceEventType == FirebaseAnalytics .Event .ADD_SHIPPING_INFO ) {
243
+ val shippingTier = customFlags[CF_SHIPPING_TIER ]
244
+ if (! shippingTier.isNullOrEmpty()) {
245
+ pickyBundle.putString(
246
+ FirebaseAnalytics .Param .SHIPPING_TIER ,
247
+ shippingTier[0 ]
248
+ )
249
+ }
250
+ } else if (commerceEventType == FirebaseAnalytics .Event .ADD_PAYMENT_INFO ) {
251
+ val paymentType = customFlags[CF_PAYMENT_TYPE ]
252
+ if (! paymentType.isNullOrEmpty()) {
253
+ pickyBundle.putString(FirebaseAnalytics .Param .PAYMENT_TYPE , paymentType[0 ])
254
+ }
255
+ }
256
+ }
257
+ }
201
258
202
259
pickyBundle
203
260
.putString(FirebaseAnalytics .Param .CURRENCY , currency)
204
261
.putBundleList(FirebaseAnalytics .Param .ITEMS , getProductBundles(commerceEvent))
205
- .putString(FirebaseAnalytics .Event .SET_CHECKOUT_OPTION , commerceEvent.checkoutOptions)
206
- .putInt(FirebaseAnalytics .Event .CHECKOUT_PROGRESS , commerceEvent.checkoutStep)
207
262
208
263
return pickyBundle
209
264
}
@@ -339,6 +394,120 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
339
394
consentState1 : ConsentState ,
340
395
filteredMParticleUser : FilteredMParticleUser
341
396
) {
397
+ setConsent(consentState1)
398
+ }
399
+
400
+ private fun setConsent (consentState : ConsentState ) {
401
+ val consentMap: MutableMap <FirebaseAnalytics .ConsentType , FirebaseAnalytics .ConsentStatus > =
402
+ EnumMap (
403
+ FirebaseAnalytics .ConsentType ::class .java
404
+ )
405
+ googleConsentMapSettings.forEach { it ->
406
+ val mpConsentSetting = settings[it.value]
407
+ if (! mpConsentSetting.isNullOrEmpty()) {
408
+ if (mpConsentSetting == GoogleConsentValues .GRANTED .consentValue) {
409
+ consentMap[it.key] = FirebaseAnalytics .ConsentStatus .GRANTED
410
+ } else if (mpConsentSetting == GoogleConsentValues .DENIED .consentValue) {
411
+ consentMap[it.key] = FirebaseAnalytics .ConsentStatus .DENIED
412
+ }
413
+ }
414
+ }
415
+
416
+ val clientConsentSettings = parseToNestedMap(consentState.toString())
417
+
418
+ parseConsentMapping(settings[consentMappingSDK]).forEach { currentConsent ->
419
+
420
+ val isConsentAvailable =
421
+ searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key)
422
+
423
+ if (isConsentAvailable != null ) {
424
+ val isConsentGranted: Boolean =
425
+ JSONObject (isConsentAvailable.toString()).opt(" consented" ) as Boolean
426
+ val consentStatus =
427
+ if (isConsentGranted) FirebaseAnalytics .ConsentStatus .GRANTED else FirebaseAnalytics .ConsentStatus .DENIED
428
+
429
+
430
+ when (currentConsent.value) {
431
+ " ad_storage" -> consentMap[FirebaseAnalytics .ConsentType .AD_STORAGE ] =
432
+ consentStatus
433
+
434
+ " ad_user_data" -> consentMap[FirebaseAnalytics .ConsentType .AD_USER_DATA ] =
435
+ consentStatus
436
+
437
+ " ad_personalization" -> consentMap[FirebaseAnalytics .ConsentType .AD_PERSONALIZATION ] =
438
+ consentStatus
439
+
440
+ " analytics_storage" -> consentMap[FirebaseAnalytics .ConsentType .ANALYTICS_STORAGE ] =
441
+ consentStatus
442
+ }
443
+ }
444
+ }
445
+ if (consentMap.isNotEmpty()) {
446
+ FirebaseAnalytics .getInstance(context).setConsent(consentMap)
447
+ }
448
+ }
449
+ private fun parseConsentMapping (json : String? ): Map <String , String > {
450
+ if (json.isNullOrEmpty()) {
451
+ return emptyMap()
452
+ }
453
+ val jsonWithFormat = json.replace(" \\ " , " " )
454
+
455
+ return try {
456
+ JSONArray (jsonWithFormat)
457
+ .let { jsonArray ->
458
+ (0 until jsonArray.length())
459
+ .associate {
460
+ val jsonObject = jsonArray.getJSONObject(it)
461
+ val map = jsonObject.getString(" map" )
462
+ val value = jsonObject.getString(" value" )
463
+ map to value
464
+ }
465
+ }
466
+ } catch (jse: JSONException ) {
467
+ Logger .warning(jse, " The Google Firebase kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status." )
468
+ emptyMap()
469
+ }
470
+ }
471
+
472
+ private fun parseToNestedMap (jsonString : String ): Map <String , Any > {
473
+ val topLevelMap = mutableMapOf<String , Any >()
474
+ try {
475
+ val jsonObject = JSONObject (jsonString)
476
+
477
+ for (key in jsonObject.keys()) {
478
+ val value = jsonObject.get(key)
479
+ if (value is JSONObject ) {
480
+ topLevelMap[key] = parseToNestedMap(value.toString())
481
+ } else {
482
+ topLevelMap[key] = value
483
+ }
484
+ }
485
+ } catch (e: Exception ) {
486
+ Logger .error(e, " The Google Firebase kit was unable to parse the user's ConsentState, consent may not be set correctly on the Google Analytics SDK" )
487
+ }
488
+ return topLevelMap
489
+ }
490
+
491
+ private fun searchKeyInNestedMap (map : Map <* , * >, key : Any ): Any? {
492
+ if (map.isNullOrEmpty()) {
493
+ return null
494
+ }
495
+ try {
496
+ for ((mapKey, mapValue) in map) {
497
+ if (mapKey.toString().equals(key.toString(), ignoreCase = true )) {
498
+ return mapValue
499
+ }
500
+ if (mapValue is Map <* , * >) {
501
+ val foundValue = searchKeyInNestedMap(mapValue, key)
502
+ if (foundValue != null ) {
503
+ return foundValue
504
+ }
505
+ }
506
+ }
507
+ } catch (e: Exception ) {
508
+ Logger .error(e, " The Google Firebase kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status." )
509
+ }
510
+ return null
342
511
}
343
512
344
513
fun standardizeAttributes (
@@ -438,11 +607,30 @@ class GoogleAnalyticsFirebaseKit : KitIntegration(), KitIntegration.EventListene
438
607
const val USER_ID_MPID_VALUE = " mpid"
439
608
private val forbiddenPrefixes = arrayOf(" google_" , " firebase_" , " ga_" )
440
609
private const val CURRENCY_FIELD_NOT_SET = " Currency field required by Firebase was not set, defaulting to 'USD'"
610
+ const val CF_COMMERCE_EVENT_TYPE = " Firebase.CommerceEventType"
611
+ const val CF_PAYMENT_TYPE = " Firebase.PaymentType"
612
+ const val CF_SHIPPING_TIER = " Firebase.ShippingTier"
613
+ const val WARNING_MESSAGE =
614
+ " Firebase no longer supports CHECKOUT_OPTION. To specify a different eventName, add CF_COMMERCE_EVENT_TYPE to your customFlags with a valid value"
441
615
private const val USD = " USD"
442
616
private const val eventMaxLength = 40
443
617
private const val userAttributeMaxLength = 24
444
618
private const val eventValMaxLength = 100
445
619
private const val userAttributeValMaxLength = 36
446
620
private const val KIT_NAME = " Google Analytics for Firebase"
621
+
622
+ // Constants for Read Consent
623
+ private const val consentMappingSDK = " consentMappingSDK"
624
+ enum class GoogleConsentValues (val consentValue : String ) {
625
+ GRANTED (" Granted" ),
626
+ DENIED (" Denied" )
627
+ }
628
+
629
+ val googleConsentMapSettings = mapOf (
630
+ FirebaseAnalytics .ConsentType .AD_STORAGE to " defaultAdStorageConsentSDK" ,
631
+ FirebaseAnalytics .ConsentType .AD_USER_DATA to " defaultAdUserDataConsentSDK" ,
632
+ FirebaseAnalytics .ConsentType .AD_PERSONALIZATION to " defaultAdPersonalizationConsentSDK" ,
633
+ FirebaseAnalytics .ConsentType .ANALYTICS_STORAGE to " defaultAnalyticsStorageConsentSDK"
634
+ )
447
635
}
448
636
}
0 commit comments