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
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
## [5.80.0](https://github.com/mParticle/mparticle-android-sdk/compare/v5.79.2...v5.80.0) (2026-06-25)


### Features

* add device-based consent to override MPID-scoped consent ([#726](https://github.com/mParticle/mparticle-android-sdk/issues/726)) ([e92d352](https://github.com/mParticle/mparticle-android-sdk/commit/e92d3522a350be90a1f15eb3b81d5e6f089f1aed))
- add device-based consent to override MPID-scoped consent ([#726](https://github.com/mParticle/mparticle-android-sdk/issues/726)) ([e92d352](https://github.com/mParticle/mparticle-android-sdk/commit/e92d3522a350be90a1f15eb3b81d5e6f089f1aed))

## Unreleased

### Fixed

- Restore async wait for user attributes to persist before Rokt `selectPlacements` delegates to the kit, fixing a regression where placement attributes could be missing in mParticle and Rokt.

### Added

- Add device-based consent APIs (`MParticle.getDeviceConsentState()`, `MParticle.setDeviceConsentState()`) and `MParticleOptions.Builder.deviceBasedConsentEnabled()` so consent can be stored and applied at the device level, overriding MPID-based consent for kit forwarding rules and event uploads.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.graphics.Typeface
import com.mparticle.MParticle
import com.mparticle.MpRoktEventCallback
import com.mparticle.RoktEvent
import com.mparticle.TypedUserAttributeListener
import com.mparticle.identity.IdentityApi
import com.mparticle.identity.IdentityApiRequest
import com.mparticle.identity.MParticleUser
Expand Down Expand Up @@ -50,17 +51,20 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
val kitConfig = kitIntegration.configuration

confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
val finalAttributes = prepareAttributes(mutableAttributes, user)
roktListener.selectPlacements(
viewName,
finalAttributes,
mpRoktEventCallback,
placeHolders,
fontTypefaces,
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
config,
options,
)
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
setRoktAttributesOnUser(finalAttributes, user) {
ensureSandboxMode(finalAttributes)
roktListener.selectPlacements(
viewName,
finalAttributes,
mpRoktEventCallback,
placeHolders,
fontTypefaces,
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
config,
options,
)
}
}
} catch (e: Exception) {
Logger.warning("Failed to call execute for Rokt Kit: ${e.message}")
Expand Down Expand Up @@ -115,16 +119,19 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
return
}
val user = instance.Identity().currentUser
val email = mutableAttributes["email"]
val email = getValueIgnoreCase(mutableAttributes, "email")
val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256")
val kitConfig = kitIntegration.configuration

confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) {
val finalAttributes = prepareAttributes(mutableAttributes, user)
roktListener.enrichAttributes(
finalAttributes,
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
)
val finalAttributes = applyPlacementAttributeMapping(mutableAttributes)
setRoktAttributesOnUser(finalAttributes, user) {
ensureSandboxMode(finalAttributes)
roktListener.enrichAttributes(
finalAttributes,
FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration),
)
}
}
} catch (e: Exception) {
Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}")
Expand All @@ -142,7 +149,7 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
return null
}

private fun prepareAttributes(finalAttributes: MutableMap<String, String>, user: MParticleUser?): MutableMap<String, String> {
private fun applyPlacementAttributeMapping(attributes: MutableMap<String, String>): MutableMap<String, String> {
val kitConfig = kitIntegration.configuration
val jsonArray = try {
kitConfig?.placementAttributesMapping ?: org.json.JSONArray()
Expand All @@ -155,27 +162,51 @@ internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListe
val obj = jsonArray.optJSONObject(i) ?: continue
val mapFrom = obj.optString("map")
val mapTo = obj.optString("value")
if (finalAttributes.containsKey(mapFrom)) {
val value = finalAttributes.remove(mapFrom)
if (attributes.containsKey(mapFrom)) {
val value = attributes.remove(mapFrom)
if (value != null) {
finalAttributes[mapTo] = value
attributes[mapTo] = value
}
}
}
return attributes
}

private fun ensureSandboxMode(finalAttributes: MutableMap<String, String>) {
if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
Objects.toString(MPUtility.isDevEnv(), "false")
}
}

/**
* Persists placement attributes on the mParticle user, then invokes [onReady] after the
* attributes have been applied. This ensures the Rokt kit reads an up-to-date user profile
* when merging attributes for the placement.
*/
private fun setRoktAttributesOnUser(finalAttributes: Map<String, String>, user: MParticleUser?, onReady: Runnable) {
val objectAttributes = mutableMapOf<String, Any>()
for ((key, value) in finalAttributes) {
if (key != Constants.MessageKey.SANDBOX_MODE_ROKT) {
objectAttributes[key] = value
}
}
user?.setUserAttributes(objectAttributes)

if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) {
finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] =
Objects.toString(MPUtility.isDevEnv(), "false")
if (user != null) {
user.setUserAttributes(objectAttributes)
user.getUserAttributes(
object : TypedUserAttributeListener {
override fun onUserAttributesReceived(
userAttributes: Map<String, Any?>,
userAttributeLists: Map<String, List<String?>?>,
mpid: Long,
) {
onReady.run()
}
},
)
} else {
onReady.run()
}
return finalAttributes
}

private fun confirmEmail(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.mparticle.MParticle
import com.mparticle.MParticleOptions
import com.mparticle.MpRoktEventCallback
import com.mparticle.RoktEvent
import com.mparticle.TypedUserAttributeListener
import com.mparticle.WrapperSdk
import com.mparticle.WrapperSdkVersion
import com.mparticle.commerce.CommerceEvent
Expand Down Expand Up @@ -1277,6 +1278,7 @@ class KitManagerImplTest {
@Test
fun testRokt_selectPlacements_with_PlacementOptions() {
val mockUser = mock(MParticleUser::class.java)
stubUserAttributesCallback(mockUser)
`when`(mockIdentity!!.currentUser).thenReturn(mockUser)

val manager: KitManagerImpl = MockKitManagerImpl()
Expand Down Expand Up @@ -1315,6 +1317,7 @@ class KitManagerImplTest {
@Test
fun testRokt_selectPlacements_without_PlacementOptions() {
val mockUser = mock(MParticleUser::class.java)
stubUserAttributesCallback(mockUser)
`when`(mockIdentity!!.currentUser).thenReturn(mockUser)

val manager: KitManagerImpl = MockKitManagerImpl()
Expand Down Expand Up @@ -1760,6 +1763,14 @@ class KitManagerImplTest {
}
}

private fun stubUserAttributesCallback(user: MParticleUser) {
`when`(user.getUserAttributes(any())).thenAnswer { invocation ->
val listener = invocation.arguments[0] as TypedUserAttributeListener
listener.onUserAttributesReceived(emptyMap(), emptyMap(), 0L)
null
}
}

internal inner class MockProvider(
val config: KitConfiguration,
) : KitIntegration(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mparticle.kits

import com.mparticle.MParticle
import com.mparticle.identity.IdentityApi
import com.mparticle.identity.MParticleUser
import com.mparticle.internal.MPUtility
import com.mparticle.mock.MockMParticle
import com.mparticle.rokt.PlacementOptions
Expand All @@ -15,6 +16,7 @@ import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.Mockito.withSettings
Expand Down Expand Up @@ -82,6 +84,61 @@ class RoktKitApiImplTest {
assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"])
}

@Test
fun testSelectPlacements_waitsForUserAttributesBeforeDelegating() {
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))
val kitIntegration =
mock(
KitIntegration::class.java,
withSettings().extraInterfaces(KitIntegration.RoktListener::class.java),
)
`when`(kitIntegration.configuration).thenReturn(kitConfig)
val roktListener = kitIntegration as KitIntegration.RoktListener
val roktApi = RoktKitApiImpl(roktListener, kitIntegration)

val identityApi = mock(IdentityApi::class.java)
val user = mock(MParticleUser::class.java)
`when`(user.id).thenReturn(12345L)
`when`(identityApi.currentUser).thenReturn(user)
val instance = MockMParticle()
instance.setIdentityApi(identityApi)
MParticle.setInstance(instance)

var capturedListener: com.mparticle.TypedUserAttributeListener? = null
`when`(user.getUserAttributes(any())).thenAnswer { invocation ->
capturedListener = invocation.arguments[0] as com.mparticle.TypedUserAttributeListener
null
}

val attributes = mapOf("country" to "US")
roktApi.selectPlacements("Test", attributes, null, null, null, null, null)

verify(user).setUserAttributes(any())
verify(roktListener, never()).selectPlacements(
any(),
any(),
any(),
any(),
any(),
any(),
any(),
any(),
)

capturedListener!!.onUserAttributesReceived(emptyMap(), emptyMap(), 12345L)

verify(roktListener).selectPlacements(
any(),
any(),
any(),
any(),
any(),
any(),
any(),
any(),
)
}

@Test
fun testSelectPlacements_passesPlacementOptions() {
val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42))
Expand Down
Loading